diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java index d35b0f6c1a..4b6dacd95e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java @@ -234,14 +234,6 @@ public class NavbarButtonsViewController { mPropertyHolders.forEach(StatePropertyHolder::endAnimation); } - public void onDestroy() { - mPropertyHolders.clear(); - mControllers.rotationButtonController.unregisterListeners(); - if (mFloatingRotationButton != null) { - mFloatingRotationButton.hide(); - } - } - private void initButtons(ViewGroup navContainer, ViewGroup endContainer, TaskbarNavButtonController navButtonController) { @@ -438,6 +430,14 @@ public class NavbarButtonsViewController { return mFloatingRotationButtonBounds.contains((int) ev.getX(), (int) ev.getY()); } + public void onDestroy() { + mPropertyHolders.clear(); + mControllers.rotationButtonController.unregisterListeners(); + if (mFloatingRotationButton != null) { + mFloatingRotationButton.hide(); + } + } + private class RotationButtonListener implements RotationButton.RotationButtonUpdatesCallback { @Override public void onVisibilityChanged(boolean isVisible) { diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java index d9319a9b00..b6f9d5846d 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.java +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java @@ -37,6 +37,7 @@ import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; +import com.android.launcher3.testing.TestProtocol; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.SplitConfigurationOptions; import com.android.systemui.shared.recents.ISystemUiProxy; @@ -444,6 +445,8 @@ public class SystemUiProxy implements ISystemUiProxy, } catch (RemoteException e) { Log.w(TAG, "Failed call handleImageBundleAsScreenshot"); } + } else if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.NO_SCREENSHOT, "sysuiproxy, no proxy available"); } } diff --git a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java index de7dbd64f5..b0c68c550e 100644 --- a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java +++ b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java @@ -49,6 +49,7 @@ import androidx.core.content.FileProvider; import com.android.internal.app.ChooserActivity; import com.android.launcher3.BuildConfig; +import com.android.launcher3.testing.TestProtocol; import com.android.quickstep.SystemUiProxy; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.utilities.BitmapUtil; @@ -77,6 +78,9 @@ public class ImageActionUtils { public static void saveScreenshot(SystemUiProxy systemUiProxy, Bitmap screenshot, Rect screenshotBounds, Insets visibleInsets, Task.TaskKey task) { + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.NO_SCREENSHOT, "image action utils calling into sysuiproxy"); + } systemUiProxy.handleImageBundleAsScreenshot(BitmapUtil.hardwareBitmapToBundle(screenshot), screenshotBounds, visibleInsets, task); } diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java index 16c3c7f9e9..76d3591325 100644 --- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java +++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java @@ -16,8 +16,6 @@ package com.android.quickstep.views; -import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS; - import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; @@ -32,7 +30,6 @@ import androidx.annotation.Nullable; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Insettable; import com.android.launcher3.R; -import com.android.launcher3.uioverrides.ApiWrapper; import com.android.launcher3.util.MultiValueAlpha; import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; import com.android.quickstep.SysUINavigationMode; @@ -149,7 +146,7 @@ public class OverviewActionsView extends FrameLayo public void setInsets(Rect insets) { mInsets.set(insets); updateVerticalMargin(SysUINavigationMode.getMode(getContext())); - updatePaddingAndTranslations(); + updateHorizontalPadding(); } public void updateHiddenFlags(@ActionsHiddenFlags int visibilityFlags, boolean enable) { @@ -192,37 +189,8 @@ public class OverviewActionsView extends FrameLayo return mMultiValueAlpha.getProperty(INDEX_FULLSCREEN_ALPHA); } - /** - * Aligns OverviewActionsView vertically with and offsets horizontal position based on - * 3 button nav container in taskbar. - */ - private void updatePaddingAndTranslations() { - boolean alignFor3ButtonTaskbar = mDp.isTaskbarPresent && - SysUINavigationMode.getMode(getContext()) == THREE_BUTTONS; - if (alignFor3ButtonTaskbar) { - // Add extra horizontal spacing - int additionalPadding = ApiWrapper.getHotseatEndOffset(getContext()); - if (isLayoutRtl()) { - setPadding(mInsets.left + additionalPadding, 0, mInsets.right, 0); - } else { - setPadding(mInsets.left, 0, mInsets.right + additionalPadding, 0); - } - - // Align vertically, using taskbar height + mDp.taskbarOffsetY() to guestimate - // where the button nav top is - View startActionView = findViewById(R.id.action_buttons); - int marginBottom = getOverviewActionsBottomMarginPx( - SysUINavigationMode.getMode(getContext()), mDp); - int actionsTop = (mDp.heightPx - marginBottom - mInsets.bottom); - int navTop = mDp.heightPx - (mDp.taskbarSize + mDp.getTaskbarOffsetY()); - int transY = navTop - actionsTop - + ((mDp.taskbarSize - startActionView.getHeight()) / 2); - setTranslationY(transY); - } else { - setPadding(mInsets.left, 0, mInsets.right, 0); - setTranslationX(0); - setTranslationY(0); - } + private void updateHorizontalPadding() { + setPadding(mInsets.left, 0, mInsets.right, 0); } /** Updates vertical margins for different navigation mode or configuration changes. */ diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index d825fada57..1a3bfa9958 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -1094,7 +1094,7 @@ public abstract class RecentsView LauncherState.NORMAL); + + AllApps allApps = mLauncher.getWorkspace().switchToAllApps(); + allApps.freeze(); + try { + allApps.getAppIcon(APP_NAME).dragToWorkspace(false, false); + } finally { + allApps.unfreeze(); + } + mLauncher.getWorkspace().getWorkspaceAppIcon(APP_NAME).launch(getAppPackageName()); + mLauncher.pressBack(); + mLauncher.getWorkspace(); + waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL); + } + @Test @PortraitLandscape public void testOverviewForTablet() throws Exception { diff --git a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java index 97052b29c5..dd5812302d 100644 --- a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java +++ b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java @@ -41,11 +41,10 @@ public class AccessibilityManagerCompat { } /** - * * @param target The view the accessibility event is initialized on. * If null, this method has no effect. - * @param type See TYPE_ constants defined in {@link AccessibilityEvent}. - * @param text Optional text to add to the event, which will be announced to the user. + * @param type See TYPE_ constants defined in {@link AccessibilityEvent}. + * @param text Optional text to add to the event, which will be announced to the user. */ public static void sendCustomAccessibilityEvent(@Nullable View target, int type, @Nullable String text) { @@ -97,6 +96,16 @@ public class AccessibilityManagerCompat { null); } + /** + * Notify running tests of a folder opened. + */ + public static void sendFolderOpenedEventToTest(Context context) { + final AccessibilityManager accessibilityManager = getAccessibilityManagerForTest(context); + if (accessibilityManager == null) return; + + sendEventToTest(accessibilityManager, context, TestProtocol.FOLDER_OPENED_MESSAGE, null); + } + private static void sendEventToTest( AccessibilityManager accessibilityManager, Context context, String eventTag, Bundle data) { diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index 879739f171..daef68226d 100644 --- a/src/com/android/launcher3/folder/Folder.java +++ b/src/com/android/launcher3/folder/Folder.java @@ -78,6 +78,7 @@ import com.android.launcher3.Utilities; import com.android.launcher3.accessibility.AccessibleDragListenerAdapter; import com.android.launcher3.accessibility.FolderAccessibilityHelper; import com.android.launcher3.anim.KeyboardInsetAnimationCallback; +import com.android.launcher3.compat.AccessibilityManagerCompat; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragController.DragListener; @@ -687,6 +688,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo public void onAnimationEnd(Animator animation) { mState = STATE_OPEN; announceAccessibilityChanges(); + AccessibilityManagerCompat.sendFolderOpenedEventToTest(getContext()); mContent.setFocusOnFirstChild(); } @@ -1265,7 +1267,8 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo PendingAddShortcutInfo pasi = d.dragInfo instanceof PendingAddShortcutInfo ? (PendingAddShortcutInfo) d.dragInfo : null; - WorkspaceItemInfo pasiSi = pasi != null ? pasi.activityInfo.createWorkspaceItemInfo() : null; + WorkspaceItemInfo pasiSi = + pasi != null ? pasi.activityInfo.createWorkspaceItemInfo() : null; if (pasi != null && pasiSi == null) { // There is no WorkspaceItemInfo, so we have to go through a configuration activity. pasi.container = mInfo.id; diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java index 97398de3ff..b72f462a27 100644 --- a/src/com/android/launcher3/model/data/ItemInfo.java +++ b/src/com/android/launcher3/model/data/ItemInfo.java @@ -373,7 +373,7 @@ public class ItemInfo { protected LauncherAtom.ItemInfo.Builder getDefaultItemInfoBuilder() { LauncherAtom.ItemInfo.Builder itemBuilder = LauncherAtom.ItemInfo.newBuilder(); - itemBuilder.setIsWork(user != Process.myUserHandle()); + itemBuilder.setIsWork(!Process.myUserHandle().equals(user)); return itemBuilder; } diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java index 5a9c074f96..17d925c1f9 100644 --- a/src/com/android/launcher3/testing/TestInformationHandler.java +++ b/src/com/android/launcher3/testing/TestInformationHandler.java @@ -21,6 +21,7 @@ import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; +import android.content.res.Resources; import android.graphics.Insets; import android.os.Build; import android.os.Bundle; @@ -148,6 +149,14 @@ public class TestInformationHandler implements ResourceBasedOverride { TestProtocol.TEST_INFO_RESPONSE_FIELD, TestLogging.sHadEventsNotFromTest); return response; + case TestProtocol.REQUEST_START_DRAG_THRESHOLD: { + final Resources resources = mContext.getResources(); + response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, + resources.getDimensionPixelSize(R.dimen.deep_shortcuts_start_drag_threshold) + + resources.getDimensionPixelSize(R.dimen.pre_drag_view_scale)); + return response; + } + default: return null; } @@ -193,6 +202,7 @@ public class TestInformationHandler implements ResourceBasedOverride { /** * Generic interface for setting a fiend in bundle + * * @param the type of value being set */ public interface BundleSetter { diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java index 5bf0342fcc..db289021a5 100644 --- a/src/com/android/launcher3/testing/TestProtocol.java +++ b/src/com/android/launcher3/testing/TestProtocol.java @@ -25,6 +25,7 @@ public final class TestProtocol { public static final String SCROLL_FINISHED_MESSAGE = "TAPL_SCROLL_FINISHED"; public static final String PAUSE_DETECTED_MESSAGE = "TAPL_PAUSE_DETECTED"; public static final String DISMISS_ANIMATION_ENDS_MESSAGE = "TAPL_DISMISS_ANIMATION_ENDS"; + public static final String FOLDER_OPENED_MESSAGE = "TAPL_FOLDER_OPENED"; public static final int NORMAL_STATE_ORDINAL = 0; public static final int SPRING_LOADED_STATE_ORDINAL = 1; public static final int OVERVIEW_STATE_ORDINAL = 2; @@ -99,6 +100,7 @@ public final class TestProtocol { public static final String REQUEST_CLEAR_DATA = "clear-data"; public static final String REQUEST_IS_TABLET = "is-tablet"; public static final String REQUEST_IS_TWO_PANELS = "is-two-panel"; + public static final String REQUEST_START_DRAG_THRESHOLD = "start-drag-threshold"; public static final String REQUEST_GET_ACTIVITIES_CREATED_COUNT = "get-activities-created-count"; public static final String REQUEST_GET_ACTIVITIES = "get-activities"; @@ -122,4 +124,5 @@ public final class TestProtocol { public static final String TASK_VIEW_ID_CRASH = "b/195430732"; public static final String NO_DROP_TARGET = "b/195031154"; public static final String NULL_INT_SET = "b/200572078"; + public static final String NO_SCREENSHOT = "b/202414125"; } diff --git a/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java b/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java index 9c32e421e6..57f8bc7717 100644 --- a/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java +++ b/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java @@ -16,6 +16,7 @@ package com.android.launcher3.widget; +import android.annotation.SuppressLint; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import android.graphics.Canvas; @@ -30,6 +31,9 @@ import android.widget.RemoteViews; import com.android.launcher3.R; +import java.io.PrintWriter; +import java.io.StringWriter; + /** * A widget host views created while the host has not bind to the system service. */ @@ -75,8 +79,22 @@ public class DeferredAppWidgetHostView extends LauncherAppWidgetHostView { && mSetupTextLayout.getWidth() == availableWidth) { return; } - mSetupTextLayout = new StaticLayout(info.label, mPaint, availableWidth, - Layout.Alignment.ALIGN_CENTER, 1, 0, true); + try { + mSetupTextLayout = new StaticLayout(info.label, mPaint, availableWidth, + Layout.Alignment.ALIGN_CENTER, 1, 0, true); + } catch (IllegalArgumentException e) { + @SuppressLint("DrawAllocation") StringWriter stringWriter = new StringWriter(); + @SuppressLint("DrawAllocation") PrintWriter printWriter = new PrintWriter(stringWriter); + mActivity.getDeviceProfile().dump(/*prefix=*/"", printWriter); + printWriter.flush(); + String message = "b/203530620 " + + "- availableWidth: " + availableWidth + + ", getMeasuredWidth: " + getMeasuredWidth() + + ", getPaddingLeft: " + getPaddingLeft() + + ", getPaddingRight: " + getPaddingRight() + + ", deviceProfile: " + stringWriter.toString(); + throw new IllegalArgumentException(message, e); + } } @Override diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java index 0ffbeeb95e..82163cbfdb 100644 --- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java +++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java @@ -211,7 +211,8 @@ public abstract class AbstractLauncherUiTest { } protected TestRule getRulesInsideActivityMonitor() { - final RuleChain inner = RuleChain.outerRule(new PortraitLandscapeRunner(this)) + final RuleChain inner = RuleChain + .outerRule(new PortraitLandscapeRunner(this)) .around(new FailureWatcher(mDevice, mLauncher)); return TestHelpers.isInLauncherProcess() diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java index 881f50cafb..4007c26f54 100644 --- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java +++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java @@ -34,6 +34,8 @@ import com.android.launcher3.tapl.AllApps; import com.android.launcher3.tapl.AppIcon; import com.android.launcher3.tapl.AppIconMenu; import com.android.launcher3.tapl.AppIconMenuItem; +import com.android.launcher3.tapl.Folder; +import com.android.launcher3.tapl.FolderIcon; import com.android.launcher3.tapl.Widgets; import com.android.launcher3.tapl.Workspace; import com.android.launcher3.views.OptionsPopupView; @@ -369,6 +371,69 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { } } + private AppIcon createShortcutIfNotExist(String name) { + AppIcon appIcon = mLauncher.getWorkspace().tryGetWorkspaceAppIcon(name); + if (appIcon == null) { + AllApps allApps = mLauncher.getWorkspace().switchToAllApps(); + allApps.freeze(); + try { + appIcon = allApps.getAppIcon(name); + appIcon.dragToWorkspace(false, false); + } finally { + allApps.unfreeze(); + } + appIcon = mLauncher.getWorkspace().getWorkspaceAppIcon(name); + } + return appIcon; + } + + @Test + @PortraitLandscape + public void testDragToFolder() throws Exception { + final AppIcon playStoreIcon = createShortcutIfNotExist("Play Store"); + final AppIcon gmailIcon = createShortcutIfNotExist("Gmail"); + + FolderIcon folderIcon = gmailIcon.dragToIcon(playStoreIcon); + + Folder folder = folderIcon.open(); + folder.getAppIcon("Play Store"); + folder.getAppIcon("Gmail"); + Workspace workspace = folder.close(); + + assertNull("Gmail should be moved to a folder.", + workspace.tryGetWorkspaceAppIcon("Gmail")); + assertNull("Play Store should be moved to a folder.", + workspace.tryGetWorkspaceAppIcon("Play Store")); + + final AppIcon youTubeIcon = createShortcutIfNotExist("YouTube"); + + folderIcon = youTubeIcon.dragToIcon(folderIcon); + folder = folderIcon.open(); + folder.getAppIcon("YouTube"); + folder.close(); + } + + @Test + @PortraitLandscape + public void testPressBack() throws Exception { + mLauncher.getWorkspace().switchToAllApps(); + mLauncher.pressBack(); + mLauncher.getWorkspace(); + waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL); + + AllApps allApps = mLauncher.getWorkspace().switchToAllApps(); + allApps.freeze(); + try { + allApps.getAppIcon(APP_NAME).dragToWorkspace(false, false); + } finally { + allApps.unfreeze(); + } + mLauncher.getWorkspace().getWorkspaceAppIcon(APP_NAME).launch(getAppPackageName()); + mLauncher.pressBack(); + mLauncher.getWorkspace(); + waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL); + } + public static String getAppPackageName() { return getInstrumentation().getContext().getPackageName(); } diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java index 21099b4934..6da59da66c 100644 --- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java +++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java @@ -16,8 +16,11 @@ package com.android.launcher3.tapl; +import android.graphics.Point; +import android.graphics.Rect; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.test.uiautomator.By; import androidx.test.uiautomator.BySelector; import androidx.test.uiautomator.UiObject2; @@ -29,7 +32,7 @@ import java.util.regex.Pattern; /** * App icon, whether in all apps or in workspace/ */ -public final class AppIcon extends Launchable { +public final class AppIcon extends Launchable implements FolderDragTarget { private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onAllAppsItemLongClick"); @@ -61,6 +64,29 @@ public final class AppIcon extends Launchable { } } + /** + * Drag the AppIcon to the given position of other icon. The drag must result in a folder. + * + * @param target the destination icon. + */ + @NonNull + public FolderIcon dragToIcon(FolderDragTarget target) { + try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); + LauncherInstrumentation.Closable c = mLauncher.addContextLayer("want to drag icon")) { + final Rect dropBounds = target.getDropLocationBounds(); + Workspace.dragIconToWorkspace( + mLauncher, this, + () -> { + final Rect bounds = target.getDropLocationBounds(); + return new Point(bounds.centerX(), bounds.centerY()); + }, + getLongPressIndicator()); + FolderIcon result = target.getTargetFolder(dropBounds); + mLauncher.assertTrue("Can't find the target folder.", result != null); + return result; + } + } + @Override protected void addExpectedEventsForLongClick() { mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT); @@ -80,4 +106,20 @@ public final class AppIcon extends Launchable { protected String launchableType() { return "app icon"; } + + @Override + public Rect getDropLocationBounds() { + return mLauncher.getVisibleBounds(mObject); + } + + @Override + public FolderIcon getTargetFolder(Rect bounds) { + for (FolderIcon folderIcon : mLauncher.getWorkspace().getFolderIcons()) { + final Rect folderIconBounds = folderIcon.getDropLocationBounds(); + if (bounds.contains(folderIconBounds.centerX(), folderIconBounds.centerY())) { + return folderIcon; + } + } + return null; + } } diff --git a/tests/tapl/com/android/launcher3/tapl/Folder.java b/tests/tapl/com/android/launcher3/tapl/Folder.java new file mode 100644 index 0000000000..dba308dbf7 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/Folder.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.tapl; + +import android.graphics.Point; +import android.graphics.Rect; +import android.os.SystemClock; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.test.uiautomator.UiObject2; + +public class Folder { + + protected static final String FOLDER_CONTENT_RES_ID = "folder_content"; + + private final UiObject2 mContainer; + private final LauncherInstrumentation mLauncher; + + Folder(LauncherInstrumentation launcher) { + this.mLauncher = launcher; + this.mContainer = launcher.waitForLauncherObject(FOLDER_CONTENT_RES_ID); + } + + /** + * Find an app icon with given name or raise assertion error. + */ + @NonNull + public AppIcon getAppIcon(String appName) { + try (LauncherInstrumentation.Closable ignored = mLauncher.addContextLayer( + "Want to get app icon in folder")) { + return new AppIcon(mLauncher, + mLauncher.waitForObjectInContainer( + mContainer, + AppIcon.getAppIconSelector(appName, mLauncher))); + } + } + + private void touchOutsideFolder() { + Rect containerBounds = mLauncher.getVisibleBounds(this.mContainer); + final long downTime = SystemClock.uptimeMillis(); + Point containerLeftTopCorner = new Point(containerBounds.left - 1, containerBounds.top - 1); + mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, + containerLeftTopCorner, LauncherInstrumentation.GestureScope.INSIDE); + mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, + containerLeftTopCorner, LauncherInstrumentation.GestureScope.INSIDE); + } + + /** + * CLose opened folder if possible. It throws assertion error if the folder is already closed. + */ + public Workspace close() { + try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); + LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + "Want to close opened folder")) { + mLauncher.waitForLauncherObject(FOLDER_CONTENT_RES_ID); + touchOutsideFolder(); + mLauncher.waitUntilLauncherObjectGone(FOLDER_CONTENT_RES_ID); + return mLauncher.getWorkspace(); + } + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/FolderDragTarget.java b/tests/tapl/com/android/launcher3/tapl/FolderDragTarget.java new file mode 100644 index 0000000000..d797418ab4 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/FolderDragTarget.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.tapl; + +import android.graphics.Rect; + +public interface FolderDragTarget { + Rect getDropLocationBounds(); + + FolderIcon getTargetFolder(Rect bounds); +} diff --git a/tests/tapl/com/android/launcher3/tapl/FolderIcon.java b/tests/tapl/com/android/launcher3/tapl/FolderIcon.java new file mode 100644 index 0000000000..2e79d70ee6 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/FolderIcon.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.tapl; + +import android.graphics.Rect; + +import androidx.annotation.NonNull; +import androidx.test.uiautomator.UiObject2; + +import com.android.launcher3.testing.TestProtocol; + +/** + * Folder Icon, an app folder in workspace. + */ +public class FolderIcon implements FolderDragTarget { + + protected final UiObject2 mObject; + protected final LauncherInstrumentation mLauncher; + + FolderIcon(LauncherInstrumentation launcher, UiObject2 icon) { + mObject = icon; + mLauncher = launcher; + } + + /** + * Open and return a folder or raise assertion error. + */ + @NonNull + public Folder open() { + try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); + LauncherInstrumentation.Closable c = mLauncher.addContextLayer("open folder")) { + mLauncher.executeAndWaitForLauncherEvent(() -> mLauncher.clickLauncherObject(mObject), + event -> TestProtocol.FOLDER_OPENED_MESSAGE.equals( + event.getClassName().toString()), + () -> "Fail to open folder.", + "open folder"); + } + return new Folder(mLauncher); + } + + @Override + public Rect getDropLocationBounds() { + return mLauncher.getVisibleBounds(mObject.getParent()); + } + + @Override + public FolderIcon getTargetFolder(Rect bounds) { + return this; + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java index 7ffdf4cef8..e2d023825c 100644 --- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java +++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java @@ -21,6 +21,7 @@ import static android.content.pm.PackageManager.DONT_KILL_APP; import static android.content.pm.PackageManager.MATCH_ALL; import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS; +import static com.android.launcher3.tapl.Folder.FOLDER_CONTENT_RES_ID; import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName; import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL; @@ -75,11 +76,13 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.LinkedList; import java.util.List; +import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; @@ -108,6 +111,9 @@ public final class LauncherInstrumentation { static final Pattern EVENT_TOUCH_DOWN_TIS = getTouchEventPatternTIS("ACTION_DOWN"); static final Pattern EVENT_TOUCH_UP_TIS = getTouchEventPatternTIS("ACTION_UP"); + static final Pattern EVENT_KEY_BACK_DOWN = getKeyEventPattern("ACTION_DOWN", "KEYCODE_BACK"); + static final Pattern EVENT_KEY_BACK_UP = getKeyEventPattern("ACTION_UP", "KEYCODE_BACK"); + private final String mLauncherPackage; private Boolean mIsLauncher3; private long mTestStartTime = -1; @@ -123,7 +129,8 @@ public final class LauncherInstrumentation { // Where the gesture happens: outside of Launcher, inside or from inside to outside and // whether the gesture recognition triggers pilfer. public enum GestureScope { - OUTSIDE_WITHOUT_PILFER, OUTSIDE_WITH_PILFER, INSIDE, INSIDE_TO_OUTSIDE + OUTSIDE_WITHOUT_PILFER, OUTSIDE_WITH_PILFER, INSIDE, INSIDE_TO_OUTSIDE, + INSIDE_TO_OUTSIDE_WITHOUT_PILFER, } // Base class for launcher containers. @@ -193,6 +200,10 @@ public final class LauncherInstrumentation { return getTouchEventPattern("TouchInteractionService.onInputEvent", action); } + private static Pattern getKeyEventPattern(String action, String keyCode) { + return Pattern.compile("Key event: KeyEvent.*action=" + action + ".*keyCode=" + keyCode); + } + /** * Constructs the root of TAPL hierarchy. You get all other objects from it. */ @@ -767,6 +778,47 @@ public final class LauncherInstrumentation { } } + /** + * Get the resource ID of visible floating view. + */ + private Optional getFloatingResId() { + if (hasLauncherObject(CONTEXT_MENU_RES_ID)) { + return Optional.of(CONTEXT_MENU_RES_ID); + } + if (hasLauncherObject(FOLDER_CONTENT_RES_ID)) { + return Optional.of(FOLDER_CONTENT_RES_ID); + } + return Optional.empty(); + } + + /** + * Using swiping up gesture to dismiss closable floating views, such as Menu or Folder Content. + */ + private void swipeUpToCloseFloatingView(boolean gestureStartFromLauncher) { + final Point displaySize = getRealDisplaySize(); + + final Optional floatingRes = getFloatingResId(); + + if (!floatingRes.isPresent()) { + return; + } + + GestureScope gestureScope = gestureStartFromLauncher + ? (isTablet() ? GestureScope.INSIDE : GestureScope.INSIDE_TO_OUTSIDE) + : GestureScope.OUTSIDE_WITH_PILFER; + linearGesture( + displaySize.x / 2, displaySize.y - 1, + displaySize.x / 2, 0, + ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, + false, gestureScope); + + try (LauncherInstrumentation.Closable c1 = addContextLayer( + String.format("Swiped up from floating view %s to home", floatingRes.get()))) { + waitUntilLauncherObjectGone(floatingRes.get()); + waitForLauncherObject(getAnyObjectSelector()); + } + } + /** * Presses nav bar home button. * @@ -791,21 +843,9 @@ public final class LauncherInstrumentation { ? !isLauncher3() || hasLauncherObject(WORKSPACE_RES_ID) : isLauncherVisible(); - if (hasLauncherObject(CONTEXT_MENU_RES_ID)) { - GestureScope gestureScope = gestureStartFromLauncher - ? (isTablet() ? GestureScope.INSIDE : GestureScope.INSIDE_TO_OUTSIDE) - : GestureScope.OUTSIDE_WITH_PILFER; - linearGesture( - displaySize.x / 2, displaySize.y - 1, - displaySize.x / 2, 0, - ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, - false, gestureScope); - try (LauncherInstrumentation.Closable c1 = addContextLayer( - "Swiped up from context menu to home")) { - waitUntilLauncherObjectGone(CONTEXT_MENU_RES_ID); - waitForLauncherObject(getAnyObjectSelector()); - } - } + // CLose floating views before going back to home. + swipeUpToCloseFloatingView(gestureStartFromLauncher); + if (hasLauncherObject(WORKSPACE_RES_ID)) { log(action = "already at home"); } else { @@ -848,6 +888,38 @@ public final class LauncherInstrumentation { } } + /** + * Press navbar back button or swipe back if in gesture navigation mode. + */ + public void pressBack() { + try (Closable e = eventsCheck(); Closable c = addContextLayer("want to press back")) { + waitForLauncherInitialized(); + final boolean launcherVisible = + isTablet() ? isLauncherContainerVisible() : isLauncherVisible(); + if (getNavigationModel() == NavigationModel.ZERO_BUTTON) { + final Point displaySize = getRealDisplaySize(); + final GestureScope gestureScope = + launcherVisible ? GestureScope.INSIDE_TO_OUTSIDE_WITHOUT_PILFER + : GestureScope.OUTSIDE_WITHOUT_PILFER; + linearGesture(0, displaySize.y / 2, displaySize.x / 2, displaySize.y / 2, + 10, false, gestureScope); + } else { + waitForNavigationUiObject("back").click(); + if (isTablet()) { + expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN); + expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_UP); + } else if (!isLauncher3() && getNavigationModel() == NavigationModel.TWO_BUTTON) { + expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS); + expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS); + } + } + if (launcherVisible) { + expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KEY_BACK_DOWN); + expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KEY_BACK_UP); + } + } + } + private static BySelector getAnyObjectSelector() { return By.textStartsWith(""); } @@ -857,6 +929,11 @@ public final class LauncherInstrumentation { return hasLauncherObject(getAnyObjectSelector()); } + boolean isLauncherContainerVisible() { + final String[] containerResources = {WORKSPACE_RES_ID, OVERVIEW_RES_ID, APPS_RES_ID}; + return Arrays.stream(containerResources).anyMatch(r -> hasLauncherObject(r)); + } + /** * Gets the Workspace object if the current state is "active home", i.e. workspace. Fails if the * launcher is not in that state. @@ -1383,6 +1460,7 @@ public final class LauncherInstrumentation { break; case MotionEvent.ACTION_UP: if (notLauncher3 && gestureScope != GestureScope.INSIDE + && gestureScope != GestureScope.INSIDE_TO_OUTSIDE_WITHOUT_PILFER && (gestureScope == GestureScope.OUTSIDE_WITH_PILFER || gestureScope == GestureScope.INSIDE_TO_OUTSIDE)) { expectEvent(TestProtocol.SEQUENCE_PILFER, EVENT_PILFER_POINTERS); diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java index 288c85319b..0145690909 100644 --- a/tests/tapl/com/android/launcher3/tapl/Workspace.java +++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java @@ -36,7 +36,10 @@ import androidx.test.uiautomator.UiObject2; import com.android.launcher3.testing.TestProtocol; +import java.util.List; +import java.util.function.Supplier; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Operations on the workspace screen. @@ -170,40 +173,100 @@ public final class Workspace extends Home { mHotseat, AppIcon.getAppIconSelector(appName, mLauncher))); } - static void dragIconToWorkspace( - LauncherInstrumentation launcher, Launchable launchable, Point dest, - String longPressIndicator, boolean startsActivity, boolean isWidgetShortcut, - Runnable expectLongClickEvents) { - LauncherInstrumentation.log("dragIconToWorkspace: begin"); - final Point launchableCenter = launchable.getObject().getVisibleCenter(); - final long downTime = SystemClock.uptimeMillis(); - launcher.runToState( - () -> { - launcher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, - launchableCenter, LauncherInstrumentation.GestureScope.INSIDE); - LauncherInstrumentation.log("dragIconToWorkspace: sent down"); - expectLongClickEvents.run(); - launcher.waitForLauncherObject(longPressIndicator); - LauncherInstrumentation.log("dragIconToWorkspace: indicator"); - launcher.movePointer(launchableCenter, dest, 10, downTime, true, - LauncherInstrumentation.GestureScope.INSIDE); - }, - SPRING_LOADED_STATE_ORDINAL, - "long-pressing and moving"); - LauncherInstrumentation.log("dragIconToWorkspace: moved pointer"); + private static int getStartDragThreshold(LauncherInstrumentation launcher) { + return launcher.getTestInfo(TestProtocol.REQUEST_START_DRAG_THRESHOLD).getInt( + TestProtocol.TEST_INFO_RESPONSE_FIELD); + } + + /** + * Finds folder icons in the current workspace. + * + * @return a list of folder icons. + */ + List getFolderIcons() { + final UiObject2 workspace = verifyActiveContainer(); + return mLauncher.getObjectsInContainer(workspace, "folder_icon_name").stream().map( + o -> new FolderIcon(mLauncher, o)).collect(Collectors.toList()); + } + + /** + * Drag an icon up with a short distance that makes workspace go to spring loaded state. + * + * @return the position after dragging. + */ + private static Point dragIconToSpringLoaded(LauncherInstrumentation launcher, long downTime, + UiObject2 icon, + String longPressIndicator, Runnable expectLongClickEvents) { + final Point iconCenter = icon.getVisibleCenter(); + final Point dragStartCenter = new Point(iconCenter.x, + iconCenter.y - getStartDragThreshold(launcher)); + + launcher.runToState(() -> { + launcher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, + iconCenter, LauncherInstrumentation.GestureScope.INSIDE); + LauncherInstrumentation.log("dragIconToSpringLoaded: sent down"); + expectLongClickEvents.run(); + launcher.waitForLauncherObject(longPressIndicator); + LauncherInstrumentation.log("dragIconToSpringLoaded: indicator"); + launcher.movePointer(iconCenter, dragStartCenter, 10, downTime, true, + LauncherInstrumentation.GestureScope.INSIDE); + }, SPRING_LOADED_STATE_ORDINAL, "long-pressing and triggering drag start"); + return dragStartCenter; + } + + private static void dropDraggedIcon(LauncherInstrumentation launcher, Point dest, long downTime, + @Nullable Runnable expectedEvents) { launcher.runToState( () -> launcher.sendPointer( downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest, LauncherInstrumentation.GestureScope.INSIDE), NORMAL_STATE_ORDINAL, "sending UP event"); - if (startsActivity || isWidgetShortcut) { - launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_START); + if (expectedEvents != null) { + expectedEvents.run(); } - LauncherInstrumentation.log("dragIconToWorkspace: end"); + LauncherInstrumentation.log("dropIcon: end"); launcher.waitUntilLauncherObjectGone("drop_target_bar"); } + static void dragIconToWorkspace(LauncherInstrumentation launcher, Launchable launchable, + Point dest, String longPressIndicator, boolean startsActivity, boolean isWidgetShortcut, + Runnable expectLongClickEvents) { + Runnable expectDropEvents = null; + if (startsActivity || isWidgetShortcut) { + expectDropEvents = () -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, + LauncherInstrumentation.EVENT_START); + } + dragIconToWorkspace(launcher, launchable, () -> dest, longPressIndicator, + expectLongClickEvents, expectDropEvents); + } + + /** + * Drag icon in workspace to else where. + * This function expects the launchable is inside the workspace and there is no drop event. + */ + static void dragIconToWorkspace(LauncherInstrumentation launcher, Launchable launchable, + Supplier destSupplier, String longPressIndicator) { + dragIconToWorkspace(launcher, launchable, destSupplier, longPressIndicator, + () -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT), null); + } + + static void dragIconToWorkspace( + LauncherInstrumentation launcher, Launchable launchable, Supplier dest, + String longPressIndicator, Runnable expectLongClickEvents, + @Nullable Runnable expectDropEvents) { + try (LauncherInstrumentation.Closable ignored = launcher.addContextLayer( + "want to drag icon to workspace")) { + final long downTime = SystemClock.uptimeMillis(); + final Point dragStartCenter = dragIconToSpringLoaded(launcher, downTime, + launchable.getObject(), longPressIndicator, expectLongClickEvents); + final Point targetDest = dest.get(); + launcher.movePointer(dragStartCenter, targetDest, 10, downTime, true, + LauncherInstrumentation.GestureScope.INSIDE); + dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents); + } + } + /** * Flings to get to screens on the right. Waits for scrolling and a possible overscroll * recoil to complete.