diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java index 3bf5f3b351..e624be7e6e 100644 --- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java +++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java @@ -217,6 +217,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener public static final int CONTENT_ALPHA_DURATION = 217; public static final int TRANSIENT_TASKBAR_TRANSITION_DURATION = 417; + public static final int PINNED_TASKBAR_TRANSITION_DURATION = 600; public static final int TASKBAR_TO_APP_DURATION = 600; // TODO(b/236145847): Tune TASKBAR_TO_HOME_DURATION to 383 after conflict with unlock animation // is solved. @@ -1745,8 +1746,21 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener return new AnimatorBackState(rectFSpringAnim, anim); } - public static int getTaskbarToHomeDuration() { - if (enableScalingRevealHomeAnimation()) { + /** Get animation duration for taskbar for going to home. */ + public static int getTaskbarToHomeDuration(boolean isPinnedTaskbar) { + return getTaskbarToHomeDuration(false, isPinnedTaskbar); + } + + /** + * Get animation duration for taskbar for going to home. + * + * @param shouldOverrideToFastAnimation should overwrite scaling reveal home animation duration + */ + public static int getTaskbarToHomeDuration(boolean shouldOverrideToFastAnimation, + boolean isPinnedTaskbar) { + if (isPinnedTaskbar) { + return PINNED_TASKBAR_TRANSITION_DURATION; + } else if (enableScalingRevealHomeAnimation() && !shouldOverrideToFastAnimation) { return TASKBAR_TO_HOME_DURATION_SLOW; } else { return TASKBAR_TO_HOME_DURATION_FAST; diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java index 57444648ea..fd0243a035 100644 --- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java +++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java @@ -488,6 +488,15 @@ public class DesktopVisibilityController { } }); } + + public void onEnterDesktopModeTransitionStarted(int transitionDuration) { + + } + + @Override + public void onExitDesktopModeTransitionStarted(int transitionDuration) { + + } } /** A listener for Taskbar in Desktop Mode. */ diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java index f92d9ddc04..cc3273e442 100644 --- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java @@ -212,8 +212,12 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } private int getTaskbarAnimationDuration(boolean isVisible) { - if (isVisible && !mLauncher.getPredictiveBackToHomeInProgress()) { - return getTaskbarToHomeDuration(); + // fast animation duration since we will not be playing workspace reveal animation. + boolean shouldOverrideToFastAnimation = + !isHotseatIconOnTopWhenAligned() || mLauncher.getPredictiveBackToHomeInProgress(); + boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(mLauncher); + if (isVisible || isPinnedTaskbar) { + return getTaskbarToHomeDuration(shouldOverrideToFastAnimation, isPinnedTaskbar); } else { return DisplayController.isTransientTaskbar(mLauncher) ? TRANSIENT_TASKBAR_TRANSITION_DURATION diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java index fa0473910b..dce377d5d7 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java @@ -17,6 +17,7 @@ package com.android.launcher3.taskbar; import static com.android.app.animation.Interpolators.EMPHASIZED; import static com.android.app.animation.Interpolators.FINAL_FRAME; +import static com.android.app.animation.Interpolators.INSTANT; import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_ALIGNMENT; import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_STASH; @@ -222,7 +223,9 @@ public class TaskbarLauncherStateController { updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, true); if (!mShouldDelayLauncherStateAnim) { if (toState == LauncherState.NORMAL) { - applyState(QuickstepTransitionManager.getTaskbarToHomeDuration()); + applyState(QuickstepTransitionManager.getTaskbarToHomeDuration( + DisplayController.isPinnedTaskbar( + mControllers.taskbarActivityContext))); } else { applyState(); } @@ -459,9 +462,12 @@ public class TaskbarLauncherStateController { private Animator onStateChangeApplied(int changedFlags, long duration, boolean start) { final boolean isInLauncher = isInLauncher(); + final boolean isInOverview = mControllers.uiController.isInOverviewUi(); final boolean isIconAlignedWithHotseat = isIconAlignedWithHotseat(); final float toAlignment = isIconAlignedWithHotseat ? 1 : 0; boolean handleOpenFloatingViews = false; + boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar( + mControllers.taskbarActivityContext); if (DEBUG) { Log.d(TAG, "onStateChangeApplied - isInLauncher: " + isInLauncher + ", mLauncherState: " + mLauncherState @@ -573,10 +579,17 @@ public class TaskbarLauncherStateController { } float backgroundAlpha = isInLauncher && isTaskbarAlignedWithHotseat() ? 0 : 1; + AnimatedFloat taskbarBgOffset = + mControllers.taskbarDragLayerController.getTaskbarBackgroundOffset(); + boolean showTaskbar = !isInLauncher || isInOverview; + float taskbarBgOffsetEnd = showTaskbar ? 0f : 1f; + float taskbarBgOffsetStart = showTaskbar ? 1f : 0f; // Don't animate if background has reached desired value. if (mTaskbarBackgroundAlpha.isAnimating() - || mTaskbarBackgroundAlpha.value != backgroundAlpha) { + || mTaskbarBackgroundAlpha.value != backgroundAlpha + || taskbarBgOffset.isAnimatingToValue(taskbarBgOffsetStart) + || taskbarBgOffset.value != taskbarBgOffsetEnd) { mTaskbarBackgroundAlpha.cancelAnimation(); if (DEBUG) { Log.d(TAG, "onStateChangeApplied - taskbarBackgroundAlpha - " @@ -587,25 +600,35 @@ public class TaskbarLauncherStateController { boolean isInLauncherIconNotAligned = isInLauncher && !isIconAlignedWithHotseat; boolean notInLauncherIconNotAligned = !isInLauncher && !isIconAlignedWithHotseat; boolean isInLauncherIconIsAligned = isInLauncher && isIconAlignedWithHotseat; + // When Hotseat icons are not on top don't change duration or add start delay. + // This will keep the duration in sync for icon alignment and background fade in/out. + // For example, launching app from launcher all apps. + boolean isHotseatIconOnTopWhenAligned = + mControllers.uiController.isHotseatIconOnTopWhenAligned(); float startDelay = 0; // We want to delay the background from fading in so that the icons have time to move // into the bounds of the background before it appears. if (isInLauncherIconNotAligned) { startDelay = duration * TASKBAR_BG_ALPHA_LAUNCHER_NOT_ALIGNED_DELAY_MULT; - } else if (notInLauncherIconNotAligned) { + } else if (notInLauncherIconNotAligned && isHotseatIconOnTopWhenAligned) { startDelay = duration * TASKBAR_BG_ALPHA_NOT_LAUNCHER_NOT_ALIGNED_DELAY_MULT; } float newDuration = duration - startDelay; - if (isInLauncherIconIsAligned) { + if (isInLauncherIconIsAligned && isHotseatIconOnTopWhenAligned) { // Make the background fade out faster so that it is gone by the time the // icons move outside of the bounds of the background. newDuration = duration * TASKBAR_BG_ALPHA_LAUNCHER_IS_ALIGNED_DURATION_MULT; } - Animator taskbarBackgroundAlpha = mTaskbarBackgroundAlpha - .animateToValue(backgroundAlpha) - .setDuration((long) newDuration); - taskbarBackgroundAlpha.setStartDelay((long) startDelay); + Animator taskbarBackgroundAlpha = mTaskbarBackgroundAlpha.animateToValue( + backgroundAlpha); + if (isPinnedTaskbar) { + setupPinnedTaskbarAnimation(animatorSet, showTaskbar, taskbarBgOffset, + taskbarBgOffsetStart, taskbarBgOffsetEnd, duration, taskbarBackgroundAlpha); + } else { + taskbarBackgroundAlpha.setDuration((long) newDuration); + taskbarBackgroundAlpha.setStartDelay((long) startDelay); + } animatorSet.play(taskbarBackgroundAlpha); } @@ -671,15 +694,18 @@ public class TaskbarLauncherStateController { + mIconAlignment.value + " -> " + toAlignment + ": " + duration); } - if (hasAnyFlag(FLAG_TASKBAR_HIDDEN)) { - iconAlignAnim.setInterpolator(FINAL_FRAME); - } else { - animatorSet.play(iconAlignAnim); + if (!isPinnedTaskbar) { + if (hasAnyFlag(FLAG_TASKBAR_HIDDEN)) { + iconAlignAnim.setInterpolator(FINAL_FRAME); + } else { + animatorSet.play(iconAlignAnim); + } } } - Interpolator interpolator = enableScalingRevealHomeAnimation() + Interpolator interpolator = enableScalingRevealHomeAnimation() && !isPinnedTaskbar ? ScalingWorkspaceRevealAnim.SCALE_INTERPOLATOR : EMPHASIZED; + animatorSet.setInterpolator(interpolator); if (start) { @@ -688,6 +714,49 @@ public class TaskbarLauncherStateController { return animatorSet; } + private void setupPinnedTaskbarAnimation(AnimatorSet animatorSet, boolean showTaskbar, + AnimatedFloat taskbarBgOffset, float taskbarBgOffsetStart, float taskbarBgOffsetEnd, + long duration, Animator taskbarBackgroundAlpha) { + float targetAlpha = !showTaskbar ? 1 : 0; + mLauncher.getHotseat().setIconsAlpha(targetAlpha, ALPHA_CHANNEL_TASKBAR_ALIGNMENT); + if (mIsQsbInline) { + mLauncher.getHotseat().setQsbAlpha(targetAlpha, + ALPHA_CHANNEL_TASKBAR_ALIGNMENT); + } + + if ((taskbarBgOffset.value != taskbarBgOffsetEnd && !taskbarBgOffset.isAnimating()) + || taskbarBgOffset.isAnimatingToValue(taskbarBgOffsetStart)) { + taskbarBgOffset.cancelAnimation(); + Animator taskbarIconAlpha = mTaskbarAlphaForHome.animateToValue( + showTaskbar ? 1f : 0f); + AnimatedFloat taskbarIconTranslationYForHome = + mControllers.taskbarViewController.mTaskbarIconTranslationYForHome; + ObjectAnimator taskbarBackgroundOffset = taskbarBgOffset.animateToValue( + taskbarBgOffsetStart, + taskbarBgOffsetEnd); + ObjectAnimator taskbarIconsYTranslation = null; + float taskbarHeight = + mControllers.taskbarActivityContext.getDeviceProfile().taskbarHeight; + if (showTaskbar) { + taskbarIconsYTranslation = taskbarIconTranslationYForHome.animateToValue( + taskbarHeight, 0); + } else { + taskbarIconsYTranslation = taskbarIconTranslationYForHome.animateToValue(0, + taskbarHeight); + } + + taskbarIconAlpha.setDuration(duration); + taskbarIconsYTranslation.setDuration(duration); + taskbarBackgroundOffset.setDuration(duration); + + animatorSet.play(taskbarIconAlpha); + animatorSet.play(taskbarIconsYTranslation); + animatorSet.play(taskbarBackgroundOffset); + } + taskbarBackgroundAlpha.setInterpolator(showTaskbar ? INSTANT : FINAL_FRAME); + taskbarBackgroundAlpha.setDuration(duration); + } + /** * Whether the taskbar is aligned with the hotseat in the current/target launcher state. * @@ -940,7 +1009,12 @@ public class TaskbarLauncherStateController { @Override public void onRecentsAnimationFinished(RecentsAnimationController controller) { - endGestureStateOverride(!controller.getFinishTargetIsLauncher(), false /*canceled*/); + endGestureStateOverride(!controller.getFinishTargetIsLauncher(), + controller.getLauncherIsVisibleAtFinish(), false /*canceled*/); + } + + private void endGestureStateOverride(boolean finishedToApp, boolean canceled) { + endGestureStateOverride(finishedToApp, finishedToApp, canceled); } /** @@ -950,10 +1024,13 @@ public class TaskbarLauncherStateController { * * @param finishedToApp {@code true} if the recents animation finished to showing an app and * not workspace or overview - * @param canceled {@code true} if the recents animation was canceled instead of finishing - * to completion + * @param launcherIsVisible {code true} if launcher is visible at finish + * @param canceled {@code true} if the recents animation was canceled instead of + * finishing + * to completion */ - private void endGestureStateOverride(boolean finishedToApp, boolean canceled) { + private void endGestureStateOverride(boolean finishedToApp, boolean launcherIsVisible, + boolean canceled) { mCallbacks.removeListener(this); mTaskBarRecentsAnimationListener = null; ((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(null); @@ -962,17 +1039,27 @@ public class TaskbarLauncherStateController { mSkipNextRecentsAnimEnd = false; return; } - updateStateForUserFinishedToApp(finishedToApp); + updateStateForUserFinishedToApp(finishedToApp, launcherIsVisible); } } /** - * Updates the visible state immediately to ensure a seamless handoff. - * @param finishedToApp True iff user is in an app. + * @see #updateStateForUserFinishedToApp(boolean, boolean) */ private void updateStateForUserFinishedToApp(boolean finishedToApp) { + updateStateForUserFinishedToApp(finishedToApp, !finishedToApp); + } + + /** + * Updates the visible state immediately to ensure a seamless handoff. + * + * @param finishedToApp True iff user is in an app. + * @param launcherIsVisible True iff launcher is still visible (ie. transparent app) + */ + private void updateStateForUserFinishedToApp(boolean finishedToApp, + boolean launcherIsVisible) { // Update the visible state immediately to ensure a seamless handoff - boolean launcherVisible = !finishedToApp; + boolean launcherVisible = !finishedToApp || launcherIsVisible; updateStateForFlag(FLAG_TRANSITION_TO_VISIBLE, false); updateStateForFlag(FLAG_VISIBLE, launcherVisible); applyState(); @@ -981,7 +1068,7 @@ public class TaskbarLauncherStateController { if (DEBUG) { Log.d(TAG, "endGestureStateOverride - FLAG_IN_APP: " + finishedToApp); } - controller.updateStateForFlag(FLAG_IN_APP, finishedToApp); + controller.updateStateForFlag(FLAG_IN_APP, finishedToApp && !launcherIsVisible); controller.applyState(); } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java index c1dd216a9c..67be8dac05 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java @@ -23,6 +23,7 @@ import static com.android.app.animation.Interpolators.INSTANT; import static com.android.app.animation.Interpolators.LINEAR; import static com.android.internal.jank.InteractionJankMonitor.Configuration; import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; +import static com.android.launcher3.QuickstepTransitionManager.PINNED_TASKBAR_TRANSITION_DURATION; import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_HIDE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_SHOW; @@ -398,6 +399,9 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba * Returns how long the stash/unstash animation should play. */ public long getStashDuration() { + if (DisplayController.isPinnedTaskbar(mActivity)) { + return PINNED_TASKBAR_TRANSITION_DURATION; + } return DisplayController.isTransientTaskbar(mActivity) ? TRANSIENT_TASKBAR_STASH_DURATION : TASKBAR_STASH_DURATION; diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java index cebabffa75..bb4f07a72f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java @@ -120,7 +120,7 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar private final TaskbarView mTaskbarView; private final MultiValueAlpha mTaskbarIconAlpha; private final AnimatedFloat mTaskbarIconScaleForStash = new AnimatedFloat(this::updateScale); - private final AnimatedFloat mTaskbarIconTranslationYForHome = new AnimatedFloat( + public final AnimatedFloat mTaskbarIconTranslationYForHome = new AnimatedFloat( this::updateTranslationY); private final AnimatedFloat mTaskbarIconTranslationYForStash = new AnimatedFloat( this::updateTranslationY); @@ -796,6 +796,8 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar */ private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp) { PendingAnimation setter = new PendingAnimation(100); + // icon alignment not needed for pinned taskbar. + if (DisplayController.isPinnedTaskbar(mActivity)) return setter.createPlaybackController(); mOnControllerPreCreateCallback.run(); DeviceProfile taskbarDp = mActivity.getDeviceProfile(); Rect hotseatPadding = launcherDp.getHotseatLayoutPadding(mActivity); diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java index 3a39cf28b1..8ad00bfd88 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java @@ -95,6 +95,7 @@ public class QuickstepAtomicAnimationFactory extends public void prepareForAtomicAnimation(LauncherState fromState, LauncherState toState, StateAnimationConfig config) { RecentsView overview = mContainer.getOverviewPanel(); + boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(mContainer); if ((fromState == OVERVIEW || fromState == OVERVIEW_SPLIT_SELECT) && toState == NORMAL) { overview.switchToScreenshot(() -> overview.finishRecentsAnimation(true /* toRecents */, null)); @@ -109,7 +110,8 @@ public class QuickstepAtomicAnimationFactory extends // We sync the scrim fade with the taskbar animation duration to avoid any flickers for // taskbar icons disappearing before hotseat icons show up. float scrimUpperBoundFromSplit = - QuickstepTransitionManager.getTaskbarToHomeDuration() / (float) config.duration; + QuickstepTransitionManager.getTaskbarToHomeDuration(isPinnedTaskbar) + / (float) config.duration; scrimUpperBoundFromSplit = Math.min(scrimUpperBoundFromSplit, 1f); config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, clampToProgress(LINEAR, 0, 0.25f)); config.setInterpolator(ANIM_SCRIM_FADE, @@ -139,7 +141,8 @@ public class QuickstepAtomicAnimationFactory extends // Sync scroll so that it ends before or at the same time as the taskbar animation. if (mContainer.getDeviceProfile().isTaskbarPresent) { config.duration = Math.min( - config.duration, QuickstepTransitionManager.getTaskbarToHomeDuration()); + config.duration, + QuickstepTransitionManager.getTaskbarToHomeDuration(isPinnedTaskbar)); } overview.snapToPage(DEFAULT_PAGE, Math.toIntExact(config.duration)); } else { diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index 31aa489bd6..95e7737906 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -100,6 +100,7 @@ import com.android.internal.jank.Cuj; import com.android.internal.util.LatencyTracker; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.QuickstepTransitionManager; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimationSuccessListener; @@ -1373,8 +1374,9 @@ public abstract class AbsSwipeUpHandler< mInputConsumerProxy.enable(); } if (endTarget == HOME) { + boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(mContext); duration = mContainer != null && mContainer.getDeviceProfile().isTaskbarPresent - ? StaggeredWorkspaceAnim.DURATION_TASKBAR_MS + ? QuickstepTransitionManager.getTaskbarToHomeDuration(isPinnedTaskbar) : StaggeredWorkspaceAnim.DURATION_MS; ContextualEduStatsManager.INSTANCE.get(mContext).updateEduStats( mGestureState.isTrackpadGesture(), GestureType.HOME); diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java index dcb01087ee..60fcff86f3 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java @@ -53,6 +53,8 @@ public class RecentsAnimationController { private boolean mFinishRequested = false; // Only valid when mFinishRequested == true. private boolean mFinishTargetIsLauncher; + // Only valid when mFinishRequested == true + private boolean mLauncherIsVisibleAtFinish; private RunnableList mPendingFinishCallbacks = new RunnableList(); public RecentsAnimationController(RecentsAnimationControllerCompat controller, @@ -116,14 +118,28 @@ public class RecentsAnimationController { finishController(toRecents, onFinishComplete, sendUserLeaveHint); } + @UiThread + public void finish(boolean toRecents, boolean launcherIsVisibleAtFinish, + Runnable onFinishComplete, boolean sendUserLeaveHint) { + Preconditions.assertUIThread(); + finishController(toRecents, launcherIsVisibleAtFinish, onFinishComplete, sendUserLeaveHint, + false); + } + @UiThread public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint) { - finishController(toRecents, callback, sendUserLeaveHint, false /* forceFinish */); + finishController(toRecents, false, callback, sendUserLeaveHint, false /* forceFinish */); } @UiThread public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint, boolean forceFinish) { + finishController(toRecents, toRecents, callback, sendUserLeaveHint, forceFinish); + } + + @UiThread + public void finishController(boolean toRecents, boolean launcherIsVisibleAtFinish, + Runnable callback, boolean sendUserLeaveHint, boolean forceFinish) { mPendingFinishCallbacks.add(callback); if (!forceFinish && mFinishRequested) { // If finish has already been requested, then add the callback to the pending list. @@ -135,6 +151,7 @@ public class RecentsAnimationController { // Finish not yet requested mFinishRequested = true; mFinishTargetIsLauncher = toRecents; + mLauncherIsVisibleAtFinish = launcherIsVisibleAtFinish; mOnFinishedListener.accept(this); Runnable finishCb = () -> { mController.finish(toRecents, sendUserLeaveHint, new IResultReceiver.Stub() { @@ -211,6 +228,14 @@ public class RecentsAnimationController { return mFinishTargetIsLauncher; } + /** + * RecentsAnimationListeners can check this in onRecentsAnimationFinished() to determine whether + * the animation was finished to launcher vs an app. + */ + public boolean getLauncherIsVisibleAtFinish() { + return mLauncherIsVisibleAtFinish; + } + public void dump(String prefix, PrintWriter pw) { pw.println(prefix + "RecentsAnimationController:"); diff --git a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java index 997a842dc2..12ca25702e 100644 --- a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java +++ b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java @@ -49,6 +49,7 @@ import com.android.launcher3.celllayout.CellLayoutLayoutParams; import com.android.launcher3.statehandlers.DepthController; import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.DynamicResource; import com.android.quickstep.views.RecentsView; import com.android.systemui.plugins.ResourceProvider; @@ -63,8 +64,7 @@ public class StaggeredWorkspaceAnim { private static final int APP_CLOSE_ROW_START_DELAY_MS = 10; // Should be used for animations running alongside this StaggeredWorkspaceAnim. public static final int DURATION_MS = 250; - public static final int DURATION_TASKBAR_MS = - QuickstepTransitionManager.getTaskbarToHomeDuration(); + private final int mTaskbarDurationInMs; private static final float MAX_VELOCITY_PX_PER_S = 22f; @@ -81,6 +81,8 @@ public class StaggeredWorkspaceAnim { public StaggeredWorkspaceAnim(QuickstepLauncher launcher, float velocity, boolean animateOverviewScrim, @Nullable View ignoredView, boolean staggerWorkspace) { + mTaskbarDurationInMs = QuickstepTransitionManager.getTaskbarToHomeDuration( + DisplayController.isPinnedTaskbar(launcher)); prepareToAnimate(launcher, animateOverviewScrim); mIgnoredView = ignoredView; @@ -93,7 +95,7 @@ public class StaggeredWorkspaceAnim { .getDimensionPixelSize(R.dimen.swipe_up_max_workspace_trans_y); DeviceProfile grid = launcher.getDeviceProfile(); - long duration = grid.isTaskbarPresent ? DURATION_TASKBAR_MS : DURATION_MS; + long duration = grid.isTaskbarPresent ? mTaskbarDurationInMs : DURATION_MS; if (staggerWorkspace) { Workspace workspace = launcher.getWorkspace(); Hotseat hotseat = launcher.getHotseat(); diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 8982850532..92c93ff9ee 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -5854,15 +5854,22 @@ public abstract class RecentsView< * Finish recents animation. */ public void finishRecentsAnimation(boolean toRecents, @Nullable Runnable onFinishComplete) { - finishRecentsAnimation(toRecents, true /* shouldPip */, onFinishComplete); + finishRecentsAnimation(toRecents, false, true /* shouldPip */, onFinishComplete); } + /** + * Finish recents animation. + */ + public void finishRecentsAnimation(boolean toRecents, boolean shouldPip, + @Nullable Runnable onFinishComplete) { + finishRecentsAnimation(toRecents, shouldPip, false, onFinishComplete); + } /** * NOTE: Whatever value gets passed through to the toRecents param may need to also be set on * {@link #mRecentsAnimationController#setWillFinishToHome}. */ public void finishRecentsAnimation(boolean toRecents, boolean shouldPip, - @Nullable Runnable onFinishComplete) { + boolean allAppTargetsAreTranslucent, @Nullable Runnable onFinishComplete) { Log.d(TAG, "finishRecentsAnimation - mRecentsAnimationController: " + mRecentsAnimationController); // TODO(b/197232424#comment#10) Move this back into onRecentsAnimationComplete(). Maybe? @@ -5894,7 +5901,7 @@ public abstract class RecentsView< tx, null /* overlay */); } } - mRecentsAnimationController.finish(toRecents, () -> { + mRecentsAnimationController.finish(toRecents, allAppTargetsAreTranslucent, () -> { if (onFinishComplete != null) { onFinishComplete.run(); } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt index 71f4ef4ee6..5e438bd8e1 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt @@ -21,6 +21,7 @@ import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING +import com.android.launcher3.QuickstepTransitionManager.PINNED_TASKBAR_TRANSITION_DURATION import com.android.launcher3.R import com.android.launcher3.taskbar.StashedHandleViewController.ALPHA_INDEX_STASHED import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN @@ -158,7 +159,7 @@ class TaskbarStashControllerTest { @Test @TaskbarMode(PINNED) fun testGetStashDuration_pinnedMode() { - assertThat(stashController.stashDuration).isEqualTo(TASKBAR_STASH_DURATION) + assertThat(stashController.stashDuration).isEqualTo(PINNED_TASKBAR_TRANSITION_DURATION) } @Test diff --git a/src/com/android/launcher3/model/GridSizeMigrationDBController.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java index 617cac7f59..bfa00bda9e 100644 --- a/src/com/android/launcher3/model/GridSizeMigrationDBController.java +++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java @@ -17,12 +17,14 @@ package com.android.launcher3.model; import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle; +import static com.android.launcher3.Flags.oneGridSpecs; import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME; import static com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE; import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET; import static com.android.launcher3.model.LoaderTask.SMARTSPACE_ON_HOME_SCREEN; import static com.android.launcher3.provider.LauncherDbUtils.copyTable; import static com.android.launcher3.provider.LauncherDbUtils.dropTable; +import static com.android.launcher3.provider.LauncherDbUtils.shiftTableByXCells; import android.content.ComponentName; import android.content.ContentValues; @@ -130,6 +132,20 @@ public class GridSizeMigrationDBController { // Only use this strategy when comparing the previous grid to the new grid and the // columns are the same and the destination has more rows copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context); + + if (oneGridSpecs()) { + DbReader destReader = new DbReader( + target.getWritableDatabase(), TABLE_NAME, context); + boolean shouldShiftCells = shouldShiftCells(destReader, srcDeviceState.getRows()); + if (shouldShiftCells) { + shiftTableByXCells( + target.getWritableDatabase(), + (destDeviceState.getRows() - srcDeviceState.getRows()), + TABLE_NAME); + } + } + + // Save current configuration, so that the migration does not run again. destDeviceState.writeToPrefs(context); return true; } @@ -427,17 +443,22 @@ public class GridSizeMigrationDBController { } } - static void copyCurrentGridToNewGrid( - @NonNull Context context, - @NonNull DeviceGridState destDeviceState, - @NonNull DatabaseHelper target, - @NonNull SQLiteDatabase source) { - // Only use this strategy when comparing the previous grid to the new grid and the - // columns are the same and the destination has more rows - copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context); - destDeviceState.writeToPrefs(context); + private static boolean shouldShiftCells(final DbReader destReader, final int srcGridRowCount) { + List workspaceItems = destReader.loadAllWorkspaceEntries(); + int firstPageItemsRowPosSum = workspaceItems.stream() + .filter(entry -> entry.screenId == 0) + .mapToInt(entry -> entry.cellY).sum(); + int firstPageWorkspaceItemsCount = (int) workspaceItems.stream() + .filter(entry -> entry.screenId == 0).count(); + if (firstPageWorkspaceItemsCount == 0) { + return false; + } + float srcGridMidPoint = srcGridRowCount / 2f; + float firstPageItemPosAvg = (float) firstPageItemsRowPosSum / firstPageWorkspaceItemsCount; + return (firstPageItemPosAvg >= srcGridMidPoint); } + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) public static class DbReader { diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt index c856d4bb46..3f52d8a73f 100644 --- a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt +++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt @@ -21,15 +21,20 @@ import android.graphics.Point import android.util.Log import androidx.annotation.VisibleForTesting import com.android.launcher3.Flags +import com.android.launcher3.Flags.oneGridSpecs import com.android.launcher3.LauncherPrefs import com.android.launcher3.LauncherPrefs.Companion.get import com.android.launcher3.LauncherPrefs.Companion.getPrefs import com.android.launcher3.LauncherSettings +import com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME +import com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE import com.android.launcher3.Utilities import com.android.launcher3.config.FeatureFlags import com.android.launcher3.model.GridSizeMigrationDBController.DbReader -import com.android.launcher3.provider.LauncherDbUtils import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction +import com.android.launcher3.provider.LauncherDbUtils.copyTable +import com.android.launcher3.provider.LauncherDbUtils.dropTable +import com.android.launcher3.provider.LauncherDbUtils.shiftTableByXCells import com.android.launcher3.util.CellAndSpan import com.android.launcher3.util.GridOccupancy import com.android.launcher3.util.IntArray @@ -59,27 +64,30 @@ class GridSizeMigrationLogic { // amount of rows we simply copy over the source grid to the destination grid, rather // than undergoing the general grid migration. if (shouldMigrateToStrictlyTallerGrid(isDestNewDb, srcDeviceState, destDeviceState)) { - GridSizeMigrationDBController.copyCurrentGridToNewGrid( - context, - destDeviceState, - target, - source, - ) + copyTable(source, TABLE_NAME, target.writableDatabase, TABLE_NAME, context) + if (oneGridSpecs()) { + val destReader = DbReader(target.writableDatabase, TABLE_NAME, context) + val shouldShiftCells = shouldShiftCells(destReader, srcDeviceState.rows) + if (shouldShiftCells) { + shiftTableByXCells( + target.writableDatabase, + (destDeviceState.rows - srcDeviceState.rows), + TABLE_NAME, + ) + } + } + // Save current configuration, so that the migration does not run again. + destDeviceState.writeToPrefs(context) return } - LauncherDbUtils.copyTable( - source, - LauncherSettings.Favorites.TABLE_NAME, - target.writableDatabase, - LauncherSettings.Favorites.TMP_TABLE, - context, - ) + + copyTable(source, TABLE_NAME, target.writableDatabase, TMP_TABLE, context) val migrationStartTime = System.currentTimeMillis() try { SQLiteTransaction(target.writableDatabase).use { t -> - val srcReader = DbReader(t.db, LauncherSettings.Favorites.TMP_TABLE, context) - val destReader = DbReader(t.db, LauncherSettings.Favorites.TABLE_NAME, context) + val srcReader = DbReader(t.db, TMP_TABLE, context) + val destReader = DbReader(t.db, TABLE_NAME, context) val targetSize = Point(destDeviceState.columns, destDeviceState.rows) @@ -95,7 +103,7 @@ class GridSizeMigrationLogic { // Migrate workspace. migrateWorkspace(srcReader, destReader, target, targetSize, idsInUse) - LauncherDbUtils.dropTable(t.db, LauncherSettings.Favorites.TMP_TABLE) + dropTable(t.db, TMP_TABLE) t.commit() } } catch (e: Exception) { @@ -112,6 +120,19 @@ class GridSizeMigrationLogic { } } + private fun shouldShiftCells(destReader: DbReader, srcGridRowCount: Int): Boolean { + val workspaceItems = destReader.loadAllWorkspaceEntries() + val firstPageItemsRowPosSum = + workspaceItems.sumOf { entry -> if (entry.screenId == 0) entry.cellY else 0 } + val firstPageWorkspaceItemsCount = workspaceItems.count { entry -> entry.screenId == 0 } + if (firstPageWorkspaceItemsCount == 0) { + return false + } + val srcGridMidPoint = srcGridRowCount / 2f + val firstPageItemPosAvg = firstPageItemsRowPosSum / firstPageWorkspaceItemsCount.toFloat() + return (firstPageItemPosAvg >= srcGridMidPoint) + } + /** Handles hotseat migration. */ @VisibleForTesting fun migrateHotseat( diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.kt b/src/com/android/launcher3/provider/LauncherDbUtils.kt index 3c68e462a8..6f1d0dddaf 100644 --- a/src/com/android/launcher3/provider/LauncherDbUtils.kt +++ b/src/com/android/launcher3/provider/LauncherDbUtils.kt @@ -131,6 +131,11 @@ object LauncherDbUtils { } } + @JvmStatic + fun shiftTableByXCells(db: SQLiteDatabase, x: Int, toTable: String) { + db.run { execSQL("UPDATE $toTable SET cellY = cellY + $x") } + } + /** * Migrates the legacy shortcuts to deep shortcuts pinned under Launcher. Removes any invalid * shortcut or any shortcut which requires some permission to launch diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt index 82229f8f21..e4c50f08b1 100644 --- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt +++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt @@ -18,18 +18,23 @@ package com.android.launcher3.recyclerview import android.content.Context import android.util.Log +import android.view.InflateException +import androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.Companion.PROTECTED import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.RecycledViewPool import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.android.launcher3.BubbleTextView import com.android.launcher3.BuildConfig import com.android.launcher3.allapps.BaseAllAppsAdapter +import com.android.launcher3.config.FeatureFlags import com.android.launcher3.util.CancellableTask import com.android.launcher3.util.Executors.MAIN_EXECUTOR import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR import com.android.launcher3.util.Themes import com.android.launcher3.views.ActivityContext import com.android.launcher3.views.ActivityContext.ActivityContextDelegate +import java.lang.IllegalStateException const val PREINFLATE_ICONS_ROW_COUNT = 4 const val EXTRA_ICONS_COUNT = 2 @@ -39,10 +44,11 @@ const val EXTRA_ICONS_COUNT = 2 * [RecyclerView]. The view inflation will happen on background thread and inflated [ViewHolder]s * will be added to [RecycledViewPool] on main thread. */ -class AllAppsRecyclerViewPool : RecycledViewPool() { +class AllAppsRecyclerViewPool : RecycledViewPool() where T : Context, T : ActivityContext { var hasWorkProfile = false - private var mCancellableTask: CancellableTask>? = null + @VisibleForTesting(otherwise = PROTECTED) + var mCancellableTask: CancellableTask>? = null companion object { private const val TAG = "AllAppsRecyclerViewPool" @@ -53,7 +59,7 @@ class AllAppsRecyclerViewPool : RecycledViewPool() { /** * Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate. */ - fun preInflateAllAppsViewHolders(context: T) where T : Context, T : ActivityContext { + fun preInflateAllAppsViewHolders(context: T) { val appsView = context.appsView ?: return val activeRv: RecyclerView = appsView.activeRecyclerView ?: return val preInflateCount = getPreinflateCount(context) @@ -97,36 +103,65 @@ class AllAppsRecyclerViewPool : RecycledViewPool() { override fun getLayoutManager(): RecyclerView.LayoutManager? = null } + preInflateAllAppsViewHolders( + adapter, + BaseAllAppsAdapter.VIEW_TYPE_ICON, + activeRv, + preInflateCount, + ) { + getPreinflateCount(context) + } + } + + @VisibleForTesting(otherwise = PROTECTED) + fun preInflateAllAppsViewHolders( + adapter: RecyclerView.Adapter<*>, + viewType: Int, + activeRv: RecyclerView, + preInflationCount: Int, + preInflationCountProvider: () -> Int, + ) { + if (preInflationCount <= 0) { + return + } mCancellableTask?.cancel() var task: CancellableTask>? = null task = CancellableTask( { val list: ArrayList = ArrayList() - for (i in 0 until preInflateCount) { + for (i in 0 until preInflationCount) { if (task?.canceled == true) { break } // If activeRv's layout manager has been reset to null on main thread, skip // the preinflation as we cannot generate correct LayoutParams if (activeRv.layoutManager == null) { + list.clear() + break + } + try { + list.add(adapter.createViewHolder(activeRv, viewType)) + } catch (e: InflateException) { + list.clear() + // It's still possible for UI thread to set activeRv's layout manager to + // null and we should break the loop and cancel the preinflation. break } - list.add( - adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON) - ) } list }, MAIN_EXECUTOR, { viewHolders -> - for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) { + // Run preInflationCountProvider again as the needed VH might have changed + val newPreInflationCount = preInflationCountProvider.invoke() + for (i in 0 until minOf(viewHolders.size, newPreInflationCount)) { putRecycledView(viewHolders[i]) } }, ) mCancellableTask = task - VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask) + VIEW_PREINFLATION_EXECUTOR.execute(mCancellableTask) } /** @@ -143,10 +178,11 @@ class AllAppsRecyclerViewPool : RecycledViewPool() { * app icons plus [EXTRA_ICONS_COUNT] is the magic minimal count of app icons to preinflate to * suffice fast scrolling. * - * Note that we need to preinfate extra app icons in size of one all apps pages, so that opening - * all apps don't need to inflate app icons. + * Note that if [FeatureFlags.ALL_APPS_GONE_VISIBILITY] is enabled, we need to preinfate extra + * app icons in size of one all apps pages, so that opening all apps don't need to inflate app + * icons. */ - fun getPreinflateCount(context: T): Int where T : Context, T : ActivityContext { + fun getPreinflateCount(context: T): Int { var targetPreinflateCount = PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns + EXTRA_ICONS_COUNT diff --git a/tests/Android.bp b/tests/Android.bp index b1d4ef6703..e4fecc5ec8 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -63,7 +63,6 @@ filegroup { "src/com/android/launcher3/dragging/TaplDragTest.java", "src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java", "src/com/android/launcher3/ui/TaplTestsLauncher3Test.java", - "src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java", "src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java", ], } diff --git a/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt new file mode 100644 index 0000000000..3afb0b503f --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2024 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.recyclerview + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.LayoutManager +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.util.Executors +import com.android.launcher3.views.ActivityContext +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class AllAppsRecyclerViewPoolTest where T : Context, T : ActivityContext { + + private lateinit var underTest: AllAppsRecyclerViewPool + private lateinit var adapter: RecyclerView.Adapter<*> + + @Mock private lateinit var parent: RecyclerView + @Mock private lateinit var itemView: View + @Mock private lateinit var layoutManager: LayoutManager + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + underTest = spy(AllAppsRecyclerViewPool()) + adapter = + object : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + object : ViewHolder(itemView) {} + + override fun getItemCount() = 0 + + override fun onBindViewHolder(holder: ViewHolder, position: Int) {} + } + underTest.setMaxRecycledViews(VIEW_TYPE, 20) + `when`(parent.layoutManager).thenReturn(layoutManager) + } + + @Test + fun preinflate_success() { + underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 } + + awaitTasksCompleted() + assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(10) + } + + @Test + fun preinflate_not_triggered() { + underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 0) { 0 } + + awaitTasksCompleted() + assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0) + } + + @Test + fun preinflate_cancel_before_runOnMainThread() { + underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 } + assertThat(underTest.mCancellableTask!!.canceled).isFalse() + + underTest.clear() + + awaitTasksCompleted() + verify(underTest, never()).putRecycledView(any(ViewHolder::class.java)) + assertThat(underTest.mCancellableTask!!.canceled).isTrue() + assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0) + } + + @Test + fun preinflate_cancel_after_run() { + underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 } + assertThat(underTest.mCancellableTask!!.canceled).isFalse() + awaitTasksCompleted() + + underTest.clear() + + verify(underTest, times(10)).putRecycledView(any(ViewHolder::class.java)) + assertThat(underTest.mCancellableTask!!.canceled).isTrue() + assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0) + } + + private fun awaitTasksCompleted() { + Executors.VIEW_PREINFLATION_EXECUTOR.submit { null }.get() + Executors.MAIN_EXECUTOR.submit { null }.get() + } + + companion object { + private const val VIEW_TYPE: Int = 4 + } +} diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java index ed5762d08c..8e4db5c6cf 100644 --- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java +++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java @@ -21,11 +21,8 @@ import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static org.junit.Assert.assertTrue; -import android.content.BroadcastReceiver; import android.content.ComponentName; -import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.os.Process; import android.system.OsConstants; import android.util.Log; @@ -53,7 +50,6 @@ import org.junit.rules.TestRule; import java.util.Objects; import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; @@ -216,38 +212,6 @@ public abstract class AbstractLauncherUiTest }, mLauncher, timeout); } - /** - * Broadcast receiver which blocks until the result is received. - */ - public class BlockingBroadcastReceiver extends BroadcastReceiver { - - private final CountDownLatch latch = new CountDownLatch(1); - private Intent mIntent; - - public BlockingBroadcastReceiver(String action) { - mTargetContext.registerReceiver(this, new IntentFilter(action), - Context.RECEIVER_EXPORTED/*UNAUDITED*/); - } - - @Override - public void onReceive(Context context, Intent intent) { - mIntent = intent; - latch.countDown(); - } - - public Intent blockingGetIntent() throws InterruptedException { - assertTrue("Timed Out", latch.await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS)); - mTargetContext.unregisterReceiver(this); - return mIntent; - } - - public Intent blockingGetExtraIntent() throws InterruptedException { - Intent intent = blockingGetIntent(); - return intent == null ? null : (Intent) intent.getParcelableExtra( - Intent.EXTRA_INTENT); - } - } - public static void startAppFast(String packageName) { startIntent( getInstrumentation().getContext().getPackageManager().getLaunchIntentForPackage( diff --git a/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java similarity index 54% rename from tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java rename to tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java index 7845222b6e..bb645d714f 100644 --- a/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java +++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java @@ -1,17 +1,17 @@ /* * Copyright (C) 2017 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 + * 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 + * 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. + * 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.widget; @@ -24,23 +24,34 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import android.appwidget.AppWidgetManager; +import android.content.Context; import android.content.Intent; +import android.os.Process; import android.view.View; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; -import androidx.test.runner.AndroidJUnit4; +import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Launcher; import com.android.launcher3.celllayout.FavoriteItemsTransaction; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.testcomponent.WidgetConfigActivity; -import com.android.launcher3.ui.AbstractLauncherUiTest; import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape; import com.android.launcher3.ui.TestViewHelpers; +import com.android.launcher3.util.BaseLauncherActivityTest; +import com.android.launcher3.util.BlockingBroadcastReceiver; +import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.Wait; import com.android.launcher3.util.rule.ShellCommandRule; +import com.android.launcher3.views.OptionsPopupView; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; +import com.android.launcher3.widget.PendingAddWidgetInfo; +import com.android.launcher3.widget.WidgetCell; +import com.android.launcher3.widget.picker.WidgetsFullSheet; +import com.android.launcher3.widget.picker.WidgetsListAdapter; +import com.android.launcher3.widget.picker.WidgetsRecyclerView; import org.junit.Before; import org.junit.Rule; @@ -52,7 +63,7 @@ import org.junit.runner.RunWith; */ @LargeTest @RunWith(AndroidJUnit4.class) -public class TaplAddConfigWidgetTest extends AbstractLauncherUiTest { +public class AddConfigWidgetTest extends BaseLauncherActivityTest { @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind(); @@ -62,12 +73,10 @@ public class TaplAddConfigWidgetTest extends AbstractLauncherUiTest { private int mWidgetId; - @Override @Before public void setUp() throws Exception { - super.setUp(); mWidgetInfo = TestViewHelpers.findWidgetProvider(true /* hasConfigureScreen */); - mAppWidgetManager = AppWidgetManager.getInstance(mTargetContext); + mAppWidgetManager = AppWidgetManager.getInstance(targetContext()); } @Test @@ -82,80 +91,86 @@ public class TaplAddConfigWidgetTest extends AbstractLauncherUiTest { runTest(false); } - /** * @param acceptConfig accept the config activity */ private void runTest(boolean acceptConfig) throws Throwable { - commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext)); + new FavoriteItemsTransaction(targetContext()).commit(); + loadLauncherSync(); - // Drag widget to homescreen + // Add widget to homescreen WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor(); - mLauncher.getWorkspace() - .openAllWidgets() - .getWidget(mWidgetInfo.getLabel()) - .dragToWorkspace(true, false); + executeOnLauncher(OptionsPopupView::openWidgets); + uiDevice.waitForIdle(); + + // Select the widget header + Context testContext = getInstrumentation().getContext(); + String packageName = testContext.getPackageName(); + executeOnLauncher(l -> { + WidgetsRecyclerView wrv = WidgetsFullSheet.getWidgetsView(l); + WidgetsListAdapter adapter = (WidgetsListAdapter) wrv.getAdapter(); + int pos = adapter.getItems().indexOf( + adapter.getItems().stream() + .filter(entry -> packageName.equals(entry.mPkgItem.packageName)) + .findFirst() + .get()); + wrv.getLayoutManager().scrollToPosition(pos); + adapter.onHeaderClicked(true, new PackageUserKey(packageName, Process.myUserHandle())); + }); + uiDevice.waitForIdle(); + + View widgetView = getOnceNotNull("Widget not found", l -> searchView(l.getDragLayer(), v -> + v instanceof WidgetCell + && v.getTag() instanceof PendingAddWidgetInfo pawi + && mWidgetInfo.provider.equals(pawi.componentName))); + addToWorkspace(widgetView); + // Widget id for which the config activity was opened mWidgetId = monitor.getWidgetId(); // Verify that the widget id is valid and bound assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId)); + setResult(acceptConfig); - setResultAndWaitForAnimation(acceptConfig); if (acceptConfig) { - Wait.atMost("", new WidgetSearchCondition(), mLauncher); + getOnceNotNull("Widget was not added", l -> { + // Close the resize frame before searching for widget + AbstractFloatingView.closeAllOpenViews(l); + return l.getWorkspace().getFirstMatch(new WidgetSearchCondition()); + }); assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId)); } else { // Verify that the widget id is deleted. - Wait.atMost("", () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null, - mLauncher); + Wait.atMost("", () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null); } } - private static void setResult(boolean success) { + private void setResult(boolean success) { getInstrumentation().getTargetContext().sendBroadcast( WidgetConfigActivity.getCommandIntent(WidgetConfigActivity.class, success ? "clickOK" : "clickCancel")); - } - - private void setResultAndWaitForAnimation(boolean success) { - if (mLauncher.isLauncher3()) { - setResult(success); - } else { - mLauncher.executeAndWaitForWallpaperAnimation( - () -> setResult(success), - "setting widget coinfig result"); - } + uiDevice.waitForIdle(); } /** * Condition for searching widget id */ - private class WidgetSearchCondition implements Wait.Condition, ItemOperator { - - @Override - public boolean isTrue() throws Throwable { - return mMainThreadExecutor.submit(() -> { - Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedContext(); - return l != null && l.getWorkspace().getFirstMatch(this) != null; - }).get(); - } + private class WidgetSearchCondition implements ItemOperator { @Override public boolean evaluate(ItemInfo info, View view) { - return info instanceof LauncherAppWidgetInfo - && ((LauncherAppWidgetInfo) info).providerName.getClassName().equals( - mWidgetInfo.provider.getClassName()) - && ((LauncherAppWidgetInfo) info).appWidgetId == mWidgetId; + return info instanceof LauncherAppWidgetInfo lawi + && lawi.providerName.equals(mWidgetInfo.provider) + && lawi.appWidgetId == mWidgetId; } } /** * Broadcast receiver for receiving widget config activity status. */ - private class WidgetConfigStartupMonitor extends BlockingBroadcastReceiver { + private static class WidgetConfigStartupMonitor extends BlockingBroadcastReceiver { - public WidgetConfigStartupMonitor() { + WidgetConfigStartupMonitor() { super(WidgetConfigActivity.class.getName()); } diff --git a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java similarity index 77% rename from tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java rename to tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java index 4cdbd96641..8846d6593a 100644 --- a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java +++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java @@ -1,17 +1,17 @@ /* * Copyright (C) 2017 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 + * 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 + * 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. + * 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.widget; @@ -22,12 +22,12 @@ import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_PROVID import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_RESTORE_STARTED; import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; +import static com.android.launcher3.util.TestUtil.getOnUiThread; +import static com.android.launcher3.util.Wait.atMost; import static com.android.launcher3.util.WidgetUtils.createWidgetInfo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; import android.appwidget.AppWidgetManager; import android.content.ComponentName; @@ -36,6 +36,7 @@ import android.content.pm.PackageInstaller.SessionParams; import android.content.pm.PackageManager; import android.database.Cursor; import android.os.Bundle; +import android.text.TextUtils; import android.widget.RemoteViews; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -49,13 +50,12 @@ import com.android.launcher3.R; import com.android.launcher3.celllayout.FavoriteItemsTransaction; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.pm.InstallSessionHelper; -import com.android.launcher3.tapl.Widget; -import com.android.launcher3.tapl.Workspace; -import com.android.launcher3.ui.AbstractLauncherUiTest; import com.android.launcher3.ui.TestViewHelpers; -import com.android.launcher3.util.TestUtil; +import com.android.launcher3.util.BaseLauncherActivityTest; import com.android.launcher3.util.rule.ShellCommandRule; +import com.android.launcher3.widget.LauncherAppWidgetHostView; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; +import com.android.launcher3.widget.PendingAppWidgetHostView; import com.android.launcher3.widget.WidgetManagerHelper; import org.junit.After; @@ -67,6 +67,7 @@ import org.junit.runner.RunWith; import java.util.HashSet; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; /** * Tests for bind widget flow. @@ -75,7 +76,7 @@ import java.util.function.Consumer; */ @LargeTest @RunWith(AndroidJUnit4.class) -public class TaplBindWidgetTest extends AbstractLauncherUiTest { +public class BindWidgetTest extends BaseLauncherActivityTest { @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind(); @@ -87,11 +88,9 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest { private LauncherModel mModel; - @Override @Before public void setUp() throws Exception { - super.setUp(); - mModel = LauncherAppState.getInstance(mTargetContext).getModel(); + mModel = LauncherAppState.getInstance(targetContext()).getModel(); } @After @@ -101,7 +100,7 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest { } if (mSessionId > -1) { - mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId); + targetContext().getPackageManager().getPackageInstaller().abandonSession(mSessionId); } } @@ -122,13 +121,12 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest { LauncherAppWidgetProviderInfo info = addWidgetToScreen(false, false, item -> item.appWidgetId = -33); - final Workspace workspace = mLauncher.getWorkspace(); // Item deleted from db mCursor = queryItem(); assertEquals(0, mCursor.getCount()); // The view does not exist - assertTrue("Widget exists", workspace.tryGetWidget(info.label, 0) == null); + verifyItemEventuallyNull("Widget exists", widgetProvider(info)); } @Test @@ -154,18 +152,19 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest { // Widget has a valid Id now. assertEquals(0, mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED)) & FLAG_ID_NOT_VALID); - assertNotNull(AppWidgetManager.getInstance(mTargetContext) + assertNotNull(AppWidgetManager.getInstance(targetContext()) .getAppWidgetInfo(mCursor.getInt(mCursor.getColumnIndex( LauncherSettings.Favorites.APPWIDGET_ID)))); // send OPTION_APPWIDGET_RESTORE_COMPLETED int appWidgetId = mCursor.getInt( mCursor.getColumnIndex(LauncherSettings.Favorites.APPWIDGET_ID)); - AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mTargetContext); + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(targetContext()); Bundle b = new Bundle(); b.putBoolean(WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED, true); - RemoteViews remoteViews = new RemoteViews(mTargetPackage, R.layout.appwidget_not_ready); + RemoteViews remoteViews = new RemoteViews( + targetContext().getPackageName(), R.layout.appwidget_not_ready); appWidgetManager.updateAppWidgetOptions(appWidgetId, b); appWidgetManager.updateAppWidget(appWidgetId, remoteViews); @@ -175,15 +174,14 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest { WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED)); executeOnLauncher(l -> l.getAppWidgetHolder().startListening()); verifyWidgetPresent(info); - assertNull(mLauncher.getWorkspace().tryGetPendingWidget(100)); + verifyItemEventuallyNull("Pending widget exists", pendingWidgetProvider()); } @Test public void testPendingWidget_notRestored_removed() { addPendingItemToScreen(getInvalidWidgetInfo(), FLAG_ID_NOT_VALID | FLAG_PROVIDER_NOT_READY); - assertTrue("Pending widget exists", - mLauncher.getWorkspace().tryGetPendingWidget(0) == null); + verifyItemEventuallyNull("Pending widget exists", pendingWidgetProvider()); // Item deleted from db mCursor = queryItem(); assertEquals(0, mCursor.getCount()); @@ -216,7 +214,7 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest { // Create an active installer session SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL); params.setAppPackageName(item.providerName.getPackageName()); - PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller(); + PackageInstaller installer = targetContext().getPackageManager().getPackageInstaller(); mSessionId = installer.createSession(params); addPendingItemToScreen(item, FLAG_ID_NOT_VALID | FLAG_PROVIDER_NOT_READY); @@ -234,36 +232,47 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest { } private void verifyWidgetPresent(LauncherAppWidgetProviderInfo info) { - final Widget widget = mLauncher.getWorkspace().tryGetWidget(info.label, - TestUtil.DEFAULT_UI_TIMEOUT); - assertTrue("Widget is not present", - widget != null); + getOnceNotNull("Widget is not present", widgetProvider(info)); } private void verifyPendingWidgetPresent() { - final Widget widget = mLauncher.getWorkspace().tryGetPendingWidget( - TestUtil.DEFAULT_UI_TIMEOUT); - assertTrue("Pending widget is not present", - widget != null); + getOnceNotNull("Widget is not present", pendingWidgetProvider()); + } + + private Function pendingWidgetProvider() { + return l -> l.getWorkspace().getFirstMatch( + (item, view) -> view instanceof PendingAppWidgetHostView); + } + + private Function widgetProvider(LauncherAppWidgetProviderInfo info) { + return l -> l.getWorkspace().getFirstMatch((item, view) -> + view instanceof LauncherAppWidgetHostView + && TextUtils.equals(info.label, view.getContentDescription())); + } + + private void verifyItemEventuallyNull(String message, Function provider) { + atMost(message, () -> getFromLauncher(provider) == null); } private void addPendingItemToScreen(LauncherAppWidgetInfo item, int restoreStatus) { item.restoreStatus = restoreStatus; item.screenId = FIRST_SCREEN_ID; - commitTransactionAndLoadHome( - new FavoriteItemsTransaction(mTargetContext).addItem(() -> item)); + new FavoriteItemsTransaction(targetContext()).addItem(() -> item).commit(); + loadLauncherSync(); } private LauncherAppWidgetProviderInfo addWidgetToScreen(boolean hasConfigureScreen, boolean bindWidget, Consumer itemOverride) { LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(hasConfigureScreen); - commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext) + new FavoriteItemsTransaction(targetContext()) .addItem(() -> { - LauncherAppWidgetInfo item = createWidgetInfo(info, mTargetContext, bindWidget); + LauncherAppWidgetInfo item = + createWidgetInfo(info, targetContext(), bindWidget); item.screenId = FIRST_SCREEN_ID; itemOverride.accept(item); return item; - })); + }).commit(); + loadLauncherSync(); return info; } @@ -277,13 +286,13 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest { Set activePackage = getOnUiThread(() -> { Set packages = new HashSet<>(); - InstallSessionHelper.INSTANCE.get(mTargetContext).getActiveSessions() + InstallSessionHelper.INSTANCE.get(targetContext()).getActiveSessions() .keySet().forEach(packageUserKey -> packages.add(packageUserKey.mPackageName)); return packages; }); while (true) { try { - mTargetContext.getPackageManager().getPackageInfo( + targetContext().getPackageManager().getPackageInfo( pkg, PackageManager.GET_UNINSTALLED_PACKAGES); } catch (Exception e) { if (!activePackage.contains(pkg)) { diff --git a/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java similarity index 61% rename from tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java rename to tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java index fe3b2eea6c..2fb7987f05 100644 --- a/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java +++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java @@ -1,34 +1,41 @@ /* * Copyright (C) 2017 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 + * 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 + * 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. + * 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.widget; import static android.app.PendingIntent.FLAG_MUTABLE; import static android.app.PendingIntent.FLAG_ONE_SHOT; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; +import static java.util.regex.Pattern.CASE_INSENSITIVE; + import android.app.PendingIntent; import android.appwidget.AppWidgetManager; +import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.view.View; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.BySelector; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherSettings.Favorites; @@ -37,14 +44,13 @@ import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.shortcuts.ShortcutKey; -import com.android.launcher3.tapl.AddToHomeScreenPrompt; import com.android.launcher3.testcomponent.AppWidgetNoConfig; import com.android.launcher3.testcomponent.AppWidgetWithConfig; import com.android.launcher3.testcomponent.RequestPinItemActivity; -import com.android.launcher3.ui.AbstractLauncherUiTest; +import com.android.launcher3.util.BaseLauncherActivityTest; +import com.android.launcher3.util.BlockingBroadcastReceiver; import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator; -import com.android.launcher3.util.Wait; -import com.android.launcher3.util.Wait.Condition; +import com.android.launcher3.util.TestUtil; import com.android.launcher3.util.rule.ShellCommandRule; import org.junit.Before; @@ -53,25 +59,27 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.util.UUID; +import java.util.regex.Pattern; /** * Test to verify pin item request flow. */ @LargeTest @RunWith(AndroidJUnit4.class) -public class TaplRequestPinItemTest extends AbstractLauncherUiTest { +public class RequestPinItemTest extends BaseLauncherActivityTest { @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind(); + @Rule + public ShellCommandRule mDefaultLauncherRule = ShellCommandRule.setDefaultLauncher(); + private String mCallbackAction; private String mShortcutId; private int mAppWidgetId; - @Override @Before public void setUp() throws Exception { - super.setUp(); mCallbackAction = UUID.randomUUID().toString(); mShortcutId = UUID.randomUUID().toString(); } @@ -81,9 +89,9 @@ public class TaplRequestPinItemTest extends AbstractLauncherUiTest { @Test public void testPinWidgetNoConfig() throws Throwable { - runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo && - ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId && - ((LauncherAppWidgetInfo) info).providerName.getClassName() + runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo + && ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId + && ((LauncherAppWidgetInfo) info).providerName.getClassName() .equals(AppWidgetNoConfig.class.getName())); } @@ -94,18 +102,18 @@ public class TaplRequestPinItemTest extends AbstractLauncherUiTest { RequestPinItemActivity.class, "setRemoteViewColor").putExtra( RequestPinItemActivity.EXTRA_PARAM + "0", Color.RED); - runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo && - ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId && - ((LauncherAppWidgetInfo) info).providerName.getClassName() + runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo + && ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId + && ((LauncherAppWidgetInfo) info).providerName.getClassName() .equals(AppWidgetNoConfig.class.getName()), command); } @Test public void testPinWidgetWithConfig() throws Throwable { runTest("pinWidgetWithConfig", true, - (info, view) -> info instanceof LauncherAppWidgetInfo && - ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId && - ((LauncherAppWidgetInfo) info).providerName.getClassName() + (info, view) -> info instanceof LauncherAppWidgetInfo + && ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId + && ((LauncherAppWidgetInfo) info).providerName.getClassName() .equals(AppWidgetWithConfig.class.getName())); } @@ -119,47 +127,48 @@ public class TaplRequestPinItemTest extends AbstractLauncherUiTest { runTest("pinShortcut", false, new ItemOperator() { @Override public boolean evaluate(ItemInfo info, View view) { - return info instanceof WorkspaceItemInfo && - info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT && - ShortcutKey.fromItemInfo(info).getId().equals(mShortcutId); + return info instanceof WorkspaceItemInfo + && info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT + && ShortcutKey.fromItemInfo(info).getId().equals(mShortcutId); } }, command); } private void runTest(String activityMethod, boolean isWidget, ItemOperator itemMatcher, Intent... commandIntents) throws Throwable { - commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext)); + new FavoriteItemsTransaction(targetContext()).commit(); + loadLauncherSync(); // Open Pin item activity BlockingBroadcastReceiver openMonitor = new BlockingBroadcastReceiver( RequestPinItemActivity.class.getName()); - mLauncher. - getWorkspace(). - switchToAllApps(). - getAppIcon("Test Pin Item"). - launch(getAppPackageName()); + Context testContext = getInstrumentation().getContext(); + startAppFast( + testContext.getPackageName(), + new Intent(testContext, RequestPinItemActivity.class)); assertNotNull(openMonitor.blockingGetExtraIntent()); // Set callback - PendingIntent callback = PendingIntent.getBroadcast(mTargetContext, 0, - new Intent(mCallbackAction).setPackage(mTargetContext.getPackageName()), + PendingIntent callback = PendingIntent.getBroadcast(targetContext(), 0, + new Intent(mCallbackAction).setPackage(targetContext().getPackageName()), FLAG_ONE_SHOT | FLAG_MUTABLE); - mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent( + targetContext().sendBroadcast(RequestPinItemActivity.getCommandIntent( RequestPinItemActivity.class, "setCallback").putExtra( RequestPinItemActivity.EXTRA_PARAM + "0", callback)); for (Intent command : commandIntents) { - mTargetContext.sendBroadcast(command); + targetContext().sendBroadcast(command); } // call the requested method to start the flow - mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent( + targetContext().sendBroadcast(RequestPinItemActivity.getCommandIntent( RequestPinItemActivity.class, activityMethod)); - final AddToHomeScreenPrompt addToHomeScreenPrompt = mLauncher.getAddToHomeScreenPrompt(); // Accept confirmation: BlockingBroadcastReceiver resultReceiver = new BlockingBroadcastReceiver(mCallbackAction); - addToHomeScreenPrompt.addAutomatically(); + BySelector selector = By.text(Pattern.compile("^Add to home screen$", CASE_INSENSITIVE)) + .pkg(targetContext().getPackageName()); + uiDevice.wait(device -> device.findObject(selector), TestUtil.DEFAULT_UI_TIMEOUT).click(); Intent result = resultReceiver.blockingGetIntent(); assertNotNull(result); mAppWidgetId = result.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); @@ -167,28 +176,9 @@ public class TaplRequestPinItemTest extends AbstractLauncherUiTest { assertNotSame(-1, mAppWidgetId); } - // Go back to home - mLauncher.goHome(); - Wait.atMost("", new ItemSearchCondition(itemMatcher), mLauncher); - } - - /** - * Condition for for an item - */ - private class ItemSearchCondition implements Condition { - - private final ItemOperator mOp; - - ItemSearchCondition(ItemOperator op) { - mOp = op; - } - - @Override - public boolean isTrue() throws Throwable { - return mMainThreadExecutor.submit(() -> { - Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedContext(); - return l != null && l.getWorkspace().getFirstMatch(mOp) != null; - }).get(); - } + // Reload activity, so that the activity is focused + closeCurrentActivity(); + loadLauncherSync(); + getOnceNotNull("", l -> l.getWorkspace().getFirstMatch(itemMatcher)); } } diff --git a/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java b/tests/src/com/android/launcher3/ui/widget/WidgetPickerTest.java similarity index 68% rename from tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java rename to tests/src/com/android/launcher3/ui/widget/WidgetPickerTest.java index 19c585085b..caad1d98b7 100644 --- a/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java +++ b/tests/src/com/android/launcher3/ui/widget/WidgetPickerTest.java @@ -22,24 +22,30 @@ import static org.junit.Assert.assertTrue; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.MediumTest; +import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Launcher; -import com.android.launcher3.tapl.Widgets; -import com.android.launcher3.ui.AbstractLauncherUiTest; import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape; +import com.android.launcher3.util.BaseLauncherActivityTest; +import com.android.launcher3.util.rule.ScreenRecordRule; import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord; +import com.android.launcher3.views.OptionsPopupView; import com.android.launcher3.widget.picker.WidgetsFullSheet; import com.android.launcher3.widget.picker.WidgetsRecyclerView; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestRule; import org.junit.runner.RunWith; /** - * This test run in both Out of process (Oop) and in-process (Ipc). * Make sure the basic interactions with the WidgetPicker works. */ @MediumTest @RunWith(AndroidJUnit4.class) -public class TaplWidgetPickerTest extends AbstractLauncherUiTest { +public class WidgetPickerTest extends BaseLauncherActivityTest { + + @Rule + public TestRule screenRecordRule = new ScreenRecordRule(); private WidgetsRecyclerView getWidgetsView(Launcher launcher) { return WidgetsFullSheet.getWidgetsView(launcher); @@ -56,30 +62,21 @@ public class TaplWidgetPickerTest extends AbstractLauncherUiTest { @ScreenRecord @PortraitLandscape public void testWidgets() { - mLauncher.goHome(); + loadLauncherSync(); // Test opening widgets. executeOnLauncher(launcher -> assertTrue("Widgets is initially opened", getWidgetsView(launcher) == null)); - Widgets widgets = mLauncher.getWorkspace().openAllWidgets(); - assertNotNull("openAllWidgets() returned null", widgets); - widgets = mLauncher.getAllWidgets(); + assertNotNull("openAllWidgets() returned null", + getFromLauncher(OptionsPopupView::openWidgets)); + WidgetsRecyclerView widgets = getFromLauncher(this::getWidgetsView); assertNotNull("getAllWidgets() returned null", widgets); - executeOnLauncher(launcher -> - assertTrue("Widgets is not shown", getWidgetsView(launcher).isShown())); + executeOnLauncher(launcher -> assertTrue("Widgets is not shown", widgets.isShown())); executeOnLauncher(launcher -> assertEquals("Widgets is scrolled upon opening", 0, getWidgetsScroll(launcher))); - // Test flinging widgets. - widgets.flingForward(); - Integer flingForwardY = getFromLauncher(launcher -> getWidgetsScroll(launcher)); - executeOnLauncher(launcher -> assertTrue("Flinging forward didn't scroll widgets", - flingForwardY > 0)); + executeOnLauncher(AbstractFloatingView::closeAllOpenViews); + uiDevice.waitForIdle(); - widgets.flingBackward(); - executeOnLauncher(launcher -> assertTrue("Flinging backward didn't scroll widgets", - getWidgetsScroll(launcher) < flingForwardY)); - - mLauncher.goHome(); waitForLauncherCondition("Widgets were not closed", launcher -> getWidgetsView(launcher) == null); } diff --git a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java index cfc0a6b286..c623513f07 100644 --- a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java +++ b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java @@ -15,8 +15,6 @@ */ package com.android.launcher3.ui.workspace; -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; - import static com.android.launcher3.AbstractFloatingView.TYPE_ACTION_POPUP; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME; @@ -29,11 +27,9 @@ import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentValues; import android.net.Uri; -import android.view.View; import android.view.ViewGroup; import androidx.test.filters.LargeTest; -import androidx.test.uiautomator.UiDevice; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BubbleTextView; @@ -49,9 +45,6 @@ import com.android.launcher3.util.TestUtil; import org.junit.Test; -import java.util.ArrayDeque; -import java.util.Queue; - /** * Tests for theme icon support in Launcher * @@ -137,27 +130,10 @@ public class ThemeIconsTest extends BaseLauncherActivityTest { } catch (Exception e) { throw new RuntimeException(e); } - - // Find the app icon - Queue viewQueue = new ArrayDeque<>(); - viewQueue.add(parent); - BubbleTextView icon = null; - while (!viewQueue.isEmpty()) { - View view = viewQueue.poll(); - if (view instanceof ViewGroup) { - parent = (ViewGroup) view; - for (int i = parent.getChildCount() - 1; i >= 0; i--) { - viewQueue.add(parent.getChildAt(i)); - } - } else if (view instanceof BubbleTextView btv) { - if (btv.getContentDescription() != null - && title.equals(btv.getContentDescription().toString())) { - icon = btv; - break; - } - } - } - return icon; + return (BubbleTextView) searchView(parent, v -> + v instanceof BubbleTextView btv + && btv.getContentDescription() != null + && title.equals(btv.getContentDescription().toString())); } private BubbleTextView verifyIconTheme(String title, ViewGroup parent, boolean isThemed) { @@ -193,11 +169,4 @@ public class ThemeIconsTest extends BaseLauncherActivityTest { rv.getLayoutManager().scrollToPosition(pos); }); } - - private void addToWorkspace(View btv) { - TestUtil.runOnExecutorSync(MAIN_EXECUTOR, () -> - btv.getAccessibilityDelegate().performAccessibilityAction( - btv, com.android.launcher3.R.id.action_add_to_workspace, null)); - UiDevice.getInstance(getInstrumentation()).waitForIdle(); - } } diff --git a/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt b/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt index 476e497807..644659296d 100644 --- a/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt +++ b/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt @@ -22,6 +22,9 @@ import android.view.InputDevice import android.view.KeyCharacterMap import android.view.KeyEvent import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import androidx.core.view.children import androidx.lifecycle.Lifecycle.State.RESUMED import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario.ActivityAction @@ -30,11 +33,13 @@ import androidx.test.uiautomator.UiDevice import com.android.launcher3.Launcher import com.android.launcher3.LauncherAppState import com.android.launcher3.LauncherState +import com.android.launcher3.R import com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST import com.android.launcher3.tapl.TestHelpers import com.android.launcher3.util.ModelTestExtensions.loadModelSync import com.android.launcher3.util.Wait.atMost import java.util.function.Function +import java.util.function.Predicate import java.util.function.Supplier import org.junit.After @@ -56,6 +61,8 @@ open class BaseLauncherActivityTest { ) .also { currentScenario = it } + @JvmField val uiDevice = UiDevice.getInstance(getInstrumentation()) + @After fun closeCurrentActivity() { currentScenario?.close() @@ -136,18 +143,43 @@ open class BaseLauncherActivityTest { event.recycle() } - fun startAppFast(packageName: String) { - val intent = targetContext().packageManager.getLaunchIntentForPackage(packageName)!! + @JvmOverloads + fun startAppFast( + packageName: String, + intent: Intent = targetContext().packageManager.getLaunchIntentForPackage(packageName)!!, + ) { intent.addCategory(Intent.CATEGORY_LAUNCHER) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) targetContext().startActivity(intent) - UiDevice.getInstance(getInstrumentation()).waitForIdle() + uiDevice.waitForIdle() } fun freezeAllApps() = executeOnLauncher { it.appsView.appsStore.enableDeferUpdates(DEFER_UPDATES_TEST) } - fun executeShellCommand(cmd: String) = - UiDevice.getInstance(getInstrumentation()).executeShellCommand(cmd) + fun executeShellCommand(cmd: String) = uiDevice.executeShellCommand(cmd) + + fun addToWorkspace(view: View) { + TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) { + view.accessibilityDelegate.performAccessibilityAction( + view, + R.id.action_add_to_workspace, + null, + ) + } + UiDevice.getInstance(getInstrumentation()).waitForIdle() + } + + fun ViewGroup.searchView(filter: Predicate): View? { + if (filter.test(this)) return this + for (child in children) { + if (filter.test(child)) return child + if (child is ViewGroup) + child.searchView(filter)?.let { + return it + } + } + return null + } } diff --git a/tests/src/com/android/launcher3/util/BlockingBroadcastReceiver.kt b/tests/src/com/android/launcher3/util/BlockingBroadcastReceiver.kt new file mode 100644 index 0000000000..20881d19fb --- /dev/null +++ b/tests/src/com/android/launcher3/util/BlockingBroadcastReceiver.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 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.util + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Parcelable +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit.SECONDS + +private const val DEFAULT_BROADCAST_TIMEOUT_SECS: Long = 10 + +/** Broadcast receiver which blocks until the result is received. */ +open class BlockingBroadcastReceiver(action: String) : BroadcastReceiver() { + + val value = CompletableFuture() + + init { + getInstrumentation() + .targetContext + .registerReceiver(this, IntentFilter(action), Context.RECEIVER_EXPORTED) + } + + override fun onReceive(context: Context, intent: Intent) { + value.complete(intent) + } + + @Throws(InterruptedException::class) + fun blockingGetIntent(): Intent = + value.get(DEFAULT_BROADCAST_TIMEOUT_SECS, SECONDS).also { + getInstrumentation().targetContext.unregisterReceiver(this) + } + + @Throws(InterruptedException::class) + fun blockingGetExtraIntent(): Intent? = + blockingGetIntent().getParcelableExtra(Intent.EXTRA_INTENT) as Intent? +}