From c4a99bff04c54283516588c1f692504020a63c09 Mon Sep 17 00:00:00 2001 From: Jon Miranda Date: Thu, 4 Nov 2021 16:53:45 -0700 Subject: [PATCH 1/5] Play return to icon animation if user swipes back to All Apps. Bug: 205187702 Test: open app in all apps, swipe back to go back to all apps Change-Id: I9e79c8365fdf667321343aaac2f8d77cb60a316a --- .../launcher3/QuickstepTransitionManager.java | 64 +++++++-------- .../quickstep/LauncherSwipeHandlerV2.java | 2 +- src/com/android/launcher3/Launcher.java | 78 +++++++++++++++++++ src/com/android/launcher3/Workspace.java | 75 ------------------ .../launcher3/util/ItemInfoMatcher.java | 13 +++- .../launcher3/views/FloatingSurfaceView.java | 4 +- 6 files changed, 125 insertions(+), 111 deletions(-) diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java index 51d7914ee8..0123c4f5b1 100644 --- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java +++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java @@ -1214,14 +1214,14 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } /** - * Returns view on the workspace that corresponds to the closing app in the list of app targets + * Returns view on launcher that corresponds to the closing app in the list of app targets */ - private @Nullable View findWorkspaceView(RemoteAnimationTargetCompat[] appTargets) { + private @Nullable View findLauncherView(RemoteAnimationTargetCompat[] appTargets) { for (RemoteAnimationTargetCompat appTarget : appTargets) { if (appTarget.mode == MODE_CLOSING) { - View workspaceView = findWorkspaceView(appTarget); - if (workspaceView != null) { - return workspaceView; + View launcherView = findLauncherView(appTarget); + if (launcherView != null) { + return launcherView; } } } @@ -1229,9 +1229,9 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } /** - * Returns view on the workspace that corresponds to the {@param runningTaskTarget}. + * Returns view on launcher that corresponds to the {@param runningTaskTarget}. */ - private @Nullable View findWorkspaceView(RemoteAnimationTargetCompat runningTaskTarget) { + private @Nullable View findLauncherView(RemoteAnimationTargetCompat runningTaskTarget) { if (runningTaskTarget == null || runningTaskTarget.taskInfo == null) { return null; } @@ -1269,7 +1269,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } } - return mLauncher.getWorkspace().getFirstMatchForAppClose(launchCookieItemId, + return mLauncher.getFirstMatchForAppClose(launchCookieItemId, packageName, UserHandle.of(runningTaskTarget.taskInfo.userId)); } @@ -1292,7 +1292,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * Closing animator that animates the window into its final location on the workspace. */ private void getClosingWindowAnimators(AnimatorSet animation, - RemoteAnimationTargetCompat[] targets, View workspaceView) { + RemoteAnimationTargetCompat[] targets, View launcherView, PointF velocityPxPerS) { FloatingIconView floatingIconView = null; FloatingWidgetView floatingWidget = null; RectF targetRect = new RectF(); @@ -1308,17 +1308,17 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } // Get floating view and target rect. - if (workspaceView instanceof LauncherAppWidgetHostView) { + if (launcherView instanceof LauncherAppWidgetHostView) { Size windowSize = new Size(mDeviceProfile.availableWidthPx, mDeviceProfile.availableHeightPx); int fallbackBackgroundColor = FloatingWidgetView.getDefaultBackgroundColor(mLauncher, runningTaskTarget); floatingWidget = FloatingWidgetView.getFloatingWidgetView(mLauncher, - (LauncherAppWidgetHostView) workspaceView, targetRect, windowSize, + (LauncherAppWidgetHostView) launcherView, targetRect, windowSize, mDeviceProfile.isMultiWindowMode ? 0 : getWindowCornerRadius(mLauncher), isTransluscent, fallbackBackgroundColor); - } else if (workspaceView != null) { - floatingIconView = getFloatingIconView(mLauncher, workspaceView, + } else if (launcherView != null) { + floatingIconView = getFloatingIconView(mLauncher, launcherView, true /* hideOriginal */, targetRect, false /* isOpening */); } else { targetRect.set(getDefaultWindowTargetRect()); @@ -1373,15 +1373,10 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } // Use a fixed velocity to start the animation. - float velocityPxPerS = DynamicResource.provider(mLauncher) - .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s); - PointF velocity = new PointF(0, -velocityPxPerS); - animation.play(new StaggeredWorkspaceAnim(mLauncher, velocity.y, - true /* animateOverviewScrim */, workspaceView).getAnimators()); animation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { - anim.start(mLauncher, velocity); + anim.start(mLauncher, velocityPxPerS); } }); } @@ -1556,22 +1551,30 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener if (anim == null) { anim = new AnimatorSet(); - View workspaceView = findWorkspaceView(appTargets); - boolean isWorkspaceViewVisible = workspaceView != null - && !mLauncher.isInState(LauncherState.ALL_APPS) - && !mLauncher.getWorkspace().isOverlayShown(); - boolean playFallBackAnimation = !isWorkspaceViewVisible - && (launcherIsATargetWithMode(appTargets, MODE_OPENING) - || mLauncher.isForceInvisible()); + final boolean launcherIsForceInvisibleOrOpening = mLauncher.isForceInvisible() + || launcherIsATargetWithMode(appTargets, MODE_OPENING); + + View launcherView = findLauncherView(appTargets); + boolean playFallBackAnimation = (launcherView == null + && launcherIsForceInvisibleOrOpening) + || mLauncher.getWorkspace().isOverlayShown(); boolean playWorkspaceReveal = true; if (mFromUnlock) { anim.play(getUnlockWindowAnimator(appTargets, wallpaperTargets)); } else if (ENABLE_BACK_SWIPE_HOME_ANIMATION.get() && !playFallBackAnimation) { - getClosingWindowAnimators(anim, appTargets, workspaceView); - // We play StaggeredWorkspaceAnim as a part of the closing window animation. - playWorkspaceReveal = false; + // Use a fixed velocity to start the animation. + float velocityPxPerS = DynamicResource.provider(mLauncher) + .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s); + PointF velocity = new PointF(0, -velocityPxPerS); + getClosingWindowAnimators(anim, appTargets, launcherView, velocity); + if (!mLauncher.isInState(LauncherState.ALL_APPS)) { + anim.play(new StaggeredWorkspaceAnim(mLauncher, velocity.y, + true /* animateOverviewScrim */, launcherView).getAnimators()); + // We play StaggeredWorkspaceAnim as a part of the closing window animation. + playWorkspaceReveal = false; + } } else { anim.play(getFallbackClosingWindowAnimators(appTargets)); } @@ -1584,8 +1587,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener // targets list because it is already visible). In that case, we force // invisibility on touch down, and only reset it after the animation to home // is initialized. - if (launcherIsATargetWithMode(appTargets, MODE_OPENING) - || mLauncher.isForceInvisible()) { + if (launcherIsForceInvisibleOrOpening) { addCujInstrumentation( anim, InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME); // Only register the content animation for cancellation when state changes diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java index 8a30aad90a..8415665af8 100644 --- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java +++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java @@ -264,7 +264,7 @@ public class LauncherSwipeHandlerV2 extends } } - return mActivity.getWorkspace().getFirstMatchForAppClose(launchCookieItemId, + return mActivity.getFirstMatchForAppClose(launchCookieItemId, runningTaskView.getTask().key.getComponent().getPackageName(), UserHandle.of(runningTaskView.getTask().key.userId)); } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 8154168e4f..d60cd311f9 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -29,6 +29,7 @@ import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE; import static com.android.launcher3.AbstractFloatingView.TYPE_SNACKBAR; import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType; import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY; +import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.FLAG_CLOSE_POPUPS; import static com.android.launcher3.LauncherState.FLAG_MULTI_PAGE; @@ -39,6 +40,7 @@ import static com.android.launcher3.LauncherState.NO_SCALE; import static com.android.launcher3.LauncherState.SPRING_LOADED; import static com.android.launcher3.Utilities.postAsyncCallback; import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.getSupportedActions; +import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM; import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_LAUNCHER_LOAD; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME; @@ -55,6 +57,7 @@ import static com.android.launcher3.popup.SystemShortcut.INSTALL; import static com.android.launcher3.popup.SystemShortcut.WIDGETS; import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK; import static com.android.launcher3.states.RotationHelper.REQUEST_NONE; +import static com.android.launcher3.util.ItemInfoMatcher.forFolderMatch; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -90,6 +93,7 @@ import android.os.Process; import android.os.StrictMode; import android.os.SystemClock; import android.os.Trace; +import android.os.UserHandle; import android.text.TextUtils; import android.text.method.TextKeyListener; import android.util.Log; @@ -215,6 +219,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -2674,6 +2679,79 @@ public class Launcher extends StatefulActivity implements Launche } } + /** + * Similar to {@link #getFirstMatch} but optimized to finding a suitable view for the app close + * animation. + * + * @param preferredItemId The id of the preferred item to match to if it exists. + * @param packageName The package name of the app to match. + * @param user The user of the app to match. + */ + public View getFirstMatchForAppClose(int preferredItemId, String packageName, UserHandle user) { + final ItemInfoMatcher preferredItem = (info, cn) -> + info != null && info.id == preferredItemId; + final ItemInfoMatcher packageAndUserAndApp = (info, cn) -> + info != null + && info.itemType == ITEM_TYPE_APPLICATION + && info.user.equals(user) + && info.getTargetComponent() != null + && TextUtils.equals(info.getTargetComponent().getPackageName(), + packageName); + + if (isInState(LauncherState.ALL_APPS)) { + return getFirstMatch(Collections.singletonList(mAppsView.getActiveRecyclerView()), + preferredItem, packageAndUserAndApp); + } else { + List containers = new ArrayList<>(mWorkspace.getPanelCount() + 1); + containers.add(mWorkspace.getHotseat().getShortcutsAndWidgets()); + mWorkspace.forEachVisiblePage(page + -> containers.add(((CellLayout) page).getShortcutsAndWidgets())); + + // Order: Preferred item by itself or in folder, then by matching package/user + if (ADAPTIVE_ICON_WINDOW_ANIM.get()) { + return getFirstMatch(containers, preferredItem, forFolderMatch(preferredItem), + packageAndUserAndApp, forFolderMatch(packageAndUserAndApp)); + } else { + // Do not use Folder as a criteria, since it'll cause a crash when trying to draw + // FolderAdaptiveIcon as the background. + return getFirstMatch(containers, preferredItem, packageAndUserAndApp); + } + } + } + + /** + * Finds the first view matching the ordered operators across the given viewgroups in order. + * @param containers List of ViewGroups to scan, in order of preference. + * @param operators List of operators, in order starting from best matching operator. + */ + private static View getFirstMatch(Iterable containers, + final ItemInfoMatcher... operators) { + for (ItemInfoMatcher operator : operators) { + for (ViewGroup container : containers) { + View match = mapOverViewGroup(container, operator); + if (match != null) { + return match; + } + } + } + return null; + } + + /** + * Returns the first view matching the operator in the given ViewGroups, or null if none. + * Forward iteration matters. + */ + private static View mapOverViewGroup(ViewGroup container, ItemInfoMatcher op) { + final int itemCount = container.getChildCount(); + for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) { + View item = container.getChildAt(itemIdx); + if (op.matchesInfo((ItemInfo) item.getTag())) { + return item; + } + } + return null; + } + private ValueAnimator createNewAppBounceAnimation(View v, int i) { ValueAnimator bounceAnim = new PropertyListBuilder().alpha(1).scale(1).build(v) .setDuration(ItemInstallQueue.NEW_SHORTCUT_BOUNCE_DURATION); diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 131fbfb071..fbaf71bc4e 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -26,7 +26,6 @@ import static com.android.launcher3.LauncherState.HINT_STATE; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.SPRING_LOADED; import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback; -import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM; import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_OVERLAY; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPELEFT; @@ -51,7 +50,6 @@ import android.os.Handler; import android.os.Message; import android.os.Parcelable; import android.os.UserHandle; -import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; @@ -3148,62 +3146,6 @@ public class Workspace extends PagedView return layouts; } - /** - * Similar to {@link #getFirstMatch} but optimized to finding a suitable view for the app close - * animation. - * - * @param preferredItemId The id of the preferred item to match to if it exists. - * @param packageName The package name of the app to match. - * @param user The user of the app to match. - */ - public View getFirstMatchForAppClose(int preferredItemId, String packageName, UserHandle user) { - final ItemOperator preferredItem = (ItemInfo info, View view) -> - info != null && info.id == preferredItemId; - final ItemOperator preferredItemInFolder = (info, view) -> { - if (info instanceof FolderInfo) { - FolderInfo folderInfo = (FolderInfo) info; - for (WorkspaceItemInfo shortcutInfo : folderInfo.contents) { - if (preferredItem.evaluate(shortcutInfo, view)) { - return true; - } - } - } - return false; - }; - final ItemOperator packageAndUserAndApp = (ItemInfo info, View view) -> - info != null - && info.itemType == ITEM_TYPE_APPLICATION - && info.user.equals(user) - && info.getTargetComponent() != null - && TextUtils.equals(info.getTargetComponent().getPackageName(), - packageName); - final ItemOperator packageAndUserAndAppInFolder = (info, view) -> { - if (info instanceof FolderInfo) { - FolderInfo folderInfo = (FolderInfo) info; - for (WorkspaceItemInfo shortcutInfo : folderInfo.contents) { - if (packageAndUserAndApp.evaluate(shortcutInfo, view)) { - return true; - } - } - } - return false; - }; - - List cellLayouts = new ArrayList<>(getPanelCount() + 1); - cellLayouts.add(getHotseat()); - forEachVisiblePage(page -> cellLayouts.add((CellLayout) page)); - - // Order: Preferred item, App icons in hotseat/workspace, app in folder in hotseat/workspace - if (ADAPTIVE_ICON_WINDOW_ANIM.get()) { - return getFirstMatch(cellLayouts, preferredItem, preferredItemInFolder, - packageAndUserAndApp, packageAndUserAndAppInFolder); - } else { - // Do not use Folder as a criteria, since it'll cause a crash when trying to draw - // FolderAdaptiveIcon as the background. - return getFirstMatch(cellLayouts, preferredItem, packageAndUserAndApp); - } - } - public View getHomescreenIconByItemId(final int id) { return getFirstMatch((info, v) -> info != null && info.id == id); } @@ -3229,23 +3171,6 @@ public class Workspace extends PagedView return value[0]; } - /** - * Finds the first view matching the ordered operators across the given cell layouts by order. - * @param cellLayouts List of CellLayouts to scan, in order of preference. - * @param operators List of operators, in order starting from best matching operator. - */ - View getFirstMatch(Iterable cellLayouts, final ItemOperator... operators) { - for (ItemOperator operator : operators) { - for (CellLayout cellLayout : cellLayouts) { - View match = mapOverCellLayout(cellLayout, operator); - if (match != null) { - return match; - } - } - } - return null; - } - void clearDropTargets() { mapOverItems(new ItemOperator() { @Override diff --git a/src/com/android/launcher3/util/ItemInfoMatcher.java b/src/com/android/launcher3/util/ItemInfoMatcher.java index ab3083de0f..7917410b3b 100644 --- a/src/com/android/launcher3/util/ItemInfoMatcher.java +++ b/src/com/android/launcher3/util/ItemInfoMatcher.java @@ -20,6 +20,7 @@ import android.content.ComponentName; import android.os.UserHandle; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.shortcuts.ShortcutKey; @@ -85,8 +86,16 @@ public interface ItemInfoMatcher { } static ItemInfoMatcher ofShortcutKeys(Set keys) { - return (info, cn) -> info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT && - keys.contains(ShortcutKey.fromItemInfo(info)); + return (info, cn) -> info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT + && keys.contains(ShortcutKey.fromItemInfo(info)); + } + + /** + * Returns a matcher for items within folders. + */ + static ItemInfoMatcher forFolderMatch(ItemInfoMatcher childOperator) { + return (info, cn) -> info instanceof FolderInfo && ((FolderInfo) info).contents.stream() + .anyMatch(childOperator::matchesInfo); } /** diff --git a/src/com/android/launcher3/views/FloatingSurfaceView.java b/src/com/android/launcher3/views/FloatingSurfaceView.java index e2e3be7976..f32f904981 100644 --- a/src/com/android/launcher3/views/FloatingSurfaceView.java +++ b/src/com/android/launcher3/views/FloatingSurfaceView.java @@ -158,7 +158,7 @@ public class FloatingSurfaceView extends AbstractFloatingView implements if (mContract == null) { return; } - View icon = mLauncher.getWorkspace().getFirstMatchForAppClose(-1, + View icon = mLauncher.getFirstMatchForAppClose(-1, mContract.componentName.getPackageName(), mContract.user); boolean iconChanged = mIcon != icon; @@ -182,7 +182,7 @@ public class FloatingSurfaceView extends AbstractFloatingView implements lp.topMargin = Math.round(mIconPosition.top); } } - if (iconChanged && !mIconBounds.isEmpty()) { + if (mIcon != null && iconChanged && !mIconBounds.isEmpty()) { // Record the icon display setCurrentIconVisible(true); Canvas c = mPicture.beginRecording(mIconBounds.width(), mIconBounds.height()); From fc846eef41b0d0773cf0c5a5300f0cf8eccb40f8 Mon Sep 17 00:00:00 2001 From: Schneider Victor-tulias Date: Wed, 1 Dec 2021 16:04:33 -0800 Subject: [PATCH 2/5] Update taskbar edu strings for i10n. Fixes: 200777300 Test: none Change-Id: I9d6e2575940d2fcdd354c94036662c178f9d6eee --- quickstep/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml index 151b8e4604..a4bbae02ac 100644 --- a/quickstep/res/values/strings.xml +++ b/quickstep/res/values/strings.xml @@ -218,10 +218,10 @@ Taskbar education closed - Use the taskbar to switch apps + Use the taskbar to switch apps - Drag to the side to use two apps at once + Drag to the side to use two apps at once Touch & hold to hide the taskbar From c820151a4cc84d70aa419e5df75d332c5e925f88 Mon Sep 17 00:00:00 2001 From: Andras Kloczl Date: Wed, 24 Nov 2021 16:01:39 +0000 Subject: [PATCH 3/5] Add TAPL tests for two panel workspace - Add test for icon dragging - Add test for page pairing - Add test for empty page removal - Add dragIcon helper method Test: TwoPanelWorkspaceTest Bug: 197631877 Change-Id: I7ce2dd88e183626feac49b34254d00a589473944 --- .../launcher3/ui/TaplTestsLauncher3.java | 12 +- .../ui/workspace/TwoPanelWorkspaceTest.java | 285 ++++++++++++++++++ .../com/android/launcher3/tapl/Workspace.java | 75 ++++- 3 files changed, 351 insertions(+), 21 deletions(-) create mode 100644 tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java index c90d283ae8..41c7c37032 100644 --- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java +++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java @@ -80,12 +80,8 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { assertTrue(message, failed); } - private int pagesPerScreen() { - return mLauncher.isTwoPanels() ? 2 : 1; - } - - private boolean isWorkspaceScrollable(Launcher launcher) { - return launcher.getWorkspace().getPageCount() > pagesPerScreen(); + public static boolean isWorkspaceScrollable(Launcher launcher) { + return launcher.getWorkspace().getPageCount() > launcher.getWorkspace().getPanelCount(); } private int getCurrentWorkspacePage(Launcher launcher) { @@ -192,7 +188,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { executeOnLauncher( launcher -> assertEquals( "Ensuring workspace scrollable didn't switch to next screen", - pagesPerScreen(), getCurrentWorkspacePage(launcher))); + workspace.pagesPerScreen(), getCurrentWorkspacePage(launcher))); executeOnLauncher( launcher -> assertTrue("ensureScrollable didn't make workspace scrollable", isWorkspaceScrollable(launcher))); @@ -209,7 +205,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { workspace.flingForward(); executeOnLauncher( launcher -> assertEquals("Flinging forward didn't switch workspace to next screen", - pagesPerScreen(), getCurrentWorkspacePage(launcher))); + workspace.pagesPerScreen(), getCurrentWorkspacePage(launcher))); assertTrue("Launcher internal state is not Home", isInState(() -> LauncherState.NORMAL)); // Test starting a workspace app. diff --git a/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java new file mode 100644 index 0000000000..b048cd4d43 --- /dev/null +++ b/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java @@ -0,0 +1,285 @@ +/* + * 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.ui.workspace; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.launcher3.CellLayout; +import com.android.launcher3.Launcher; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.tapl.Workspace; +import com.android.launcher3.ui.AbstractLauncherUiTest; +import com.android.launcher3.ui.TaplTestsLauncher3; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Tests for two panel workspace. + * + * Note running these tests will clear the workspace on the device. + */ +@LargeTest +@RunWith(AndroidJUnit4.class) +public class TwoPanelWorkspaceTest extends AbstractLauncherUiTest { + + Workspace mWorkspace; + + @Before + public void setUp() throws Exception { + super.setUp(); + TaplTestsLauncher3.initialize(this); + mWorkspace = mLauncher.getWorkspace(); + } + + @Test + public void testDragIconToRightPanel() { + if (!mLauncher.isTwoPanels()) { + return; + } + + // Pre verifying the screens + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1); + assertItemsOnPage(launcher, 0, "Play Store", "Maps"); + assertPageEmpty(launcher, 1); + }); + + mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Chrome"), 1); + + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1); + assertItemsOnPage(launcher, 0, "Maps", "Play Store"); + assertItemsOnPage(launcher, 1, "Chrome"); + }); + } + + @Test + public void testDragIconToPage2() { + if (!mLauncher.isTwoPanels()) { + return; + } + + // Pre verifying the screens + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1); + assertItemsOnPage(launcher, 0, "Play Store", "Maps"); + assertPageEmpty(launcher, 1); + }); + + mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), 2); + + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1, 2, 3); + assertItemsOnPage(launcher, 0, "Play Store"); + assertPageEmpty(launcher, 1); + assertItemsOnPage(launcher, 2, "Maps"); + assertPageEmpty(launcher, 3); + }); + } + + @Test + public void testDragIconToPage3() { + if (!mLauncher.isTwoPanels()) { + return; + } + + // Pre verifying the screens + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1); + assertItemsOnPage(launcher, 0, "Play Store", "Maps"); + assertPageEmpty(launcher, 1); + }); + + mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Phone"), 3); + + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1, 2, 3); + assertItemsOnPage(launcher, 0, "Play Store", "Maps"); + assertPageEmpty(launcher, 1); + assertPageEmpty(launcher, 2); + assertItemsOnPage(launcher, 3, "Phone"); + }); + } + + + @Test + public void testEmptyPageDoesNotGetRemovedIfPagePairIsNotEmpty() { + if (!mLauncher.isTwoPanels()) { + return; + } + + // Pre verifying the screens + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1); + assertItemsOnPage(launcher, 0, "Play Store", "Maps"); + assertPageEmpty(launcher, 1); + }); + + mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), 3); + mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Chrome"), 0); + + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1, 2, 3); + assertItemsOnPage(launcher, 0, "Play Store"); + assertPageEmpty(launcher, 1); + assertItemsOnPage(launcher, 2, "Chrome"); + assertItemsOnPage(launcher, 3, "Maps"); + }); + + mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), -1); + + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1, 2, 3); + assertItemsOnPage(launcher, 0, "Play Store"); + assertItemsOnPage(launcher, 1, "Maps"); + assertItemsOnPage(launcher, 2, "Chrome"); + assertPageEmpty(launcher, 3); + }); + + // Move Chrome to the right panel as well, to make sure pages are not deleted whichever + // page is the empty one + mWorkspace.flingForward(); + mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Chrome"), 1); + + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1, 2, 3); + assertItemsOnPage(launcher, 0, "Play Store"); + assertItemsOnPage(launcher, 1, "Maps"); + assertPageEmpty(launcher, 2); + assertItemsOnPage(launcher, 3, "Chrome"); + }); + } + + + @Test + public void testEmptyPagesGetRemovedIfBothPagesAreEmpty() { + if (!mLauncher.isTwoPanels()) { + return; + } + + // Pre verifying the screens + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1); + assertItemsOnPage(launcher, 0, "Play Store", "Maps"); + assertPageEmpty(launcher, 1); + }); + + mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Play Store"), 2); + mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Camera"), 1); + + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1, 2, 3); + assertItemsOnPage(launcher, 0, "Maps"); + assertPageEmpty(launcher, 1); + assertItemsOnPage(launcher, 2, "Play Store"); + assertItemsOnPage(launcher, 3, "Camera"); + }); + + mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Camera"), -1); + mWorkspace.flingForward(); + mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Play Store"), -2); + + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1); + assertItemsOnPage(launcher, 0, "Play Store", "Maps"); + assertItemsOnPage(launcher, 1, "Camera"); + }); + } + + @Test + public void testMiddleEmptyPagesGetRemoved() { + if (!mLauncher.isTwoPanels()) { + return; + } + + // Pre verifying the screens + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1); + assertItemsOnPage(launcher, 0, "Play Store", "Maps"); + assertPageEmpty(launcher, 1); + }); + + mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), 2); + mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Messages"), 3); + + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1, 2, 3, 4, 5); + assertItemsOnPage(launcher, 0, "Play Store"); + assertPageEmpty(launcher, 1); + assertItemsOnPage(launcher, 2, "Maps"); + assertPageEmpty(launcher, 3); + assertPageEmpty(launcher, 4); + assertItemsOnPage(launcher, 5, "Messages"); + }); + + mWorkspace.flingBackward(); + mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), 2); + + executeOnLauncher(launcher -> { + assertPagesExist(launcher, 0, 1, 4, 5); + assertItemsOnPage(launcher, 0, "Play Store"); + assertPageEmpty(launcher, 1); + assertItemsOnPage(launcher, 4, "Maps"); + assertItemsOnPage(launcher, 5, "Messages"); + }); + } + + private void assertPageEmpty(Launcher launcher, int pageId) { + CellLayout page = launcher.getWorkspace().getScreenWithId(pageId); + assertNotNull("Page " + pageId + " does NOT exist.", page); + assertEquals("Page " + pageId + " is NOT empty. Number of items on the page:", 0, + page.getShortcutsAndWidgets().getChildCount()); + } + + private void assertPagesExist(Launcher launcher, int... pageIds) { + int pageCount = launcher.getWorkspace().getPageCount(); + assertEquals("Existing page count does NOT match.", pageIds.length, pageCount); + for (int i = 0; i < pageCount; i++) { + CellLayout page = (CellLayout) launcher.getWorkspace().getPageAt(i); + int pageId = launcher.getWorkspace().getIdForScreen(page); + assertEquals("The page's id at index " + i + " does NOT match.", pageId, + pageIds[i]); + } + } + + private void assertItemsOnPage(Launcher launcher, int pageId, String... itemTitles) { + Set itemTitleSet = Arrays.stream(itemTitles).collect(Collectors.toSet()); + CellLayout page = launcher.getWorkspace().getScreenWithId(pageId); + int itemCount = page.getShortcutsAndWidgets().getChildCount(); + for (int i = 0; i < itemCount; i++) { + ItemInfo itemInfo = (ItemInfo) page.getShortcutsAndWidgets().getChildAt(i).getTag(); + if (itemInfo != null) { + assertTrue("There was an extra item on page " + pageId + ": " + itemInfo.title, + itemTitleSet.remove(itemInfo.title)); + } + } + assertTrue("Could NOT find some of the items on page " + pageId + ": " + + itemTitleSet.stream().collect(Collectors.joining(",")), + itemTitleSet.isEmpty()); + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java index 0145690909..d9f5cc821d 100644 --- a/tests/tapl/com/android/launcher3/tapl/Workspace.java +++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java @@ -145,16 +145,7 @@ public final class Workspace extends Home { if (!isWorkspaceScrollable(workspace)) { try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( "dragging icon to a second page of workspace to make it scrollable")) { - dragIconToWorkspace( - mLauncher, - getHotseatAppIcon("Chrome"), - new Point(mLauncher.getDevice().getDisplayWidth(), - mLauncher.getVisibleBounds(workspace).centerY()), - "popup_container", - false, - false, - () -> mLauncher.expectEvent( - TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT)); + dragIcon(workspace, getHotseatAppIcon("Chrome"), pagesPerScreen()); verifyActiveContainer(); } } @@ -163,6 +154,48 @@ public final class Workspace extends Home { } } + /** + * Returns the number of pages that are visible on the screen simultaneously. + */ + public int pagesPerScreen() { + return mLauncher.isTwoPanels() ? 2 : 1; + } + + /** + * Drags an icon to the (currentPage + pageDelta) page if the page already exists. + * If the target page doesn't exist, the icon will be put onto an existing page that is the + * closest to the target page. + * + * @param appIcon - icon to drag. + * @param pageDelta - how many pages should the icon be dragged from the current page. + * It can be a negative value. + */ + public void dragIcon(AppIcon appIcon, int pageDelta) { + try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { + final UiObject2 workspace = verifyActiveContainer(); + try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( + "dragging icon to page with delta: " + pageDelta)) { + dragIcon(workspace, appIcon, pageDelta); + verifyActiveContainer(); + } + } + } + + private void dragIcon(UiObject2 workspace, AppIcon appIcon, int pageDelta) { + int pageWidth = mLauncher.getDevice().getDisplayWidth() / pagesPerScreen(); + int targetX = (pageWidth / 2) + pageWidth * pageDelta; + dragIconToWorkspace( + mLauncher, + appIcon, + new Point(targetX, mLauncher.getVisibleBounds(workspace).centerY()), + "popup_container", + false, + false, + () -> mLauncher.expectEvent( + TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT)); + verifyActiveContainer(); + } + private boolean isWorkspaceScrollable(UiObject2 workspace) { return workspace.getChildCount() > (mLauncher.isTwoPanels() ? 2 : 1); } @@ -258,10 +291,26 @@ public final class Workspace extends Home { try (LauncherInstrumentation.Closable ignored = launcher.addContextLayer( "want to drag icon to workspace")) { final long downTime = SystemClock.uptimeMillis(); - final Point dragStartCenter = dragIconToSpringLoaded(launcher, downTime, + Point dragStart = dragIconToSpringLoaded(launcher, downTime, launchable.getObject(), longPressIndicator, expectLongClickEvents); - final Point targetDest = dest.get(); - launcher.movePointer(dragStartCenter, targetDest, 10, downTime, true, + Point targetDest = dest.get(); + int displayX = launcher.getRealDisplaySize().x; + + // Since the destination can be on another page, we need to drag to the edge first + // until we reach the target page + while (targetDest.x > displayX || targetDest.x < 0) { + int edgeX = targetDest.x > 0 ? displayX : 0; + Point screenEdge = new Point(edgeX, targetDest.y); + launcher.movePointer(dragStart, screenEdge, 10, downTime, true, + LauncherInstrumentation.GestureScope.INSIDE); + launcher.waitForIdle(); // Wait for the page change to happen + targetDest.x += displayX * (targetDest.x > 0 ? -1 : 1); + dragStart = screenEdge; + } + + // targetDest.x is now between 0 and displayX so we found the target page, + // we just have to put move the icon to the destination and drop it + launcher.movePointer(dragStart, targetDest, 10, downTime, true, LauncherInstrumentation.GestureScope.INSIDE); dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents); } From e59a723af38eb30b16115e7c4d9c9870745e8141 Mon Sep 17 00:00:00 2001 From: Tony Wickham Date: Wed, 1 Dec 2021 15:08:23 -0800 Subject: [PATCH 4/5] Fix taskbar jumping when going home - Set resumed state immediately when finishing recents controller to launcher, otherwise there's a race where we start animating gesture state to 0 before animating resumed state to 1, which causes the jump in the bug Test: Swipe from app to home 10 times, no jump Fixes: 203725820 Change-Id: Ie8a1add29902a9961ba4d7861ba5424ab656f1c8 --- .../taskbar/TaskbarLauncherStateController.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java index f3c8cf3480..b2c29b2d90 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java @@ -195,7 +195,7 @@ import java.util.function.Supplier; if (hasAnyFlag(changedFlags, FLAG_RESUMED)) { boolean isResumed = isResumed(); ObjectAnimator anim = mIconAlignmentForResumedState - .animateToValue(getCurrentIconAlignmentRatio(), isResumed ? 1 : 0) + .animateToValue(isResumed ? 1 : 0) .setDuration(duration); anim.addListener(new AnimatorListenerAdapter() { @@ -351,9 +351,16 @@ import java.util.function.Supplier; private void endGestureStateOverride(boolean finishedToApp) { mCallbacks.removeListener(this); + + // Update the resumed state immediately to ensure a seamless handoff + boolean launcherResumed = !finishedToApp; + mIconAlignmentForResumedState.updateValue(launcherResumed ? 1 : 0); + updateStateForFlag(FLAG_RECENTS_ANIMATION_RUNNING, false); + updateStateForFlag(FLAG_RESUMED, launcherResumed); applyState(); + TaskbarStashController controller = mControllers.taskbarStashController; controller.updateStateForFlag(FLAG_IN_APP, finishedToApp); controller.applyState(); From 1bf0fe1b70445854d4ff99d87eb4e4593c327fee Mon Sep 17 00:00:00 2001 From: Alex Chau Date: Tue, 30 Nov 2021 15:32:45 +0000 Subject: [PATCH 5/5] Add back screenshot button to Launcher3 - Also make spacing between buttons always 36dp - Updated screenshot button image for Launcher3 Fix: 208384949 Test: Start overview in Launcher3WithQuickStep, test screenshot button is working Change-Id: Ieb60ce134813202c1d14137fff0a131e1f5f8c96 --- quickstep/res/drawable/ic_screenshot.xml | 23 +++++++++++++------ .../res/layout/overview_actions_container.xml | 12 ++++------ quickstep/res/values/dimens.xml | 2 ++ .../quickstep/views/OverviewActionsView.java | 9 ++++++++ res/values/dimens.xml | 2 ++ src/com/android/launcher3/DeviceProfile.java | 5 ++++ 6 files changed, 39 insertions(+), 14 deletions(-) diff --git a/quickstep/res/drawable/ic_screenshot.xml b/quickstep/res/drawable/ic_screenshot.xml index d97eae1d15..9ee6c44011 100644 --- a/quickstep/res/drawable/ic_screenshot.xml +++ b/quickstep/res/drawable/ic_screenshot.xml @@ -13,11 +13,20 @@ limitations under the License. --> + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> - + android:pathData="M5.8334,1.666H8.3334V3.3327H5.8334V6.666H4.1667V3.3327C4.1667,2.4122 4.9129,1.666 5.8334,1.666Z" + android:fillColor="#000000"/> + + + + \ No newline at end of file diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/res/layout/overview_actions_container.xml index 1c7b5099a8..0fda0bf8d4 100644 --- a/quickstep/res/layout/overview_actions_container.xml +++ b/quickstep/res/layout/overview_actions_container.xml @@ -38,13 +38,13 @@ android:layout_height="wrap_content" android:drawableStart="@drawable/ic_screenshot" android:text="@string/action_screenshot" - android:theme="@style/ThemeControlHighlightWorkspaceColor" - android:visibility="gone" /> + android:theme="@style/ThemeControlHighlightWorkspaceColor" /> + android:visibility="gone" />