summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt111
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java455
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt367
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java7
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java10
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java48
8 files changed, 528 insertions, 474 deletions
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
index e422198c40c5..e73d8802f0b2 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
@@ -26,6 +26,7 @@ import android.view.WindowManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.internal.protolog.common.ProtoLog
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT
import com.android.wm.shell.common.bubbles.BubbleBarLocation
@@ -54,6 +55,7 @@ class BubblePositionerTest {
@Before
fun setUp() {
+ ProtoLog.REQUIRE_PROTOLOGTOOL = false
val windowManager = context.getSystemService(WindowManager::class.java)
positioner = BubblePositioner(context, windowManager)
}
@@ -167,8 +169,9 @@ class BubblePositionerTest {
@Test
fun testGetRestingPosition_afterBoundsChange() {
- positioner.update(defaultDeviceConfig.copy(isLargeScreen = true,
- windowBounds = Rect(0, 0, 2000, 1600)))
+ positioner.update(
+ defaultDeviceConfig.copy(isLargeScreen = true, windowBounds = Rect(0, 0, 2000, 1600))
+ )
// Set the resting position to the right side
var allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
@@ -176,8 +179,9 @@ class BubblePositionerTest {
positioner.restingPosition = restingPosition
// Now make the device smaller
- positioner.update(defaultDeviceConfig.copy(isLargeScreen = false,
- windowBounds = Rect(0, 0, 1000, 1600)))
+ positioner.update(
+ defaultDeviceConfig.copy(isLargeScreen = false, windowBounds = Rect(0, 0, 1000, 1600))
+ )
// Check the resting position is on the correct side
allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
@@ -236,7 +240,8 @@ class BubblePositionerTest {
0 /* taskId */,
null /* locus */,
true /* isDismissable */,
- directExecutor()) {}
+ directExecutor()
+ ) {}
// Ensure the height is the same as the desired value
assertThat(positioner.getExpandedViewHeight(bubble))
@@ -263,7 +268,8 @@ class BubblePositionerTest {
0 /* taskId */,
null /* locus */,
true /* isDismissable */,
- directExecutor()) {}
+ directExecutor()
+ ) {}
// Ensure the height is the same as the desired value
val minHeight =
@@ -471,20 +477,20 @@ class BubblePositionerTest {
fun testGetTaskViewContentWidth_onLeft() {
positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
val taskViewWidth = positioner.getTaskViewContentWidth(true /* onLeft */)
- val paddings = positioner.getExpandedViewContainerPadding(true /* onLeft */,
- false /* isOverflow */)
- assertThat(taskViewWidth).isEqualTo(
- positioner.screenRect.width() - paddings[0] - paddings[2])
+ val paddings =
+ positioner.getExpandedViewContainerPadding(true /* onLeft */, false /* isOverflow */)
+ assertThat(taskViewWidth)
+ .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2])
}
@Test
fun testGetTaskViewContentWidth_onRight() {
positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
val taskViewWidth = positioner.getTaskViewContentWidth(false /* onLeft */)
- val paddings = positioner.getExpandedViewContainerPadding(false /* onLeft */,
- false /* isOverflow */)
- assertThat(taskViewWidth).isEqualTo(
- positioner.screenRect.width() - paddings[0] - paddings[2])
+ val paddings =
+ positioner.getExpandedViewContainerPadding(false /* onLeft */, false /* isOverflow */)
+ assertThat(taskViewWidth)
+ .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2])
}
@Test
@@ -513,6 +519,66 @@ class BubblePositionerTest {
assertThat(positioner.isBubbleBarOnLeft).isFalse()
}
+ @Test
+ fun testGetBubbleBarExpandedViewBounds_onLeft() {
+ testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = false)
+ }
+
+ @Test
+ fun testGetBubbleBarExpandedViewBounds_onRight() {
+ testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = false)
+ }
+
+ @Test
+ fun testGetBubbleBarExpandedViewBounds_isOverflow_onLeft() {
+ testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = true)
+ }
+
+ @Test
+ fun testGetBubbleBarExpandedViewBounds_isOverflow_onRight() {
+ testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = true)
+ }
+
+ private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) {
+ positioner.setShowingInBubbleBar(true)
+ val deviceConfig =
+ defaultDeviceConfig.copy(
+ isLargeScreen = true,
+ isLandscape = true,
+ insets = Insets.of(10, 20, 5, 15),
+ windowBounds = Rect(0, 0, 2000, 2600)
+ )
+ positioner.update(deviceConfig)
+
+ positioner.bubbleBarBounds = getBubbleBarBounds(onLeft, deviceConfig)
+
+ val expandedViewPadding =
+ context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
+
+ val left: Int
+ val right: Int
+ if (onLeft) {
+ // Pin to the left, calculate right
+ left = deviceConfig.insets.left + expandedViewPadding
+ right = left + positioner.getExpandedViewWidthForBubbleBar(isOverflow)
+ } else {
+ // Pin to the right, calculate left
+ right =
+ deviceConfig.windowBounds.right - deviceConfig.insets.right - expandedViewPadding
+ left = right - positioner.getExpandedViewWidthForBubbleBar(isOverflow)
+ }
+ // Above the bubble bar
+ val bottom = positioner.bubbleBarBounds.top - expandedViewPadding
+ // Calculate right and top based on size
+ val top = bottom - positioner.getExpandedViewHeightForBubbleBar(isOverflow)
+ val expectedBounds = Rect(left, top, right, bottom)
+
+ val bounds = Rect()
+ positioner.getBubbleBarExpandedViewBounds(onLeft, isOverflow, bounds)
+
+ assertThat(bounds).isEqualTo(expectedBounds)
+ }
+
private val defaultYPosition: Float
/**
* Calculates the Y position bubbles should be placed based on the config. Based on the
@@ -544,4 +610,21 @@ class BubblePositionerTest {
positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent
}
+
+ private fun getBubbleBarBounds(onLeft: Boolean, deviceConfig: DeviceConfig): Rect {
+ val width = 200
+ val height = 100
+ val bottom = deviceConfig.windowBounds.bottom - deviceConfig.insets.bottom
+ val top = bottom - height
+ val left: Int
+ val right: Int
+ if (onLeft) {
+ left = deviceConfig.insets.left
+ right = left + width
+ } else {
+ right = deviceConfig.windowBounds.right - deviceConfig.insets.right
+ left = right - width
+ }
+ return Rect(left, top, right, bottom)
+ }
}
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 00fb298ea1cc..43ce1668c4df 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -535,5 +535,7 @@
<!-- The vertical margin that needs to be preserved between the scaled window bounds and the
original window bounds (once the surface is scaled enough to do so) -->
<dimen name="cross_task_back_vertical_margin">8dp</dimen>
+ <!-- The offset from the left edge of the entering page for the cross-activity animation -->
+ <dimen name="cross_activity_back_entering_start_offset">96dp</dimen>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
deleted file mode 100644
index d6f7c367f772..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
+++ /dev/null
@@ -1,455 +0,0 @@
-/*
- * Copyright (C) 2022 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.wm.shell.back;
-
-import static android.view.RemoteAnimationTarget.MODE_CLOSING;
-import static android.view.RemoteAnimationTarget.MODE_OPENING;
-import static android.window.BackEvent.EDGE_RIGHT;
-
-import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY;
-import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD;
-import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.annotation.NonNull;
-import android.content.Context;
-import android.graphics.Matrix;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.os.RemoteException;
-import android.util.FloatProperty;
-import android.util.TypedValue;
-import android.view.IRemoteAnimationFinishedCallback;
-import android.view.IRemoteAnimationRunner;
-import android.view.RemoteAnimationTarget;
-import android.view.SurfaceControl;
-import android.view.animation.Interpolator;
-import android.window.BackEvent;
-import android.window.BackMotionEvent;
-import android.window.BackProgressAnimator;
-import android.window.IOnBackInvokedCallback;
-
-import com.android.internal.dynamicanimation.animation.SpringAnimation;
-import com.android.internal.dynamicanimation.animation.SpringForce;
-import com.android.internal.policy.ScreenDecorationsUtils;
-import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.animation.Interpolators;
-import com.android.wm.shell.common.annotations.ShellMainThread;
-
-import javax.inject.Inject;
-
-/** Class that defines cross-activity animation. */
-@ShellMainThread
-public class CrossActivityBackAnimation extends ShellBackAnimation {
- /**
- * Minimum scale of the entering/closing window.
- */
- private static final float MIN_WINDOW_SCALE = 0.9f;
-
- /** Duration of post animation after gesture committed. */
- private static final int POST_ANIMATION_DURATION = 350;
- private static final Interpolator INTERPOLATOR = Interpolators.STANDARD_DECELERATE;
- private static final FloatProperty<CrossActivityBackAnimation> ENTER_PROGRESS_PROP =
- new FloatProperty<>("enter-alpha") {
- @Override
- public void setValue(CrossActivityBackAnimation anim, float value) {
- anim.setEnteringProgress(value);
- }
-
- @Override
- public Float get(CrossActivityBackAnimation object) {
- return object.getEnteringProgress();
- }
- };
- private static final FloatProperty<CrossActivityBackAnimation> LEAVE_PROGRESS_PROP =
- new FloatProperty<>("leave-alpha") {
- @Override
- public void setValue(CrossActivityBackAnimation anim, float value) {
- anim.setLeavingProgress(value);
- }
-
- @Override
- public Float get(CrossActivityBackAnimation object) {
- return object.getLeavingProgress();
- }
- };
- private static final float MIN_WINDOW_ALPHA = 0.01f;
- private static final float WINDOW_X_SHIFT_DP = 48;
- private static final int SCALE_FACTOR = 100;
- // TODO(b/264710590): Use the progress commit threshold from ViewConfiguration once it exists.
- private static final float TARGET_COMMIT_PROGRESS = 0.5f;
- private static final float ENTER_ALPHA_THRESHOLD = 0.22f;
-
- private final Rect mStartTaskRect = new Rect();
- private final float mCornerRadius;
-
- // The closing window properties.
- private final RectF mClosingRect = new RectF();
-
- // The entering window properties.
- private final Rect mEnteringStartRect = new Rect();
- private final RectF mEnteringRect = new RectF();
- private final SpringAnimation mEnteringProgressSpring;
- private final SpringAnimation mLeavingProgressSpring;
- // Max window x-shift in pixels.
- private final float mWindowXShift;
- private final BackAnimationRunner mBackAnimationRunner;
-
- private float mEnteringProgress = 0f;
- private float mLeavingProgress = 0f;
-
- private final PointF mInitialTouchPos = new PointF();
-
- private final Matrix mTransformMatrix = new Matrix();
-
- private final float[] mTmpFloat9 = new float[9];
-
- private RemoteAnimationTarget mEnteringTarget;
- private RemoteAnimationTarget mClosingTarget;
- private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
-
- private boolean mBackInProgress = false;
- private boolean mIsRightEdge;
- private boolean mTriggerBack = false;
-
- private PointF mTouchPos = new PointF();
- private IRemoteAnimationFinishedCallback mFinishCallback;
-
- private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
-
- private final BackAnimationBackground mBackground;
-
- @Inject
- public CrossActivityBackAnimation(Context context, BackAnimationBackground background) {
- mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
- mBackAnimationRunner = new BackAnimationRunner(
- new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY);
- mBackground = background;
- mEnteringProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP);
- mEnteringProgressSpring.setSpring(new SpringForce()
- .setStiffness(SpringForce.STIFFNESS_MEDIUM)
- .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
- mLeavingProgressSpring = new SpringAnimation(this, LEAVE_PROGRESS_PROP);
- mLeavingProgressSpring.setSpring(new SpringForce()
- .setStiffness(SpringForce.STIFFNESS_MEDIUM)
- .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
- mWindowXShift = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, WINDOW_X_SHIFT_DP,
- context.getResources().getDisplayMetrics());
- }
-
- /**
- * Returns 1 if x >= edge1, 0 if x <= edge0, and a smoothed value between the two.
- * From https://en.wikipedia.org/wiki/Smoothstep
- */
- private static float smoothstep(float edge0, float edge1, float x) {
- if (x < edge0) return 0;
- if (x >= edge1) return 1;
-
- x = (x - edge0) / (edge1 - edge0);
- return x * x * (3 - 2 * x);
- }
-
- /**
- * Linearly map x from range (a1, a2) to range (b1, b2).
- */
- private static float mapLinear(float x, float a1, float a2, float b1, float b2) {
- return b1 + (x - a1) * (b2 - b1) / (a2 - a1);
- }
-
- /**
- * Linearly map a normalized value from (0, 1) to (min, max).
- */
- private static float mapRange(float value, float min, float max) {
- return min + (value * (max - min));
- }
-
- private void startBackAnimation() {
- if (mEnteringTarget == null || mClosingTarget == null) {
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null.");
- return;
- }
- mTransaction.setAnimationTransaction();
-
- // Offset start rectangle to align task bounds.
- mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds());
- mStartTaskRect.offsetTo(0, 0);
-
- // Draw background with task background color.
- mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(),
- mEnteringTarget.taskInfo.taskDescription.getBackgroundColor(), mTransaction);
- setEnteringProgress(0);
- setLeavingProgress(0);
- }
-
- private void applyTransform(SurfaceControl leash, RectF targetRect, float targetAlpha) {
- if (leash == null || !leash.isValid()) {
- return;
- }
-
- final float scale = targetRect.width() / mStartTaskRect.width();
- mTransformMatrix.reset();
- mTransformMatrix.setScale(scale, scale);
- mTransformMatrix.postTranslate(targetRect.left, targetRect.top);
- mTransaction.setAlpha(leash, targetAlpha)
- .setMatrix(leash, mTransformMatrix, mTmpFloat9)
- .setWindowCrop(leash, mStartTaskRect)
- .setCornerRadius(leash, mCornerRadius);
- }
-
- private void finishAnimation() {
- if (mEnteringTarget != null) {
- if (mEnteringTarget.leash != null && mEnteringTarget.leash.isValid()) {
- mTransaction.setCornerRadius(mEnteringTarget.leash, 0);
- mEnteringTarget.leash.release();
- }
- mEnteringTarget = null;
- }
- if (mClosingTarget != null) {
- if (mClosingTarget.leash != null) {
- mClosingTarget.leash.release();
- }
- mClosingTarget = null;
- }
- if (mBackground != null) {
- mBackground.removeBackground(mTransaction);
- }
-
- mTransaction.apply();
- mBackInProgress = false;
- mTransformMatrix.reset();
- mInitialTouchPos.set(0, 0);
-
- if (mFinishCallback != null) {
- try {
- mFinishCallback.onAnimationFinished();
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- mFinishCallback = null;
- }
- mEnteringProgressSpring.animateToFinalPosition(0);
- mEnteringProgressSpring.skipToEnd();
- mLeavingProgressSpring.animateToFinalPosition(0);
- mLeavingProgressSpring.skipToEnd();
- }
-
- private void onGestureProgress(@NonNull BackEvent backEvent) {
- if (!mBackInProgress) {
- mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT;
- mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
- mBackInProgress = true;
- }
- mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
-
- float progress = backEvent.getProgress();
- float springProgress = (mTriggerBack
- ? mapLinear(progress, 0f, 1, TARGET_COMMIT_PROGRESS, 1)
- : mapLinear(progress, 0, 1f, 0, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR;
- mLeavingProgressSpring.animateToFinalPosition(springProgress);
- mEnteringProgressSpring.animateToFinalPosition(springProgress);
- mBackground.onBackProgressed(progress);
- }
-
- private void onGestureCommitted() {
- if (mEnteringTarget == null || mClosingTarget == null || mClosingTarget.leash == null
- || mEnteringTarget.leash == null || !mEnteringTarget.leash.isValid()
- || !mClosingTarget.leash.isValid()) {
- finishAnimation();
- return;
- }
- // End the fade animations
- mLeavingProgressSpring.cancel();
- mEnteringProgressSpring.cancel();
-
- // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
- // coordinate of the gesture driven phase.
- mEnteringRect.round(mEnteringStartRect);
- mTransaction.hide(mClosingTarget.leash);
-
- ValueAnimator valueAnimator =
- ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION);
- valueAnimator.setInterpolator(INTERPOLATOR);
- valueAnimator.addUpdateListener(animation -> {
- float progress = animation.getAnimatedFraction();
- updatePostCommitEnteringAnimation(progress);
- if (progress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD) {
- mBackground.resetStatusBarCustomization();
- }
- mTransaction.apply();
- });
-
- valueAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mBackground.resetStatusBarCustomization();
- finishAnimation();
- }
- });
- valueAnimator.start();
- }
-
- private void updatePostCommitEnteringAnimation(float progress) {
- float left = mapRange(progress, mEnteringStartRect.left, mStartTaskRect.left);
- float top = mapRange(progress, mEnteringStartRect.top, mStartTaskRect.top);
- float width = mapRange(progress, mEnteringStartRect.width(), mStartTaskRect.width());
- float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height());
- float alpha = mapRange(progress, getPreCommitEnteringAlpha(), 1.0f);
- mEnteringRect.set(left, top, left + width, top + height);
- applyTransform(mEnteringTarget.leash, mEnteringRect, alpha);
- }
-
- private float getPreCommitEnteringAlpha() {
- return Math.max(smoothstep(ENTER_ALPHA_THRESHOLD, 0.7f, mEnteringProgress),
- MIN_WINDOW_ALPHA);
- }
-
- private float getEnteringProgress() {
- return mEnteringProgress * SCALE_FACTOR;
- }
-
- private void setEnteringProgress(float value) {
- mEnteringProgress = value / SCALE_FACTOR;
- if (mEnteringTarget != null && mEnteringTarget.leash != null) {
- transformWithProgress(
- mEnteringProgress,
- getPreCommitEnteringAlpha(),
- mEnteringTarget.leash,
- mEnteringRect,
- -mWindowXShift,
- 0
- );
- }
- }
-
- private float getPreCommitLeavingAlpha() {
- return Math.max(1 - smoothstep(0, ENTER_ALPHA_THRESHOLD, mLeavingProgress),
- MIN_WINDOW_ALPHA);
- }
-
- private float getLeavingProgress() {
- return mLeavingProgress * SCALE_FACTOR;
- }
-
- private void setLeavingProgress(float value) {
- mLeavingProgress = value / SCALE_FACTOR;
- if (mClosingTarget != null && mClosingTarget.leash != null) {
- transformWithProgress(
- mLeavingProgress,
- getPreCommitLeavingAlpha(),
- mClosingTarget.leash,
- mClosingRect,
- 0,
- mIsRightEdge ? 0 : mWindowXShift
- );
- }
- }
-
- private void transformWithProgress(float progress, float alpha, SurfaceControl surface,
- RectF targetRect, float deltaXMin, float deltaXMax) {
-
- final int width = mStartTaskRect.width();
- final int height = mStartTaskRect.height();
-
- final float interpolatedProgress = INTERPOLATOR.getInterpolation(progress);
- final float closingScale = MIN_WINDOW_SCALE
- + (1 - interpolatedProgress) * (1 - MIN_WINDOW_SCALE);
- final float closingWidth = closingScale * width;
- final float closingHeight = (float) height / width * closingWidth;
-
- // Move the window along the X axis.
- float closingLeft = mStartTaskRect.left + (width - closingWidth) / 2;
- closingLeft += mapRange(interpolatedProgress, deltaXMin, deltaXMax);
-
- // Move the window along the Y axis.
- final float closingTop = (height - closingHeight) * 0.5f;
- targetRect.set(
- closingLeft, closingTop, closingLeft + closingWidth, closingTop + closingHeight);
-
- applyTransform(surface, targetRect, Math.max(alpha, MIN_WINDOW_ALPHA));
- mTransaction.apply();
- }
-
- @Override
- public BackAnimationRunner getRunner() {
- return mBackAnimationRunner;
- }
-
- private final class Callback extends IOnBackInvokedCallback.Default {
- @Override
- public void onBackStarted(BackMotionEvent backEvent) {
- mTriggerBack = backEvent.getTriggerBack();
- mProgressAnimator.onBackStarted(backEvent,
- CrossActivityBackAnimation.this::onGestureProgress);
- }
-
- @Override
- public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
- mTriggerBack = backEvent.getTriggerBack();
- mProgressAnimator.onBackProgressed(backEvent);
- }
-
- @Override
- public void onBackCancelled() {
- mProgressAnimator.onBackCancelled(() -> {
- // mProgressAnimator can reach finish stage earlier than mLeavingProgressSpring,
- // and if we release all animation leash first, the leavingProgressSpring won't
- // able to update the animation anymore, which cause flicker.
- // Here should force update the closing animation target to the final stage before
- // release it.
- setLeavingProgress(0);
- finishAnimation();
- });
- }
-
- @Override
- public void onBackInvoked() {
- mProgressAnimator.reset();
- onGestureCommitted();
- }
- }
-
- private final class Runner extends IRemoteAnimationRunner.Default {
- @Override
- public void onAnimationStart(
- int transit,
- RemoteAnimationTarget[] apps,
- RemoteAnimationTarget[] wallpapers,
- RemoteAnimationTarget[] nonApps,
- IRemoteAnimationFinishedCallback finishedCallback) {
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to activity animation.");
- for (RemoteAnimationTarget a : apps) {
- if (a.mode == MODE_CLOSING) {
- mClosingTarget = a;
- }
- if (a.mode == MODE_OPENING) {
- mEnteringTarget = a;
- }
- }
-
- startBackAnimation();
- mFinishCallback = finishedCallback;
- }
-
- @Override
- public void onAnimationCancelled() {
- finishAnimation();
- }
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
new file mode 100644
index 000000000000..edf29dd484fc
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2022 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.wm.shell.back
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Matrix
+import android.graphics.PointF
+import android.graphics.Rect
+import android.graphics.RectF
+import android.os.RemoteException
+import android.view.Display
+import android.view.IRemoteAnimationFinishedCallback
+import android.view.IRemoteAnimationRunner
+import android.view.RemoteAnimationTarget
+import android.view.SurfaceControl
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.Interpolator
+import android.window.BackEvent
+import android.window.BackMotionEvent
+import android.window.BackProgressAnimator
+import android.window.IOnBackInvokedCallback
+import com.android.internal.jank.Cuj
+import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.internal.protolog.common.ProtoLog
+import com.android.wm.shell.R
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.animation.Interpolators
+import com.android.wm.shell.common.annotations.ShellMainThread
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+import javax.inject.Inject
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
+
+/** Class that defines cross-activity animation. */
+@ShellMainThread
+class CrossActivityBackAnimation @Inject constructor(
+ private val context: Context,
+ private val background: BackAnimationBackground,
+ private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+) : ShellBackAnimation() {
+
+ private val startClosingRect = RectF()
+ private val targetClosingRect = RectF()
+ private val currentClosingRect = RectF()
+
+ private val startEnteringRect = RectF()
+ private val targetEnteringRect = RectF()
+ private val currentEnteringRect = RectF()
+
+ private val taskBoundsRect = Rect()
+
+ private val cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
+
+ private val backAnimationRunner = BackAnimationRunner(
+ Callback(), Runner(), context, Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY
+ )
+ private val initialTouchPos = PointF()
+ private val transformMatrix = Matrix()
+ private val tmpFloat9 = FloatArray(9)
+ private var enteringTarget: RemoteAnimationTarget? = null
+ private var closingTarget: RemoteAnimationTarget? = null
+ private val transaction = SurfaceControl.Transaction()
+ private var triggerBack = false
+ private var finishCallback: IRemoteAnimationFinishedCallback? = null
+ private val progressAnimator = BackProgressAnimator()
+ private val displayBoundsMargin =
+ context.resources.getDimension(R.dimen.cross_task_back_vertical_margin)
+ private val enteringStartOffset =
+ context.resources.getDimension(R.dimen.cross_activity_back_entering_start_offset)
+
+ private val gestureInterpolator = Interpolators.STANDARD_DECELERATE
+ private val postCommitInterpolator = Interpolators.FAST_OUT_SLOW_IN
+ private val verticalMoveInterpolator: Interpolator = DecelerateInterpolator()
+
+ private var scrimLayer: SurfaceControl? = null
+ private var maxScrimAlpha: Float = 0f
+
+ override fun getRunner() = backAnimationRunner
+
+ private fun startBackAnimation(backMotionEvent: BackMotionEvent) {
+ if (enteringTarget == null || closingTarget == null) {
+ ProtoLog.d(
+ ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
+ "Entering target or closing target is null."
+ )
+ return
+ }
+ triggerBack = backMotionEvent.triggerBack
+ initialTouchPos.set(backMotionEvent.touchX, backMotionEvent.touchY)
+
+ transaction.setAnimationTransaction()
+
+ // Offset start rectangle to align task bounds.
+ taskBoundsRect.set(closingTarget!!.windowConfiguration.bounds)
+ taskBoundsRect.offsetTo(0, 0)
+
+ startClosingRect.set(taskBoundsRect)
+
+ // scale closing target into the middle for rhs and to the right for lhs
+ targetClosingRect.set(startClosingRect)
+ targetClosingRect.scaleCentered(MAX_SCALE)
+ if (backMotionEvent.swipeEdge != BackEvent.EDGE_RIGHT) {
+ targetClosingRect.offset(
+ startClosingRect.right - targetClosingRect.right - displayBoundsMargin, 0f
+ )
+ }
+
+ // the entering target starts 96dp to the left of the screen edge...
+ startEnteringRect.set(startClosingRect)
+ startEnteringRect.offset(-enteringStartOffset, 0f)
+
+ // ...and gets scaled in sync with the closing target
+ targetEnteringRect.set(startEnteringRect)
+ targetEnteringRect.scaleCentered(MAX_SCALE)
+
+ // Draw background with task background color.
+ background.ensureBackground(
+ closingTarget!!.windowConfiguration.bounds,
+ enteringTarget!!.taskInfo.taskDescription!!.backgroundColor, transaction
+ )
+ ensureScrimLayer()
+ transaction.apply()
+ }
+
+ private fun onGestureProgress(backEvent: BackEvent) {
+ val progress = gestureInterpolator.getInterpolation(backEvent.progress)
+ background.onBackProgressed(progress)
+ currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
+ val yOffset = getYOffset(currentClosingRect, backEvent.touchY)
+ currentClosingRect.offset(0f, yOffset)
+ applyTransform(closingTarget?.leash, currentClosingRect, 1f)
+ currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress)
+ currentEnteringRect.offset(0f, yOffset)
+ applyTransform(enteringTarget?.leash, currentEnteringRect, 1f)
+ transaction.apply()
+ }
+
+ private fun getYOffset(centeredRect: RectF, touchY: Float): Float {
+ val screenHeight = taskBoundsRect.height()
+ // Base the window movement in the Y axis on the touch movement in the Y axis.
+ val rawYDelta = touchY - initialTouchPos.y
+ val yDirection = (if (rawYDelta < 0) -1 else 1)
+ // limit yDelta interpretation to 1/2 of screen height in either direction
+ val deltaYRatio = min(screenHeight / 2f, abs(rawYDelta)) / (screenHeight / 2f)
+ val interpolatedYRatio: Float = verticalMoveInterpolator.getInterpolation(deltaYRatio)
+ // limit y-shift so surface never passes 8dp screen margin
+ val deltaY = yDirection * interpolatedYRatio * max(
+ 0f, (screenHeight - centeredRect.height()) / 2f - displayBoundsMargin
+ )
+ return deltaY
+ }
+
+ private fun onGestureCommitted() {
+ if (closingTarget?.leash == null || enteringTarget?.leash == null ||
+ !enteringTarget!!.leash.isValid || !closingTarget!!.leash.isValid
+ ) {
+ finishAnimation()
+ return
+ }
+
+ // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
+ // coordinate of the gesture driven phase. Let's update the start and target rects and kick
+ // off the animator
+ startClosingRect.set(currentClosingRect)
+ startEnteringRect.set(currentEnteringRect)
+ targetEnteringRect.set(taskBoundsRect)
+ targetClosingRect.set(taskBoundsRect)
+ targetClosingRect.offset(currentClosingRect.left + enteringStartOffset, 0f)
+
+ val valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION)
+ valueAnimator.addUpdateListener { animation: ValueAnimator ->
+ val progress = animation.animatedFraction
+ onPostCommitProgress(progress)
+ if (progress > 1 - BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD) {
+ background.resetStatusBarCustomization()
+ }
+ }
+ valueAnimator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ background.resetStatusBarCustomization()
+ finishAnimation()
+ }
+ })
+ valueAnimator.start()
+ }
+
+ private fun onPostCommitProgress(linearProgress: Float) {
+ val closingAlpha = max(1f - linearProgress * 2, 0f)
+ val progress = postCommitInterpolator.getInterpolation(linearProgress)
+ scrimLayer?.let { transaction.setAlpha(it, maxScrimAlpha * (1f - linearProgress)) }
+ currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
+ applyTransform(closingTarget?.leash, currentClosingRect, closingAlpha)
+ currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress)
+ applyTransform(enteringTarget?.leash, currentEnteringRect, 1f)
+ transaction.apply()
+ }
+
+ private fun finishAnimation() {
+ enteringTarget?.let {
+ if (it.leash != null && it.leash.isValid) {
+ transaction.setCornerRadius(it.leash, 0f)
+ it.leash.release()
+ }
+ enteringTarget = null
+ }
+
+ closingTarget?.leash?.release()
+ closingTarget = null
+
+ background.removeBackground(transaction)
+ transaction.apply()
+ transformMatrix.reset()
+ initialTouchPos.set(0f, 0f)
+ try {
+ finishCallback?.onAnimationFinished()
+ } catch (e: RemoteException) {
+ e.printStackTrace()
+ }
+ finishCallback = null
+ removeScrimLayer()
+ }
+
+ private fun applyTransform(leash: SurfaceControl?, rect: RectF, alpha: Float) {
+ if (leash == null || !leash.isValid) return
+ val scale = rect.width() / taskBoundsRect.width()
+ transformMatrix.reset()
+ transformMatrix.setScale(scale, scale)
+ transformMatrix.postTranslate(rect.left, rect.top)
+ transaction.setAlpha(leash, alpha)
+ .setMatrix(leash, transformMatrix, tmpFloat9)
+ .setCrop(leash, taskBoundsRect)
+ .setCornerRadius(leash, cornerRadius)
+ }
+
+ private fun ensureScrimLayer() {
+ if (scrimLayer != null) return
+ val isDarkTheme: Boolean = isDarkMode(context)
+ val scrimBuilder = SurfaceControl.Builder()
+ .setName("Cross-Activity back animation scrim")
+ .setCallsite("CrossActivityBackAnimation")
+ .setColorLayer()
+ .setOpaque(false)
+ .setHidden(false)
+
+ rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, scrimBuilder)
+ scrimLayer = scrimBuilder.build()
+ val colorComponents = floatArrayOf(0f, 0f, 0f)
+ maxScrimAlpha = if (isDarkTheme) MAX_SCRIM_ALPHA_DARK else MAX_SCRIM_ALPHA_LIGHT
+ transaction
+ .setColor(scrimLayer, colorComponents)
+ .setAlpha(scrimLayer!!, maxScrimAlpha)
+ .setRelativeLayer(scrimLayer!!, closingTarget!!.leash, -1)
+ .show(scrimLayer)
+ }
+
+ private fun removeScrimLayer() {
+ scrimLayer?.let {
+ if (it.isValid) {
+ transaction.remove(it).apply()
+ }
+ }
+ scrimLayer = null
+ }
+
+
+ private inner class Callback : IOnBackInvokedCallback.Default() {
+ override fun onBackStarted(backMotionEvent: BackMotionEvent) {
+ startBackAnimation(backMotionEvent)
+ progressAnimator.onBackStarted(backMotionEvent) { backEvent: BackEvent ->
+ onGestureProgress(backEvent)
+ }
+ }
+
+ override fun onBackProgressed(backEvent: BackMotionEvent) {
+ triggerBack = backEvent.triggerBack
+ progressAnimator.onBackProgressed(backEvent)
+ }
+
+ override fun onBackCancelled() {
+ progressAnimator.onBackCancelled {
+ finishAnimation()
+ }
+ }
+
+ override fun onBackInvoked() {
+ progressAnimator.reset()
+ onGestureCommitted()
+ }
+ }
+
+ private inner class Runner : IRemoteAnimationRunner.Default() {
+ override fun onAnimationStart(
+ transit: Int,
+ apps: Array<RemoteAnimationTarget>,
+ wallpapers: Array<RemoteAnimationTarget>?,
+ nonApps: Array<RemoteAnimationTarget>?,
+ finishedCallback: IRemoteAnimationFinishedCallback
+ ) {
+ ProtoLog.d(
+ ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, "Start back to activity animation."
+ )
+ for (a in apps) {
+ when (a.mode) {
+ RemoteAnimationTarget.MODE_CLOSING -> closingTarget = a
+ RemoteAnimationTarget.MODE_OPENING -> enteringTarget = a
+ }
+ }
+ finishCallback = finishedCallback
+ }
+
+ override fun onAnimationCancelled() {
+ finishAnimation()
+ }
+ }
+
+ companion object {
+ /** Max scale of the entering/closing window.*/
+ private const val MAX_SCALE = 0.9f
+
+ /** Duration of post animation after gesture committed. */
+ private const val POST_ANIMATION_DURATION = 300L
+
+ private const val MAX_SCRIM_ALPHA_DARK = 0.8f
+ private const val MAX_SCRIM_ALPHA_LIGHT = 0.2f
+ }
+}
+
+private fun isDarkMode(context: Context): Boolean {
+ return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
+ Configuration.UI_MODE_NIGHT_YES
+}
+
+private fun RectF.setInterpolatedRectF(start: RectF, target: RectF, progress: Float) {
+ require(!(progress < 0 || progress > 1)) { "Progress value must be between 0 and 1" }
+ left = start.left + (target.left - start.left) * progress
+ top = start.top + (target.top - start.top) * progress
+ right = start.right + (target.right - start.right) * progress
+ bottom = start.bottom + (target.bottom - start.bottom) * progress
+}
+
+private fun RectF.scaleCentered(
+ scale: Float,
+ pivotX: Float = left + width() / 2,
+ pivotY: Float = top + height() / 2
+) {
+ offset(-pivotX, -pivotY) // move pivot to origin
+ scale(scale)
+ offset(pivotX, pivotY) // Move back to the original position
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index f4a401c64a31..4d5e516f76e5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -870,7 +870,7 @@ public class BubblePositioner {
if (onLeft) {
left = getInsets().left + padding;
} else {
- left = getAvailableRect().width() - width - padding;
+ left = getAvailableRect().right - width - padding;
}
int top = getExpandedViewBottomForBubbleBar() - height;
out.offsetTo(left, top);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 1c54754e9953..370720746808 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -332,6 +332,8 @@ public class RecentTasksController implements TaskStackListenerCallback,
ArrayList<ActivityManager.RecentTaskInfo> freeformTasks = new ArrayList<>();
+ int mostRecentFreeformTaskIndex = Integer.MAX_VALUE;
+
// Pull out the pairs as we iterate back in the list
ArrayList<GroupedRecentTaskInfo> recentTasks = new ArrayList<>();
for (int i = 0; i < rawList.size(); i++) {
@@ -344,6 +346,9 @@ public class RecentTasksController implements TaskStackListenerCallback,
if (DesktopModeStatus.isEnabled() && mDesktopModeTaskRepository.isPresent()
&& mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) {
// Freeform tasks will be added as a separate entry
+ if (mostRecentFreeformTaskIndex == Integer.MAX_VALUE) {
+ mostRecentFreeformTaskIndex = recentTasks.size();
+ }
freeformTasks.add(taskInfo);
continue;
}
@@ -362,7 +367,7 @@ public class RecentTasksController implements TaskStackListenerCallback,
// Add a special entry for freeform tasks
if (!freeformTasks.isEmpty()) {
- recentTasks.add(0, GroupedRecentTaskInfo.forFreeformTasks(
+ recentTasks.add(mostRecentFreeformTaskIndex, GroupedRecentTaskInfo.forFreeformTasks(
freeformTasks.toArray(new ActivityManager.RecentTaskInfo[0])));
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 9ded6ea1d187..703eb199f260 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -61,6 +61,7 @@ import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -113,6 +114,8 @@ public class BackAnimationControllerTest extends ShellTestCase {
private InputManager mInputManager;
@Mock
private ShellCommandHandler mShellCommandHandler;
+ @Mock
+ private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
private BackAnimationController mController;
private TestableContentResolver mContentResolver;
@@ -133,7 +136,8 @@ public class BackAnimationControllerTest extends ShellTestCase {
mShellInit = spy(new ShellInit(mShellExecutor));
mShellBackAnimationRegistry =
new ShellBackAnimationRegistry(
- new CrossActivityBackAnimation(mContext, mAnimationBackground),
+ new CrossActivityBackAnimation(
+ mContext, mAnimationBackground, mRootTaskDisplayAreaOrganizer),
new CrossTaskBackAnimation(mContext, mAnimationBackground),
/* dialogCloseAnimation= */ null,
new CustomizeActivityAnimation(mContext, mAnimationBackground),
@@ -528,8 +532,8 @@ public class BackAnimationControllerTest extends ShellTestCase {
@Test
public void testBackToActivity() throws RemoteException {
- final CrossActivityBackAnimation animation = new CrossActivityBackAnimation(mContext,
- mAnimationBackground);
+ final CrossActivityBackAnimation animation = new CrossActivityBackAnimation(
+ mContext, mAnimationBackground, mRootTaskDisplayAreaOrganizer);
verifySystemBackBehavior(BackNavigationInfo.TYPE_CROSS_ACTIVITY, animation.getRunner());
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 41a4e8d503c9..d38e97f378c9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -302,6 +302,54 @@ public class RecentTasksControllerTest extends ShellTestCase {
}
@Test
+ public void testGetRecentTasks_hasActiveDesktopTasks_proto2Enabled_freeformTaskOrder() {
+ StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
+ DesktopModeStatus.class).startMocking();
+ when(DesktopModeStatus.isEnabled()).thenReturn(true);
+
+ ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
+ ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
+ ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
+ ActivityManager.RecentTaskInfo t4 = makeTaskInfo(4);
+ ActivityManager.RecentTaskInfo t5 = makeTaskInfo(5);
+ setRawList(t1, t2, t3, t4, t5);
+
+ SplitBounds pair1Bounds =
+ new SplitBounds(new Rect(), new Rect(), 1, 2, SNAP_TO_50_50);
+ mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, pair1Bounds);
+
+ when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true);
+ when(mDesktopModeTaskRepository.isActiveTask(5)).thenReturn(true);
+
+ ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks(
+ MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0);
+
+ // 2 split screen tasks grouped, 2 freeform tasks grouped, 3 total recents entries
+ assertEquals(3, recentTasks.size());
+ GroupedRecentTaskInfo splitGroup = recentTasks.get(0);
+ GroupedRecentTaskInfo freeformGroup = recentTasks.get(1);
+ GroupedRecentTaskInfo singleGroup = recentTasks.get(2);
+
+ // Check that groups have expected types
+ assertEquals(GroupedRecentTaskInfo.TYPE_SPLIT, splitGroup.getType());
+ assertEquals(GroupedRecentTaskInfo.TYPE_FREEFORM, freeformGroup.getType());
+ assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, singleGroup.getType());
+
+ // Check freeform group entries
+ assertEquals(t3, freeformGroup.getTaskInfoList().get(0));
+ assertEquals(t5, freeformGroup.getTaskInfoList().get(1));
+
+ // Check split group entries
+ assertEquals(t1, splitGroup.getTaskInfoList().get(0));
+ assertEquals(t2, splitGroup.getTaskInfoList().get(1));
+
+ // Check single entry
+ assertEquals(t4, singleGroup.getTaskInfo1());
+
+ mockitoSession.finishMocking();
+ }
+
+ @Test
public void testGetRecentTasks_hasActiveDesktopTasks_proto2Disabled_doNotGroupFreeformTasks() {
StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
DesktopModeStatus.class).startMocking();