Merge "Adding MSDL history to Launcher's dump information." into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index e05178d..f9e7cf0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.taskbar;
+import static android.view.KeyEvent.ACTION_UP;
import static android.view.View.AccessibilityDelegate;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
@@ -72,7 +73,6 @@
import android.graphics.drawable.RotateDrawable;
import android.inputmethodservice.InputMethodService;
import android.os.Handler;
-import android.os.SystemClock;
import android.util.Property;
import android.view.Gravity;
import android.view.KeyEvent;
@@ -864,17 +864,12 @@
TaskbarNavButtonController navButtonController) {
buttonView.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_MOVE) return false;
- long time = SystemClock.uptimeMillis();
- int action = event.getAction();
- KeyEvent keyEvent = new KeyEvent(time, time,
- action == MotionEvent.ACTION_DOWN ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP,
- KeyEvent.KEYCODE_BACK, 0);
- if (event.getAction() == MotionEvent.ACTION_CANCEL) {
- keyEvent.cancel();
- }
- navButtonController.executeBack(keyEvent);
-
- if (action == MotionEvent.ACTION_UP) {
+ int motionEventAction = event.getAction();
+ int keyEventAction = motionEventAction == MotionEvent.ACTION_DOWN
+ ? KeyEvent.ACTION_DOWN : ACTION_UP;
+ boolean isCancelled = event.getAction() == MotionEvent.ACTION_CANCEL;
+ navButtonController.sendBackKeyEvent(keyEventAction, isCancelled);
+ if (motionEventAction == MotionEvent.ACTION_UP) {
buttonView.performClick();
}
return false;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index d4764c7..4881836 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -16,7 +16,8 @@
package com.android.launcher3.taskbar;
-import static android.view.MotionEvent.ACTION_UP;
+import static android.view.KeyEvent.ACTION_DOWN;
+import static android.view.KeyEvent.ACTION_UP;
import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS;
import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_KEY;
@@ -38,6 +39,7 @@
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
+import android.os.SystemClock;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
@@ -78,6 +80,7 @@
private long mLastScreenPinLongPress;
private boolean mScreenPinned;
private boolean mAssistantLongPressEnabled;
+ private int mLastSentBackAction = ACTION_UP;
@Override
public void dumpLogs(String prefix, PrintWriter pw) {
@@ -85,6 +88,8 @@
pw.println(prefix + "\tmLastScreenPinLongPress=" + mLastScreenPinLongPress);
pw.println(prefix + "\tmScreenPinned=" + mScreenPinned);
+ pw.println(prefix + "\tmLastSentBackAction="
+ + KeyEvent.actionToString(mLastSentBackAction));
}
@Retention(RetentionPolicy.SOURCE)
@@ -141,6 +146,11 @@
if (buttonType == BUTTON_SPACE) {
return;
}
+ if (predictiveBackThreeButtonNav() && mLastSentBackAction == ACTION_DOWN) {
+ Log.i(TAG, "Button click ignored while back button is pressed");
+ // prevent interactions with other buttons while back button is pressed
+ return;
+ }
// Provide the same haptic feedback that the system offers for virtual keys.
view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
switch (buttonType) {
@@ -180,6 +190,13 @@
if (buttonType == BUTTON_SPACE) {
return false;
}
+ if (predictiveBackThreeButtonNav() && mLastSentBackAction == ACTION_DOWN
+ && buttonType != BUTTON_BACK && buttonType != BUTTON_RECENTS) {
+ // prevent interactions with other buttons while back button is pressed (except back
+ // and recents button for screen-unpin action).
+ Log.i(TAG, "Button long click ignored while back button is pressed");
+ return false;
+ }
// Provide the same haptic feedback that the system offers for long press.
// The haptic feedback from long pressing on the home button is handled by circle to search.
@@ -327,13 +344,27 @@
mCallbacks.onToggleOverview();
}
- void executeBack(@Nullable KeyEvent keyEvent) {
+ void sendBackKeyEvent(int action, boolean cancelled) {
+ if (action == mLastSentBackAction) {
+ // There must always be an alternating sequence of ACTION_DOWN and ACTION_UP events
+ return;
+ }
+ long time = SystemClock.uptimeMillis();
+ KeyEvent keyEvent = new KeyEvent(time, time, action, KeyEvent.KEYCODE_BACK, 0);
+ if (cancelled) {
+ keyEvent.cancel();
+ }
+ executeBack(keyEvent);
+ }
+
+ private void executeBack(@Nullable KeyEvent keyEvent) {
if (keyEvent == null || (keyEvent.getAction() == ACTION_UP && !keyEvent.isCanceled())) {
logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_TAP);
mContextualEduStatsManager.updateEduStats(/* isTrackpadGesture= */ false,
GestureType.BACK);
}
mSystemUiProxy.onBackEvent(keyEvent);
+ mLastSentBackAction = keyEvent != null ? keyEvent.getAction() : ACTION_UP;
}
private void onImeSwitcherPress() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 8816a6d..3c5d71e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -848,6 +848,13 @@
}
/**
+ * The max number of icon views the taskbar can have when taskbar overflow is enabled.
+ */
+ int getMaxNumIconViews() {
+ return mMaxNumIcons;
+ }
+
+ /**
* Returns the all apps button in the taskbar.
*/
public TaskbarAllAppsButtonContainer getAllAppsButtonContainer() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index bb4f07a..bc5f9a3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -351,6 +351,11 @@
OneShotPreDrawListener.add(mTaskbarView, listener);
}
+ @VisibleForTesting
+ int getMaxNumIconViews() {
+ return mTaskbarView.getMaxNumIconViews();
+ }
+
public Rect getIconLayoutVisualBounds() {
return mTaskbarView.getIconLayoutVisualBounds();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index c74fa9b..92fd5e8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -351,14 +351,20 @@
.start();
}
- /** Suppresses or un-suppresses drawing the dot due to an update for this bubble. */
- public void suppressDotForBubbleUpdate(boolean suppress) {
- mDotSuppressedForBubbleUpdate = suppress;
- if (suppress) {
- setDotScale(0);
- } else {
- showDotIfNeeded(/* animate= */ false);
- }
+ /** Suppresses drawing the dot due to an update for this bubble. */
+ public void suppressDotForBubbleUpdate() {
+ mDotSuppressedForBubbleUpdate = true;
+ setDotScale(0);
+ }
+
+ /**
+ * Unsuppresses the dot after the bubble update finished animating.
+ *
+ * @param animate whether or not to animate the dot back in
+ */
+ public void unsuppressDotForBubbleUpdate(boolean animate) {
+ mDotSuppressedForBubbleUpdate = false;
+ showDotIfNeeded(animate);
}
boolean hasUnseenContent() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index 6c354f3..5184a9f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -486,7 +486,7 @@
if (flyout != null) {
bubbleBarFlyoutController.setUpAndShowFlyout(
BubbleBarFlyoutMessage(flyout.icon, flyout.title, flyout.message),
- onInit = { bubbleView.suppressDotForBubbleUpdate(true) },
+ onInit = { bubbleView.suppressDotForBubbleUpdate() },
onEnd = {
moveToState(AnimatingBubble.State.IN)
bubbleStashController.updateTaskbarTouchRegion()
@@ -498,11 +498,12 @@
}
private fun cancelFlyout() {
- bubbleBarFlyoutController.cancelFlyout { onFlyoutRemoved() }
+ animatingBubble?.bubbleView?.unsuppressDotForBubbleUpdate(/* animate= */ true)
+ bubbleBarFlyoutController.cancelFlyout { bubbleStashController.updateTaskbarTouchRegion() }
}
private fun onFlyoutRemoved() {
- animatingBubble?.bubbleView?.suppressDotForBubbleUpdate(false)
+ animatingBubble?.bubbleView?.unsuppressDotForBubbleUpdate(/* animate= */ false)
bubbleStashController.updateTaskbarTouchRegion()
}
@@ -573,7 +574,7 @@
val flyout = bubble?.flyoutMessage
if (flyout != null) {
// the flyout is currently expanding and we need to update it with new data
- bubbleView.suppressDotForBubbleUpdate(true)
+ bubbleView.suppressDotForBubbleUpdate()
bubbleBarFlyoutController.updateFlyoutWhileExpanding(flyout)
} else {
// the flyout is expanding but we don't have new flyout data to update it with,
@@ -588,7 +589,7 @@
isExpanding: Boolean,
) {
// unsuppress the current bubble because we are about to hide its flyout
- animatingBubble.bubbleView.suppressDotForBubbleUpdate(false)
+ animatingBubble.bubbleView.unsuppressDotForBubbleUpdate(/* animate= */ false)
this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
// we're currently idle, waiting for the hide animation to start. update the flyout
@@ -601,7 +602,7 @@
val bubble = bubbleView.bubble as? BubbleBarBubble
val flyout = bubble?.flyoutMessage
if (flyout != null) {
- bubbleView.suppressDotForBubbleUpdate(true)
+ bubbleView.suppressDotForBubbleUpdate()
bubbleBarFlyoutController.updateFlyoutFullyExpanded(flyout) {
bubbleStashController.updateTaskbarTouchRegion()
}
@@ -616,7 +617,7 @@
isExpanding: Boolean,
) {
// unsuppress the current bubble because we are about to hide its flyout
- animatingBubble.bubbleView.suppressDotForBubbleUpdate(false)
+ animatingBubble.bubbleView.unsuppressDotForBubbleUpdate(/* animate= */ false)
this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
// the hide animation already started so it can't be canceled, just post it again
@@ -629,7 +630,7 @@
// the flyout is collapsing. update it with the new flyout
if (flyout != null) {
moveToState(AnimatingBubble.State.ANIMATING_IN)
- bubbleView.suppressDotForBubbleUpdate(true)
+ bubbleView.suppressDotForBubbleUpdate()
bubbleBarFlyoutController.updateFlyoutWhileCollapsing(flyout) {
moveToState(AnimatingBubble.State.IN)
bubbleStashController.updateTaskbarTouchRegion()
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 95e7737..21c4d8c 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -56,9 +56,11 @@
import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED;
import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED;
import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
+import static com.android.quickstep.TaskViewUtils.extractTargetsAndStates;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED;
import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -77,6 +79,7 @@
import android.os.IBinder;
import android.os.SystemClock;
import android.util.Log;
+import android.util.Pair;
import android.view.MotionEvent;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
@@ -90,6 +93,7 @@
import android.widget.Toast;
import android.window.DesktopModeFlags;
import android.window.PictureInPictureSurfaceTransaction;
+import android.window.WindowAnimationState;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -143,6 +147,7 @@
import com.android.quickstep.views.RecentsViewContainer;
import com.android.quickstep.views.TaskContainer;
import com.android.quickstep.views.TaskView;
+import com.android.systemui.animation.TransitionAnimator;
import com.android.systemui.contextualeducation.GestureType;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -156,6 +161,8 @@
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.shared.startingsurface.SplashScreenExitAnimationUtils;
+import kotlin.Unit;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -165,8 +172,6 @@
import java.util.OptionalInt;
import java.util.function.Consumer;
-import kotlin.Unit;
-
/**
* Handles the navigation gestures when Launcher is the default home activity.
*/
@@ -347,6 +352,9 @@
// Indicates whether the divider is shown, only used when split screen is activated.
private boolean mIsDividerShown = true;
private boolean mStartMovingTasks;
+ // Whether the animation to home should be handed off to another handler once the gesture is
+ // committed.
+ protected boolean mHandOffAnimationToHome = false;
@Nullable
private RemoteAnimationTargets.ReleaseCheck mSwipePipToHomeReleaseCheck = null;
@@ -945,6 +953,10 @@
mSwipePipToHomeReleaseCheck = new RemoteAnimationTargets.ReleaseCheck();
mSwipePipToHomeReleaseCheck.setCanRelease(true);
mRecentsAnimationTargets.addReleaseCheck(mSwipePipToHomeReleaseCheck);
+ if (TransitionAnimator.Companion.longLivedReturnAnimationsEnabled()) {
+ mHandOffAnimationToHome =
+ targets.extras.getBoolean(KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION, false);
+ }
// Only initialize the device profile, if it has not been initialized before, as in some
// configurations targets.homeContentInsets may not be correct.
@@ -1629,6 +1641,10 @@
}
windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
+ if (mHandOffAnimationToHome) {
+ handOffAnimation(velocityPxPerMs);
+ }
+
windowAnim[0].addAnimatorListener(new AnimationSuccessListener() {
@Override
public void onAnimationSuccess(Animator animator) {
@@ -1711,6 +1727,19 @@
}
}
+ private void handOffAnimation(PointF velocityPxPerMs) {
+ if (!TransitionAnimator.Companion.longLivedReturnAnimationsEnabled()
+ || mRecentsAnimationController == null) {
+ return;
+ }
+
+ Pair<RemoteAnimationTarget[], WindowAnimationState[]> targetsAndStates =
+ extractTargetsAndStates(mRemoteTargetHandles, velocityPxPerMs);
+ mRecentsAnimationController.handOffAnimation(
+ targetsAndStates.first, targetsAndStates.second);
+ ActiveGestureProtoLogProxy.logHandOffAnimation();
+ }
+
private int calculateWindowRotation(RemoteAnimationTarget runningTaskTarget,
RecentsOrientedState orientationState) {
if (runningTaskTarget.rotationChange != 0) {
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index 6719ab7..f3ed491 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -49,8 +49,8 @@
import android.window.BackEvent;
import android.window.BackMotionEvent;
import android.window.BackProgressAnimator;
+import android.window.IBackAnimationHandoffHandler;
import android.window.IOnBackInvokedCallback;
-
import com.android.app.animation.Interpolators;
import com.android.internal.policy.SystemBarUtils;
import com.android.internal.view.AppearanceRegion;
@@ -225,6 +225,12 @@
public void setTriggerBack(boolean triggerBack) {
// TODO(b/261654570): track touch from the Launcher process.
}
+
+ @Override
+ public void setHandoffHandler(IBackAnimationHandoffHandler unused) {
+ // For now, Launcher handles this internally so it doesn't need to hand off the
+ // animation.
+ }
}
private static class RemoteAnimationRunnerStub extends IRemoteAnimationRunner.Stub {
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index dacafd4..6087dc2 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -46,7 +46,6 @@
import com.android.launcher3.views.FloatingIconView;
import com.android.launcher3.views.FloatingView;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
-import com.android.quickstep.fallback.window.RecentsWindowManager;
import com.android.quickstep.util.RectFSpringAnim;
import com.android.quickstep.util.ScalingWorkspaceRevealAnim;
import com.android.quickstep.util.StaggeredWorkspaceAnim;
@@ -54,6 +53,7 @@
import com.android.quickstep.views.FloatingWidgetView;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
+import com.android.systemui.animation.TransitionAnimator;
import com.android.systemui.shared.system.InputConsumerController;
import java.util.Collections;
@@ -108,7 +108,9 @@
mContainer.getRootView().setForceHideBackArrow(true);
- if (!canUseWorkspaceView || appCanEnterPip || mIsSwipeForSplit) {
+ boolean handOffAnimation = TransitionAnimator.Companion.longLivedReturnAnimationsEnabled()
+ && mHandOffAnimationToHome;
+ if (handOffAnimation || !canUseWorkspaceView || appCanEnterPip || mIsSwipeForSplit) {
return new LauncherHomeAnimationFactory() {
@Nullable
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 60fcff8..145773d 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -21,9 +21,11 @@
import android.os.Bundle;
import android.os.RemoteException;
import android.util.Log;
+import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.WindowManagerGlobal;
import android.window.PictureInPictureSurfaceTransaction;
+import android.window.WindowAnimationState;
import androidx.annotation.UiThread;
@@ -32,6 +34,7 @@
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.RunnableList;
import com.android.quickstep.util.ActiveGestureProtoLogProxy;
+import com.android.systemui.animation.TransitionAnimator;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
@@ -90,6 +93,16 @@
}
@UiThread
+ public void handOffAnimation(RemoteAnimationTarget[] targets, WindowAnimationState[] states) {
+ if (TransitionAnimator.Companion.longLivedReturnAnimationsEnabled()) {
+ UI_HELPER_EXECUTOR.execute(() -> mController.handOffAnimation(targets, states));
+ } else {
+ Log.e(TAG, "Tried to hand off the animation, but the feature is disabled",
+ new Exception());
+ }
+ }
+
+ @UiThread
public void finishAnimationToHome() {
finishController(true /* toRecents */, null, false /* sendUserLeaveHint */);
}
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 07ee479..783c87c 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -47,12 +47,15 @@
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Matrix.ScaleToFit;
+import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.util.Pair;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.View;
import android.window.TransitionInfo;
+import android.window.WindowAnimationState;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -784,4 +787,43 @@
animatorHandler.accept(dockFadeAnimator);
return dockFadeAnimator;
}
+
+ /**
+ * Creates an array of {@link RemoteAnimationTarget}s and a matching array of
+ * {@link WindowAnimationState}s from the provided handles.
+ * Important: the ordering of the two arrays is the same, so the state at each index of the
+ * second applies to the target in the same index of the first.
+ *
+ * @param handles The handles wrapping each target.
+ * @param velocityPxPerMs The current velocity of the target animations.
+ */
+ @NonNull
+ public static Pair<RemoteAnimationTarget[], WindowAnimationState[]> extractTargetsAndStates(
+ @NonNull RemoteTargetHandle[] handles, @NonNull PointF velocityPxPerMs) {
+ RemoteAnimationTarget[] targets = new RemoteAnimationTarget[handles.length];
+ WindowAnimationState[] animationStates = new WindowAnimationState[handles.length];
+ long timestamp = System.currentTimeMillis();
+
+ for (int i = 0; i < handles.length; i++) {
+ targets[i] = handles[i].getTransformParams().getTargetSet().apps[i];
+
+ TaskViewSimulator taskViewSimulator = handles[i].getTaskViewSimulator();
+ RectF startRect = taskViewSimulator.getCurrentRect();
+ float cornerRadius = taskViewSimulator.getCurrentCornerRadius();
+
+ WindowAnimationState state = new WindowAnimationState();
+ state.timestamp = timestamp;
+ state.bounds = new RectF(
+ startRect.left, startRect.top, startRect.right, startRect.bottom);
+ state.topLeftRadius = cornerRadius;
+ state.topRightRadius = cornerRadius;
+ state.bottomRightRadius = cornerRadius;
+ state.bottomLeftRadius = cornerRadius;
+ state.velocityPxPerMs = velocityPxPerMs;
+
+ animationStates[i] = state;
+ }
+
+ return new Pair<>(targets, animationStates);
+ }
}
diff --git a/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
index f43a125..0091036 100644
--- a/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
+++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
@@ -96,6 +96,12 @@
+ "force finish recents animation complete; clearing state callback.");
}
+ public static void logHandOffAnimation() {
+ ActiveGestureLog.INSTANCE.addLog("AbsSwipeUpHandler.handOffAnimation");
+ if (!enableActiveGestureProtoLog()) return;
+ ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.handOffAnimation");
+ }
+
public static void logFinishRecentsAnimationOnTasksAppeared() {
ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimationOnTasksAppeared");
if (!enableActiveGestureProtoLog()) return;
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
index 4b04dba..6ebae49 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
@@ -15,9 +15,11 @@
import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS;
import static com.android.launcher3.taskbar.TaskbarNavButtonController.SCREEN_PIN_LONG_PRESS_THRESHOLD;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
+import static com.android.window.flags.Flags.FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -28,6 +30,10 @@
import static org.mockito.Mockito.when;
import android.os.Handler;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.Flags;
@@ -43,8 +49,10 @@
import com.android.systemui.contextualeducation.GestureType;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -76,6 +84,9 @@
@Mock
View mockView;
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
private int mHomePressCount;
private int mOverviewToggleCount;
private final TaskbarNavButtonCallbacks mCallbacks = new TaskbarNavButtonCallbacks() {
@@ -333,4 +344,46 @@
verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS);
verify(mockStatsLogger, times(0)).log(LAUNCHER_TASKBAR_BACK_BUTTON_TAP);
}
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV)
+ public void testPredictiveBackInvoked() {
+ ArgumentCaptor<KeyEvent> keyEventCaptor = ArgumentCaptor.forClass(KeyEvent.class);
+ mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false);
+ mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, false);
+ verify(mockSystemUiProxy, times(2)).onBackEvent(keyEventCaptor.capture());
+ verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_DOWN, false);
+ verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_UP, false);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV)
+ public void testPredictiveBackCancelled() {
+ ArgumentCaptor<KeyEvent> keyEventCaptor = ArgumentCaptor.forClass(KeyEvent.class);
+ mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false);
+ mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, true);
+ verify(mockSystemUiProxy, times(2)).onBackEvent(keyEventCaptor.capture());
+ verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_DOWN, false);
+ verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_UP, true);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV)
+ public void testButtonsDisabledWhileBackPressed() {
+ mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false);
+ mNavButtonController.onButtonClick(BUTTON_HOME, mockView);
+ mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView);
+ mNavButtonController.onButtonLongClick(BUTTON_A11Y, mockView);
+ mNavButtonController.onButtonClick(BUTTON_IME_SWITCH, mockView);
+ mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, false);
+ assertThat(mHomePressCount).isEqualTo(0);
+ verify(mockSystemUiProxy, never()).notifyAccessibilityButtonLongClicked();
+ assertThat(mOverviewToggleCount).isEqualTo(0);
+ verify(mockSystemUiProxy, never()).onImeSwitcherPressed();
+ }
+
+ private void verifyKeyEvent(KeyEvent keyEvent, int action, boolean isCancelled) {
+ assertEquals(isCancelled, keyEvent.isCanceled());
+ assertEquals(action, KeyEvent.ACTION_DOWN, keyEvent.getAction());
+ }
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
new file mode 100644
index 0000000..cc8582c
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
@@ -0,0 +1,313 @@
+/*
+ * 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.taskbar
+
+import android.content.ComponentName
+import android.content.Intent
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import com.android.launcher3.Flags.FLAG_TASKBAR_OVERFLOW
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
+import com.android.launcher3.taskbar.rules.MockedRecentsModelTestRule
+import com.android.launcher3.taskbar.rules.TaskbarModeRule
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.android.launcher3.util.TestUtil.getOnUiThread
+import com.android.quickstep.SystemUiProxy
+import com.android.quickstep.util.DesktopTask
+import com.android.systemui.shared.recents.model.Task
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS
+import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR
+import com.android.wm.shell.desktopmode.IDesktopTaskListener
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelTablet2023"])
+@EnableFlags(
+ FLAG_TASKBAR_OVERFLOW,
+ FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS,
+ FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ FLAG_ENABLE_BUBBLE_BAR,
+)
+class TaskbarOverflowTest {
+ @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
+
+ @get:Rule(order = 1)
+ val context =
+ TaskbarWindowSandboxContext.create { builder ->
+ builder.bindSystemUiProxy(
+ object : SystemUiProxy(this) {
+ override fun setDesktopTaskListener(listener: IDesktopTaskListener?) {
+ desktopTaskListener = listener
+ }
+ }
+ )
+ }
+
+ @get:Rule(order = 2) val recentsModel = MockedRecentsModelTestRule(context)
+
+ @get:Rule(order = 3) val taskbarModeRule = TaskbarModeRule(context)
+
+ @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+ @InjectController lateinit var taskbarViewController: TaskbarViewController
+ @InjectController lateinit var recentAppsController: TaskbarRecentAppsController
+ @InjectController lateinit var bubbleBarViewController: BubbleBarViewController
+ @InjectController lateinit var bubbleStashController: BubbleStashController
+
+ private var desktopTaskListener: IDesktopTaskListener? = null
+
+ @Before
+ fun ensureRunningAppsShowing() {
+ runOnMainSync {
+ if (!recentAppsController.canShowRunningApps) {
+ recentAppsController.onDestroy()
+ recentAppsController.canShowRunningApps = true
+ recentAppsController.init(taskbarUnitTestRule.activityContext.controllers)
+ }
+ recentsModel.resolvePendingTaskRequests()
+ }
+ }
+
+ @Test
+ @TaskbarMode(PINNED)
+ fun testTaskbarWithMaxNumIcons_pinned() {
+ addRunningAppsAndVerifyOverflowState(0)
+
+ assertThat(taskbarIconsCentered).isTrue()
+ assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing)
+ }
+
+ @Test
+ @TaskbarMode(TRANSIENT)
+ fun testTaskbarWithMaxNumIcons_transient() {
+ addRunningAppsAndVerifyOverflowState(0)
+
+ assertThat(taskbarIconsCentered).isTrue()
+ assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing)
+ }
+
+ @Test
+ @TaskbarMode(PINNED)
+ fun testOverflownTaskbar_pinned() {
+ addRunningAppsAndVerifyOverflowState(5)
+
+ assertThat(taskbarIconsCentered).isTrue()
+ assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing)
+ }
+
+ @Test
+ @TaskbarMode(TRANSIENT)
+ fun testOverflownTaskbar_transient() {
+ addRunningAppsAndVerifyOverflowState(5)
+
+ assertThat(taskbarIconsCentered).isTrue()
+ assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing)
+ }
+
+ @Test
+ @TaskbarMode(PINNED)
+ fun testBubbleBarReducesTaskbarMaxNumIcons_pinned() {
+ var initialMaxNumIconViews = maxNumberOfTaskbarIcons
+ assertThat(initialMaxNumIconViews).isGreaterThan(0)
+
+ runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+
+ val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2)
+ assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews)
+
+ assertThat(taskbarIconsCentered).isTrue()
+ }
+
+ @Test
+ @TaskbarMode(TRANSIENT)
+ fun testBubbleBarReducesTaskbarMaxNumIcons_transient() {
+ var initialMaxNumIconViews = maxNumberOfTaskbarIcons
+ assertThat(initialMaxNumIconViews).isGreaterThan(0)
+
+ runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+
+ val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2)
+ assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews)
+
+ assertThat(taskbarIconsCentered).isTrue()
+ assertThat(taskbarEndMargin)
+ .isAtLeast(
+ navButtonEndSpacing +
+ bubbleBarViewController.collapsedWidthWithMaxVisibleBubbles.toInt()
+ )
+ }
+
+ @Test
+ @TaskbarMode(TRANSIENT)
+ fun testBubbleBarReducesTaskbarMaxNumIcons_transientBubbleInitiallyStashed() {
+ var initialMaxNumIconViews = maxNumberOfTaskbarIcons
+ assertThat(initialMaxNumIconViews).isGreaterThan(0)
+ runOnMainSync {
+ bubbleStashController.stashBubbleBarImmediate()
+ bubbleBarViewController.setHiddenForBubbles(false)
+ }
+
+ val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2)
+ assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews)
+
+ assertThat(taskbarIconsCentered).isTrue()
+ assertThat(taskbarEndMargin)
+ .isAtLeast(
+ navButtonEndSpacing +
+ bubbleBarViewController.collapsedWidthWithMaxVisibleBubbles.toInt()
+ )
+ }
+
+ @Test
+ @TaskbarMode(TRANSIENT)
+ fun testStashingBubbleBarMaintainsMaxNumIcons_transient() {
+ runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+
+ val initialNumIcons = currentNumberOfTaskbarIcons
+ val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2)
+
+ runOnMainSync { bubbleStashController.stashBubbleBarImmediate() }
+ assertThat(maxNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+ assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+ assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2))
+ }
+
+ @Test
+ @TaskbarMode(PINNED)
+ fun testHidingBubbleBarIncreasesMaxNumIcons_pinned() {
+ runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+
+ val initialNumIcons = currentNumberOfTaskbarIcons
+ val initialMaxNumIconViews = addRunningAppsAndVerifyOverflowState(5)
+
+ runOnMainSync { bubbleBarViewController.setHiddenForBubbles(true) }
+
+ val maxNumIconViews = maxNumberOfTaskbarIcons
+ assertThat(maxNumIconViews).isGreaterThan(initialMaxNumIconViews)
+ assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+ assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2))
+
+ assertThat(taskbarIconsCentered).isTrue()
+ }
+
+ @Test
+ @TaskbarMode(TRANSIENT)
+ fun testHidingBubbleBarIncreasesMaxNumIcons_transient() {
+ runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+
+ val initialNumIcons = currentNumberOfTaskbarIcons
+ val initialMaxNumIconViews = addRunningAppsAndVerifyOverflowState(5)
+
+ runOnMainSync { bubbleBarViewController.setHiddenForBubbles(true) }
+
+ val maxNumIconViews = maxNumberOfTaskbarIcons
+ assertThat(maxNumIconViews).isGreaterThan(initialMaxNumIconViews)
+ assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+ assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2))
+
+ assertThat(taskbarIconsCentered).isTrue()
+ }
+
+ private fun createDesktopTask(tasksToAdd: Int) {
+ val tasks =
+ (0..<tasksToAdd).map {
+ Task(Task.TaskKey(it, 0, Intent(), ComponentName("", ""), 0, 2000))
+ }
+ recentsModel.updateRecentTasks(listOf(DesktopTask(tasks)))
+ desktopTaskListener?.onTasksVisibilityChanged(
+ context.virtualDisplay.display.displayId,
+ tasksToAdd,
+ )
+ runOnMainSync { recentsModel.resolvePendingTaskRequests() }
+ }
+
+ private val navButtonEndSpacing: Int
+ get() {
+ return taskbarUnitTestRule.activityContext.resources.getDimensionPixelSize(
+ taskbarUnitTestRule.activityContext.deviceProfile.inv.inlineNavButtonsEndSpacing
+ )
+ }
+
+ private val taskbarOverflowIconIndex: Int
+ get() {
+ return getOnUiThread {
+ taskbarViewController.iconViews.indexOfFirst { it is TaskbarOverflowView }
+ }
+ }
+
+ private val maxNumberOfTaskbarIcons: Int
+ get() = getOnUiThread { taskbarViewController.maxNumIconViews }
+
+ private val currentNumberOfTaskbarIcons: Int
+ get() = getOnUiThread { taskbarViewController.iconViews.size }
+
+ private val taskbarIconsCentered: Boolean
+ get() {
+ return getOnUiThread {
+ val iconLayoutBounds = taskbarViewController.iconLayoutBounds
+ val availableWidth = taskbarUnitTestRule.activityContext.deviceProfile.widthPx
+ iconLayoutBounds.left - (availableWidth - iconLayoutBounds.right) < 2
+ }
+ }
+
+ private val taskbarEndMargin: Int
+ get() {
+ return getOnUiThread {
+ taskbarUnitTestRule.activityContext.deviceProfile.widthPx -
+ taskbarViewController.iconLayoutBounds.right
+ }
+ }
+
+ /**
+ * Adds enough running apps for taskbar to enter overflow of `targetOverflowSize`, and verifies
+ * * max number of icons in the taskbar remains unchanged
+ * * number of icons in the taskbar is at most max number of icons
+ * * whether the taskbar overflow icon is shown, and its position in taskbar.
+ *
+ * Returns max number of icons.
+ */
+ private fun addRunningAppsAndVerifyOverflowState(targetOverflowSize: Int): Int {
+ val maxNumIconViews = maxNumberOfTaskbarIcons
+ assertThat(maxNumIconViews).isGreaterThan(0)
+ // Assume there are at least all apps and divider icon, as they would appear once running
+ // apps are added, even if not present initially.
+ val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2)
+ assertThat(initialIconCount).isLessThan(maxNumIconViews)
+
+ createDesktopTask(maxNumIconViews - initialIconCount + targetOverflowSize)
+
+ assertThat(maxNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+ assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+ assertThat(taskbarOverflowIconIndex)
+ .isEqualTo(if (targetOverflowSize > 0) initialIconCount else -1)
+ return maxNumIconViews
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt
new file mode 100644
index 0000000..ed1443d
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.taskbar.rules
+
+import com.android.quickstep.RecentsModel
+import com.android.quickstep.RecentsModel.RecentTasksChangedListener
+import com.android.quickstep.TaskIconCache
+import com.android.quickstep.util.GroupTask
+import java.util.function.Consumer
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+class MockedRecentsModelTestRule(private val context: TaskbarWindowSandboxContext) : TestRule {
+
+ private val mockIconCache: TaskIconCache = mock()
+
+ private val mockRecentsModel: RecentsModel = mock {
+ on { iconCache } doReturn mockIconCache
+
+ on { unregisterRecentTasksChangedListener() } doAnswer { recentTasksChangedListener = null }
+
+ on { registerRecentTasksChangedListener(any<RecentTasksChangedListener>()) } doAnswer
+ {
+ recentTasksChangedListener = it.getArgument<RecentTasksChangedListener>(0)
+ }
+
+ on { getTasks(anyOrNull(), anyOrNull()) } doAnswer
+ {
+ val request = it.getArgument<Consumer<List<GroupTask>>?>(0)
+ if (request != null) {
+ taskRequests.add { response -> request.accept(response) }
+ }
+ taskListId
+ }
+
+ on { getTasks(anyOrNull()) } doAnswer
+ {
+ val request = it.getArgument<Consumer<List<GroupTask>>?>(0)
+ if (request != null) {
+ taskRequests.add { response -> request.accept(response) }
+ }
+ taskListId
+ }
+
+ on { isTaskListValid(any()) } doAnswer { taskListId == it.getArgument(0) }
+ }
+
+ private var recentTasks: List<GroupTask> = emptyList()
+ private var taskListId = 0
+ private var recentTasksChangedListener: RecentTasksChangedListener? = null
+ private var taskRequests: MutableList<(List<GroupTask>) -> Unit> = mutableListOf()
+
+ override fun apply(base: Statement?, description: Description?): Statement {
+ return object : Statement() {
+ override fun evaluate() {
+ context.putObject(RecentsModel.INSTANCE, mockRecentsModel)
+ base?.evaluate()
+ }
+ }
+ }
+
+ // NOTE: For the update to take effect, `resolvePendingTaskRequests()` needs to be called, so
+ // calbacks to any pending `RecentsModel.getTasks()` get called with the updated task list.
+ fun updateRecentTasks(tasks: List<GroupTask>) {
+ ++taskListId
+ recentTasks = tasks
+ recentTasksChangedListener?.onRecentTasksChanged()
+ }
+
+ fun resolvePendingTaskRequests() {
+ val requests = mutableListOf<(List<GroupTask>) -> Unit>()
+ requests.addAll(taskRequests)
+ taskRequests.clear()
+
+ requests.forEach { it(recentTasks) }
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
index 6b95f8d..970bdec 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
@@ -17,6 +17,7 @@
package com.android.quickstep;
import static com.android.quickstep.AbsSwipeUpHandler.STATE_HANDLER_INVALIDATED;
+import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertTrue;
@@ -28,6 +29,7 @@
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -40,6 +42,9 @@
import android.graphics.Rect;
import android.os.Bundle;
import android.os.SystemClock;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.ViewTreeObserver;
@@ -58,6 +63,7 @@
import com.android.quickstep.util.ContextInitListener;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.RecentsViewContainer;
+import com.android.systemui.shared.Flags;
import com.android.systemui.shared.system.InputConsumerController;
import org.junit.Before;
@@ -103,14 +109,8 @@
/* startBounds= */ null,
/* taskInfo= */ mRunningTaskInfo,
/* allowEnterPip= */ false);
- protected final RecentsAnimationTargets mRecentsAnimationTargets = new RecentsAnimationTargets(
- new RemoteAnimationTarget[] {mRemoteAnimationTarget},
- new RemoteAnimationTarget[] {mRemoteAnimationTarget},
- new RemoteAnimationTarget[] {mRemoteAnimationTarget},
- /* homeContentInsets= */ new Rect(),
- /* minimizedHomeBounds= */ null,
- new Bundle());
+ protected RecentsAnimationTargets mRecentsAnimationTargets;
protected TaskAnimationManager mTaskAnimationManager;
protected RecentsAnimationDeviceState mRecentsAnimationDeviceState;
@@ -127,6 +127,22 @@
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ @Before
+ public void setUpAnimationTargets() {
+ Bundle extras = new Bundle();
+ extras.putBoolean(KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION, true);
+ mRecentsAnimationTargets = new RecentsAnimationTargets(
+ new RemoteAnimationTarget[] {mRemoteAnimationTarget},
+ new RemoteAnimationTarget[] {mRemoteAnimationTarget},
+ new RemoteAnimationTarget[] {mRemoteAnimationTarget},
+ /* homeContentInsets= */ new Rect(),
+ /* minimizedHomeBounds= */ null,
+ extras);
+ }
+
@Before
public void setUpRunningTaskInfo() {
mRunningTaskInfo.baseIntent = new Intent(Intent.ACTION_MAIN)
@@ -237,6 +253,30 @@
});
}
+ @EnableFlags({Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+ Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED})
+ @Test
+ public void testHomeGesture_handsOffAnimation() {
+ createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME);
+
+ runOnMainSync(() -> {
+ verify(mRecentsAnimationController).handOffAnimation(any(), any());
+ verifyRecentsAnimationFinishedAndCallCallback();
+ });
+ }
+
+ @DisableFlags({Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+ Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED})
+ @Test
+ public void testHomeGesture_doesNotHandOffAnimation_withFlagsDisabled() {
+ createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME);
+
+ runOnMainSync(() -> {
+ verify(mRecentsAnimationController, never()).handOffAnimation(any(), any());
+ verifyRecentsAnimationFinishedAndCallCallback();
+ });
+ }
+
@Test
public void testHomeGesture_invalidatesHandlerAfterParallelAnim() {
ValueAnimator parallelAnim = new ValueAnimator();
diff --git a/src/com/android/launcher3/model/DatabaseHelper.java b/src/com/android/launcher3/model/DatabaseHelper.java
index 8368256..ed4f492 100644
--- a/src/com/android/launcher3/model/DatabaseHelper.java
+++ b/src/com/android/launcher3/model/DatabaseHelper.java
@@ -56,6 +56,7 @@
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Locale;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.ToLongFunction;
import java.util.stream.Collectors;
@@ -79,8 +80,8 @@
private final Context mContext;
private final ToLongFunction<UserHandle> mUserSerialProvider;
private final Runnable mOnEmptyDbCreateCallback;
+ private final AtomicInteger mMaxItemId = new AtomicInteger(-1);
- private int mMaxItemId = -1;
public boolean mHotseatRestoreTableExists;
/**
@@ -97,21 +98,19 @@
protected void initIds() {
// In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
// the DB here
- if (mMaxItemId == -1) {
- mMaxItemId = initializeMaxItemId(getWritableDatabase());
- }
+ mMaxItemId.compareAndSet(-1, initializeMaxItemId(getWritableDatabase()));
}
@Override
public void onCreate(SQLiteDatabase db) {
if (LOGD) Log.d(TAG, "creating new launcher database");
- mMaxItemId = 1;
+ mMaxItemId.set(1);
addTableToDb(db, getDefaultUserSerial(), false /* optional */);
// Fresh and clean launcher DB.
- mMaxItemId = initializeMaxItemId(db);
+ mMaxItemId.set(initializeMaxItemId(db));
mOnEmptyDbCreateCallback.run();
}
@@ -451,11 +450,10 @@
// after that point
@Override
public int generateNewItemId() {
- if (mMaxItemId < 0) {
+ if (mMaxItemId.get() < 0) {
throw new RuntimeException("Error: max item id was not initialized");
}
- mMaxItemId += 1;
- return mMaxItemId;
+ return mMaxItemId.incrementAndGet();
}
/**
@@ -484,7 +482,7 @@
public void checkId(ContentValues values) {
int id = values.getAsInteger(Favorites._ID);
- mMaxItemId = Math.max(id, mMaxItemId);
+ mMaxItemId.accumulateAndGet(id, Math::max);
}
private int initializeMaxItemId(SQLiteDatabase db) {
@@ -508,7 +506,7 @@
int count = loader.loadLayout(db);
// Ensure that the max ids are initialized
- mMaxItemId = initializeMaxItemId(db);
+ mMaxItemId.set(initializeMaxItemId(db));
return count;
}