Loading libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java +82 −15 Original line number Diff line number Diff line Loading @@ -20,9 +20,11 @@ import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_A import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.PendingIntent.FLAG_IMMUTABLE; import static android.app.PendingIntent.FLAG_ONE_SHOT; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.view.View.INVISIBLE; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; Loading Loading @@ -132,7 +134,8 @@ public class BubbleTransitions { mAppInfoProvider = appInfoProvider; } void setBubbleController(BubbleController controller) { @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public void setBubbleController(BubbleController controller) { mBubbleController = controller; } Loading @@ -143,20 +146,6 @@ public class BubbleTransitions { return mBubbleController.shouldBeAppBubble(taskInfo); } /** * Returns whether bubbles are showing as the bubble bar. */ public boolean isShowingAsBubbleBar() { return mBubbleController.isShowingAsBubbleBar(); } /** * Returns whether there is an existing bubble with the given task id. */ public boolean hasBubbleWithTaskId(int taskId) { return mBubbleData.getBubbleInStackWithTaskId(taskId) != null; } /** * Returns whether there is a pending transition for the given request. */ Loading Loading @@ -306,6 +295,84 @@ public class BubbleTransitions { return new DraggedBubbleIconToFullscreen(bubble, dropLocation); } /** * Finds the Task that is entering Bubble. This can be either a Bubble Task that is becoming * visible, or a visible Task that is changing to Bubble from other windowing mode. */ @Nullable public TransitionInfo.Change getEnterBubbleTask(@NonNull TransitionInfo info) { for (int i = 0; i < info.getChanges().size(); i++) { final TransitionInfo.Change chg = info.getChanges().get(i); final ActivityManager.RunningTaskInfo taskInfo = chg.getTaskInfo(); // Exclude activity transition scenarios. if (taskInfo == null || taskInfo.getActivityType() != ACTIVITY_TYPE_STANDARD) { continue; } // Only process opening or change transitions. if (!isOpeningMode(chg.getMode()) && chg.getMode() != TRANSIT_CHANGE) { continue; } // Skip non-app-bubble tasks (e.g. a reused task in a bubble-to-fullscreen scenario). if (!shouldBeAppBubble(taskInfo)) { continue; } return chg; } return null; } /** * Finds the Bubble Task that is closing. * Note: this doesn't find move-to-back Task. */ @Nullable public TransitionInfo.Change getClosingBubbleTask(@NonNull TransitionInfo info) { for (int i = 0; i < info.getChanges().size(); i++) { final TransitionInfo.Change chg = info.getChanges().get(i); final ActivityManager.RunningTaskInfo taskInfo = chg.getTaskInfo(); // Exclude activity transition scenarios. if (taskInfo == null || taskInfo.getActivityType() != ACTIVITY_TYPE_STANDARD) { continue; } // Only process closing transitions. if (chg.getMode() != TRANSIT_CLOSE) { continue; } // Skip non-app-bubble tasks (e.g., a reused task in a bubble-to-fullscreen scenario). if (!shouldBeAppBubble(taskInfo)) { continue; } return chg; } return null; } /** * Whether the transition contains any Task that is changed from expanded App Bubbled to * non-Bubbled. */ public boolean containsNonBubbledExpandedTaskInStack(@NonNull TransitionInfo info) { if (!mBubbleData.isExpanded() || mBubbleData.getSelectedBubble() == null) { // No expanded. return false; } if (!(mBubbleData.getSelectedBubble() instanceof Bubble bubble) || !bubble.isApp()) { // Not app Bubble. return false; } final int expandedTaskId = bubble.getTaskId(); for (int i = 0; i < info.getChanges().size(); i++) { final TransitionInfo.Change chg = info.getChanges().get(i); final ActivityManager.RunningTaskInfo taskInfo = chg.getTaskInfo(); if (taskInfo == null || taskInfo.taskId != expandedTaskId) { continue; } // Check whether it is still an app bubble. return !shouldBeAppBubble(taskInfo); } return false; } /** * Plucks the task-surface out of an ancestor view while making the view invisible. This helper * attempts to do this seamlessly (ie. view becomes invisible in sync with task reparent). Loading libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +1 −2 Original line number Diff line number Diff line Loading @@ -863,8 +863,7 @@ public class DefaultMixedHandler implements MixedTransitionHandler, if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) { return null; } final TransitionInfo.Change change = DefaultMixedTransition.getChangeForBubblingTask(info, mBubbleTransitions); final TransitionInfo.Change change = mBubbleTransitions.getEnterBubbleTask(info); if (!com.android.wm.shell.Flags.fixTaskViewRotationAnimation()) { return change; } Loading libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java +94 −47 Original line number Diff line number Diff line Loading @@ -16,11 +16,10 @@ package com.android.wm.shell.transition; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static com.android.wm.shell.shared.TransitionUtil.isOpeningType; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_REQUEST; import static com.android.wm.shell.transition.DefaultMixedHandler.subCopy; import static com.android.wm.shell.transition.MixedTransitionHelper.animateEnterPipFromSplit; Loading @@ -28,7 +27,6 @@ import static com.android.wm.shell.transition.MixedTransitionHelper.animateKeygu import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.os.IBinder; import android.view.SurfaceControl; import android.window.TransitionInfo; Loading @@ -41,7 +39,6 @@ import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.phone.transition.PipTransitionUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.shared.pip.PipFlags; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreenController; Loading @@ -49,6 +46,7 @@ import com.android.wm.shell.splitscreen.StageCoordinator; import com.android.wm.shell.unfold.UnfoldTransitionHandler; import java.util.List; import java.util.function.Consumer; class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { private final UnfoldTransitionHandler mUnfoldHandler; Loading Loading @@ -373,7 +371,7 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " + "entering Bubbles while Split-Screen is foreground by %s", handler); TransitionInfo.Change bubblingTask = getChangeForBubblingTask(info, bubbleTransitions); final TransitionInfo.Change bubblingTask = bubbleTransitions.getEnterBubbleTask(info); // find previous split location for other task @SplitScreen.StageType int topSplitStageToKeep = SplitScreen.STAGE_TYPE_UNDEFINED; for (int i = info.getChanges().size() - 1; i >= 0; i--) { Loading Loading @@ -414,6 +412,62 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { return true; } /** * This is called when a task is being launched from a bubble, or when a task is launching to an * existing bubble. It may be one of the following cases, and each should be animated * differently: * - Case 1: a Task was in an expanded Bubble, and a new Activity was launched on top of it * from the task itself, or from a non-activity window, such as notification. * - Pattern: * - There is no open type Task in TransitionInfo (because it's an Activity transition). * - A Task is Bubbled and expanded before and after the transition. * - Expected Behavior: * - Skip here. * - Play Activity launch animation. * - Case 2: a Task was in an expanded Bubble, and a new Activity was launched on top of it * from a source activity of different windowing mode, such as Launcher. * - Pattern: * - There is a change type Task in TransitionInfo, which is no longer Bubbled. * - Expected Behavior: * - Skip here. * - The Task should be dismissed from Bubble, and get opened in new windowing mode. * Note: this shouldn't happen from normal user flow, and it now skipped here, but if it * happens, there may not be a good animation. * - Case 3: a Task was in an unfocused Bubble, a new Activity was launched to it from the * focused expanded Bubble, or from a non-activity window, such as notification. * - Pattern: * - There is a move-to-front type Task in TransitionInfo, which is Bubbled. * - That Task is Bubbled before and after the transition. * - (Optional) There is a move-to-back type Task in TransitionInfo, which is Bubbled. * - Expected Behavior: * - Play expand Bubble animation. * - (Optional) Hide the previous expanded Bubble. * - Case 4: a Task was in an unfocused Bubble, a new Activity was launched to it from a source * activity of different windowing mode, such as Launcher. * - Pattern: * - There is a move-to-front type Task in TransitionInfo, but is not Bubbled. * - That Task was Bubbled before the transition. * - Expected Behavior: * - Skip here. * - The Task should be dismissed from Bubble, and get opened in source's windowing mode. * - Case 5: the source Task was in an expanded Bubble, it launched an Activity in new Task, * and finished itself, such as Task trampoline. * - Pattern: * - There is an open type Task in TransitionInfo, which is Bubbled. * - There is a close type Task in TransitionInfo, which is Bubbled. * - Expected Behavior: * - Jump cut, so the user should not see an extra animation for Task trampoline. * - Case 6: the source Task was in an expanded Bubble, it launched an Activity in new Task, * but didn't finish itself. * - Pattern: * - There is an opening Task in TransitionInfo, which is Bubbled. * - (Optional) That Bubbled Task can be change/move-to-front type if it was in a different * windowing mode before the transition. * - A different Task was expanded Bubbled, but it may not be in TransitionInfo, as it may * be closed later. * - Expected Behavior: * - Play Bubble switch animation. */ static boolean animateEnterBubblesFromBubble( @NonNull IBinder transition, @NonNull TransitionInfo info, Loading @@ -422,53 +476,46 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { @NonNull Transitions.TransitionFinishCallback finishCallback, @NonNull BubbleTransitions bubbleTransitions) { // Identify the task being launched into a bubble final TransitionInfo.Change change = getChangeForBubblingTask(info, bubbleTransitions); if (change == null) { // Fallback to remote transition scenarios, ex: // 1. Move bubble'd app to fullscreen for launcher icon clicked // 2. Launch activity in expanded and selected bubble for notification clicked final TransitionInfo.Change enterBubbleTask = bubbleTransitions.getEnterBubbleTask(info); if (enterBubbleTask == null) { // The trigger Task is no longer in Bubble (Case 1/2/4) ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " No bubbling task found"); if (bubbleTransitions.containsNonBubbledExpandedTaskInStack(info)) { // The expanded Bubbled Task is no longer Bubbled (Case 2) ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " An activity launch converted the expanded Bubbled Task to non-Bubbled"); } return false; } final TransitionInfo.Change closingBubble = bubbleTransitions.getClosingBubbleTask(info); // Task transition scenarios, ex: // 1. Start a new task from a bubbled task // 2. Expand the collapsed bubble for notification launch // 3. Switch the expanded bubble for notification launch ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " + "entering bubble from another bubbled task or for an existing bubble"); bubbleTransitions.startBubbleToBubbleLaunchOrExistingBubbleConvert( transition, change.getTaskInfo(), handler -> { final Consumer<Transitions.TransitionHandler> onInflatedCallback = handler -> { final Transitions.TransitionHandler h = bubbleTransitions .getRunningEnterTransition(transition); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation played by %s", h); h.startAnimation( transition, info, startTransaction, finishTransaction, finishCallback); }); return true; } }; static @Nullable TransitionInfo.Change getChangeForBubblingTask( @NonNull TransitionInfo info, BubbleTransitions bubbleTransitions) { for (int i = 0; i < info.getChanges().size(); i++) { final TransitionInfo.Change chg = info.getChanges().get(i); final ActivityManager.RunningTaskInfo taskInfo = chg.getTaskInfo(); // Exclude activity transition scenarios. if (taskInfo == null || taskInfo.getActivityType() != ACTIVITY_TYPE_STANDARD) { continue; } // Only process opening or change transitions. if (!TransitionUtil.isOpeningMode(chg.getMode()) && chg.getMode() != TRANSIT_CHANGE) { continue; } // Skip non-app-bubble tasks (e.g., a reused task in a bubble-to-fullscreen scenario). if (!bubbleTransitions.shouldBeAppBubble(taskInfo)) { continue; } return chg; if (closingBubble != null && isOpeningType(enterBubbleTask.getMode())) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " + "opening bubble from another closing bubbled task"); // Task Trampoline (Case 5) // TODO(b/417848405): Update the trampoline transition to jumpcut. bubbleTransitions.startBubbleToBubbleLaunchOrExistingBubbleConvert( transition, enterBubbleTask.getTaskInfo(), onInflatedCallback); } else { // Opening a Bubble Task (Case 3/6) ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " + "entering bubble from another bubbled task or for an existing bubble"); bubbleTransitions.startBubbleToBubbleLaunchOrExistingBubbleConvert( transition, enterBubbleTask.getTaskInfo(), onInflatedCallback); } return null; return true; } private boolean animateUnfold( Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java +51 −1 Original line number Diff line number Diff line Loading @@ -16,7 +16,9 @@ package com.android.wm.shell.bubbles; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; Loading Loading @@ -172,6 +174,7 @@ public class BubbleTransitionsTest extends ShellTestCase { final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); final WindowContainerToken token = new MockToken().token(); taskInfo.token = token; taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); when(taskViewTaskController.getTaskInfo()).thenReturn(taskInfo); when(taskView.getController()).thenReturn(taskViewTaskController); when(mBubble.getTaskView()).thenReturn(taskView); Loading @@ -189,7 +192,10 @@ public class BubbleTransitionsTest extends ShellTestCase { when(mBubble.isApp()).thenReturn(true); when(mBubble.getIntent()).thenReturn(new Intent()); when(mBubble.getUser()).thenReturn(new UserHandle(0)); return setupBubble(taskView, taskViewTaskController); final ActivityManager.RunningTaskInfo taskInfo = setupBubble( taskView, taskViewTaskController); when(mBubbleController.shouldBeAppBubble(taskInfo)).thenReturn(true); return taskInfo; } private TransitionInfo setupFullscreenTaskTransition(ActivityManager.RunningTaskInfo taskInfo, Loading Loading @@ -1073,4 +1079,48 @@ public class BubbleTransitionsTest extends ShellTestCase { // Now the queue should be empty assertThat(mTaskViewTransitions.hasPending()).isFalse(); } @Test public void testGetEnterBubbleTask() { final SurfaceControl leash = new SurfaceControl.Builder().setName("testLeash").build(); final ActivityManager.RunningTaskInfo taskInfo0 = setupAppBubble(); final ActivityManager.RunningTaskInfo taskInfo1 = setupAppBubble(); final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0); final TransitionInfo.Change openingBubble = new TransitionInfo.Change( taskInfo0.token, leash); openingBubble.setTaskInfo(taskInfo0); openingBubble.setMode(TRANSIT_OPEN); final TransitionInfo.Change closingBubble = new TransitionInfo.Change( taskInfo1.token, leash); closingBubble.setTaskInfo(taskInfo1); closingBubble.setMode(TRANSIT_CLOSE); info.addChange(closingBubble); info.addChange(openingBubble); info.addRoot(new TransitionInfo.Root(0, mock(SurfaceControl.class), 0, 0)); assertThat(mBubbleTransitions.getEnterBubbleTask(info)).isEqualTo(openingBubble); } @Test public void testGetClosingBubbleTask() { final SurfaceControl leash = new SurfaceControl.Builder().setName("testLeash").build(); final ActivityManager.RunningTaskInfo taskInfo0 = setupAppBubble(); final ActivityManager.RunningTaskInfo taskInfo1 = setupAppBubble(); final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0); final TransitionInfo.Change openingBubble = new TransitionInfo.Change( taskInfo0.token, leash); openingBubble.setTaskInfo(taskInfo0); openingBubble.setMode(TRANSIT_OPEN); final TransitionInfo.Change closingBubble = new TransitionInfo.Change( taskInfo1.token, leash); closingBubble.setTaskInfo(taskInfo1); closingBubble.setMode(TRANSIT_CLOSE); info.addChange(openingBubble); info.addChange(closingBubble); info.addRoot(new TransitionInfo.Root(0, mock(SurfaceControl.class), 0, 0)); assertThat(mBubbleTransitions.getClosingBubbleTask(info)).isEqualTo(closingBubble); } } libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultMixedHandlerTest.kt +22 −16 Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ import com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.activityembedding.ActivityEmbeddingController import com.android.wm.shell.bubbles.BubbleController import com.android.wm.shell.bubbles.BubbleTransitions import com.android.wm.shell.desktopmode.DesktopTasksController import com.android.wm.shell.keyguard.KeyguardTransitionHandler Loading @@ -50,6 +51,7 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.spy import org.mockito.kotlin.stub import org.mockito.kotlin.verify Loading @@ -70,7 +72,16 @@ class DefaultMixedHandlerTest : ShellTestCase() { private val desktopTasksController = mock<DesktopTasksController>() private val unfoldTransitionHandler = mock<UnfoldTransitionHandler>() private val activityEmbeddingController = mock<ActivityEmbeddingController>() private val bubbleTransitions = mock<BubbleTransitions>() private val bubbleController = mock<BubbleController>() private val bubbleTransitions = spy(BubbleTransitions( mContext, transitions, mock(), mock(), mock(), mock(), mock(), )) private val shellInit: ShellInit = ShellInit(TestShellExecutor()) private val mixedHandler = DefaultMixedHandler( Loading @@ -89,6 +100,7 @@ class DefaultMixedHandlerTest : ShellTestCase() { @Before fun setUp() { shellInit.init() bubbleTransitions.setBubbleController(bubbleController) } @Test Loading Loading @@ -120,7 +132,6 @@ class DefaultMixedHandlerTest : ShellTestCase() { bubbleTransitions.stub { on { hasPendingEnterTransition(request) } doReturn true on { isShowingAsBubbleBar } doReturn false } assertThat(mixedHandler.requestHasBubbleEnter(request)).isTrue() Loading @@ -134,7 +145,6 @@ class DefaultMixedHandlerTest : ShellTestCase() { bubbleTransitions.stub { on { hasPendingEnterTransition(request) } doReturn true on { isShowingAsBubbleBar } doReturn true } assertThat(mixedHandler.requestHasBubbleEnter(request)).isTrue() Loading @@ -149,8 +159,9 @@ class DefaultMixedHandlerTest : ShellTestCase() { bubbleTransitions.stub { on { hasPendingEnterTransition(request) } doReturn true on { isShowingAsBubbleBar } doReturn true } doReturn(mock<Transitions.TransitionHandler>()).`when`(bubbleTransitions) .storePendingEnterTransition(any(), any()) mixedHandler.handleRequest(Binder(), request) verify(remoteTransition).onTransitionConsumed(any(), eq(false)) Loading Loading @@ -182,8 +193,7 @@ class DefaultMixedHandlerTest : ShellTestCase() { val runningTask = createRunningTask(100) val request = createTransitionRequestInfo(runningTask) bubbleTransitions.stub { on { isShowingAsBubbleBar } doReturn false bubbleController.stub { on { shouldBeAppBubble(runningTask) } doReturn true } Loading @@ -197,8 +207,7 @@ class DefaultMixedHandlerTest : ShellTestCase() { val runningTask = createRunningTask(100) val request = createTransitionRequestInfo(runningTask) bubbleTransitions.stub { on { isShowingAsBubbleBar } doReturn true bubbleController.stub { on { shouldBeAppBubble(runningTask) } doReturn true } Loading @@ -213,8 +222,7 @@ class DefaultMixedHandlerTest : ShellTestCase() { val remoteTransition = mock<IRemoteTransition>() val request = createTransitionRequestInfo(runningTask, RemoteTransition(remoteTransition)) bubbleTransitions.stub { on { isShowingAsBubbleBar } doReturn true bubbleController.stub { on { shouldBeAppBubble(runningTask) } doReturn true } Loading @@ -228,8 +236,7 @@ class DefaultMixedHandlerTest : ShellTestCase() { fun test_startAnimation_NoBubbleEnterFromAppBubble() { val info = TransitionInfo(TRANSIT_OPEN, 0) bubbleTransitions.stub { on { isShowingAsBubbleBar } doReturn true bubbleController.stub { on { shouldBeAppBubble(any()) } doReturn true } Loading @@ -237,7 +244,7 @@ class DefaultMixedHandlerTest : ShellTestCase() { mock<SurfaceControl.Transaction>(), mock<Transitions.TransitionFinishCallback>()) verify(bubbleTransitions, never()).startBubbleToBubbleLaunchOrExistingBubbleConvert( any(), any(), any()); any(), any(), any()) } @Test Loading @@ -249,8 +256,7 @@ class DefaultMixedHandlerTest : ShellTestCase() { val info = TransitionInfo(TRANSIT_OPEN, 0) info.addChange(change) bubbleTransitions.stub { on { isShowingAsBubbleBar } doReturn true bubbleController.stub { on { shouldBeAppBubble(any()) } doReturn true } Loading @@ -258,7 +264,7 @@ class DefaultMixedHandlerTest : ShellTestCase() { mock<SurfaceControl.Transaction>(), mock<Transitions.TransitionFinishCallback>()) verify(bubbleTransitions).startBubbleToBubbleLaunchOrExistingBubbleConvert( any(), any(), any()); any(), any(), any()) } private fun createTransitionRequestInfo( Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java +82 −15 Original line number Diff line number Diff line Loading @@ -20,9 +20,11 @@ import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_A import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.PendingIntent.FLAG_IMMUTABLE; import static android.app.PendingIntent.FLAG_ONE_SHOT; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.view.View.INVISIBLE; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; Loading Loading @@ -132,7 +134,8 @@ public class BubbleTransitions { mAppInfoProvider = appInfoProvider; } void setBubbleController(BubbleController controller) { @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public void setBubbleController(BubbleController controller) { mBubbleController = controller; } Loading @@ -143,20 +146,6 @@ public class BubbleTransitions { return mBubbleController.shouldBeAppBubble(taskInfo); } /** * Returns whether bubbles are showing as the bubble bar. */ public boolean isShowingAsBubbleBar() { return mBubbleController.isShowingAsBubbleBar(); } /** * Returns whether there is an existing bubble with the given task id. */ public boolean hasBubbleWithTaskId(int taskId) { return mBubbleData.getBubbleInStackWithTaskId(taskId) != null; } /** * Returns whether there is a pending transition for the given request. */ Loading Loading @@ -306,6 +295,84 @@ public class BubbleTransitions { return new DraggedBubbleIconToFullscreen(bubble, dropLocation); } /** * Finds the Task that is entering Bubble. This can be either a Bubble Task that is becoming * visible, or a visible Task that is changing to Bubble from other windowing mode. */ @Nullable public TransitionInfo.Change getEnterBubbleTask(@NonNull TransitionInfo info) { for (int i = 0; i < info.getChanges().size(); i++) { final TransitionInfo.Change chg = info.getChanges().get(i); final ActivityManager.RunningTaskInfo taskInfo = chg.getTaskInfo(); // Exclude activity transition scenarios. if (taskInfo == null || taskInfo.getActivityType() != ACTIVITY_TYPE_STANDARD) { continue; } // Only process opening or change transitions. if (!isOpeningMode(chg.getMode()) && chg.getMode() != TRANSIT_CHANGE) { continue; } // Skip non-app-bubble tasks (e.g. a reused task in a bubble-to-fullscreen scenario). if (!shouldBeAppBubble(taskInfo)) { continue; } return chg; } return null; } /** * Finds the Bubble Task that is closing. * Note: this doesn't find move-to-back Task. */ @Nullable public TransitionInfo.Change getClosingBubbleTask(@NonNull TransitionInfo info) { for (int i = 0; i < info.getChanges().size(); i++) { final TransitionInfo.Change chg = info.getChanges().get(i); final ActivityManager.RunningTaskInfo taskInfo = chg.getTaskInfo(); // Exclude activity transition scenarios. if (taskInfo == null || taskInfo.getActivityType() != ACTIVITY_TYPE_STANDARD) { continue; } // Only process closing transitions. if (chg.getMode() != TRANSIT_CLOSE) { continue; } // Skip non-app-bubble tasks (e.g., a reused task in a bubble-to-fullscreen scenario). if (!shouldBeAppBubble(taskInfo)) { continue; } return chg; } return null; } /** * Whether the transition contains any Task that is changed from expanded App Bubbled to * non-Bubbled. */ public boolean containsNonBubbledExpandedTaskInStack(@NonNull TransitionInfo info) { if (!mBubbleData.isExpanded() || mBubbleData.getSelectedBubble() == null) { // No expanded. return false; } if (!(mBubbleData.getSelectedBubble() instanceof Bubble bubble) || !bubble.isApp()) { // Not app Bubble. return false; } final int expandedTaskId = bubble.getTaskId(); for (int i = 0; i < info.getChanges().size(); i++) { final TransitionInfo.Change chg = info.getChanges().get(i); final ActivityManager.RunningTaskInfo taskInfo = chg.getTaskInfo(); if (taskInfo == null || taskInfo.taskId != expandedTaskId) { continue; } // Check whether it is still an app bubble. return !shouldBeAppBubble(taskInfo); } return false; } /** * Plucks the task-surface out of an ancestor view while making the view invisible. This helper * attempts to do this seamlessly (ie. view becomes invisible in sync with task reparent). Loading
libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +1 −2 Original line number Diff line number Diff line Loading @@ -863,8 +863,7 @@ public class DefaultMixedHandler implements MixedTransitionHandler, if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) { return null; } final TransitionInfo.Change change = DefaultMixedTransition.getChangeForBubblingTask(info, mBubbleTransitions); final TransitionInfo.Change change = mBubbleTransitions.getEnterBubbleTask(info); if (!com.android.wm.shell.Flags.fixTaskViewRotationAnimation()) { return change; } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java +94 −47 Original line number Diff line number Diff line Loading @@ -16,11 +16,10 @@ package com.android.wm.shell.transition; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static com.android.wm.shell.shared.TransitionUtil.isOpeningType; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_REQUEST; import static com.android.wm.shell.transition.DefaultMixedHandler.subCopy; import static com.android.wm.shell.transition.MixedTransitionHelper.animateEnterPipFromSplit; Loading @@ -28,7 +27,6 @@ import static com.android.wm.shell.transition.MixedTransitionHelper.animateKeygu import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.os.IBinder; import android.view.SurfaceControl; import android.window.TransitionInfo; Loading @@ -41,7 +39,6 @@ import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.phone.transition.PipTransitionUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.shared.pip.PipFlags; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreenController; Loading @@ -49,6 +46,7 @@ import com.android.wm.shell.splitscreen.StageCoordinator; import com.android.wm.shell.unfold.UnfoldTransitionHandler; import java.util.List; import java.util.function.Consumer; class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { private final UnfoldTransitionHandler mUnfoldHandler; Loading Loading @@ -373,7 +371,7 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " + "entering Bubbles while Split-Screen is foreground by %s", handler); TransitionInfo.Change bubblingTask = getChangeForBubblingTask(info, bubbleTransitions); final TransitionInfo.Change bubblingTask = bubbleTransitions.getEnterBubbleTask(info); // find previous split location for other task @SplitScreen.StageType int topSplitStageToKeep = SplitScreen.STAGE_TYPE_UNDEFINED; for (int i = info.getChanges().size() - 1; i >= 0; i--) { Loading Loading @@ -414,6 +412,62 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { return true; } /** * This is called when a task is being launched from a bubble, or when a task is launching to an * existing bubble. It may be one of the following cases, and each should be animated * differently: * - Case 1: a Task was in an expanded Bubble, and a new Activity was launched on top of it * from the task itself, or from a non-activity window, such as notification. * - Pattern: * - There is no open type Task in TransitionInfo (because it's an Activity transition). * - A Task is Bubbled and expanded before and after the transition. * - Expected Behavior: * - Skip here. * - Play Activity launch animation. * - Case 2: a Task was in an expanded Bubble, and a new Activity was launched on top of it * from a source activity of different windowing mode, such as Launcher. * - Pattern: * - There is a change type Task in TransitionInfo, which is no longer Bubbled. * - Expected Behavior: * - Skip here. * - The Task should be dismissed from Bubble, and get opened in new windowing mode. * Note: this shouldn't happen from normal user flow, and it now skipped here, but if it * happens, there may not be a good animation. * - Case 3: a Task was in an unfocused Bubble, a new Activity was launched to it from the * focused expanded Bubble, or from a non-activity window, such as notification. * - Pattern: * - There is a move-to-front type Task in TransitionInfo, which is Bubbled. * - That Task is Bubbled before and after the transition. * - (Optional) There is a move-to-back type Task in TransitionInfo, which is Bubbled. * - Expected Behavior: * - Play expand Bubble animation. * - (Optional) Hide the previous expanded Bubble. * - Case 4: a Task was in an unfocused Bubble, a new Activity was launched to it from a source * activity of different windowing mode, such as Launcher. * - Pattern: * - There is a move-to-front type Task in TransitionInfo, but is not Bubbled. * - That Task was Bubbled before the transition. * - Expected Behavior: * - Skip here. * - The Task should be dismissed from Bubble, and get opened in source's windowing mode. * - Case 5: the source Task was in an expanded Bubble, it launched an Activity in new Task, * and finished itself, such as Task trampoline. * - Pattern: * - There is an open type Task in TransitionInfo, which is Bubbled. * - There is a close type Task in TransitionInfo, which is Bubbled. * - Expected Behavior: * - Jump cut, so the user should not see an extra animation for Task trampoline. * - Case 6: the source Task was in an expanded Bubble, it launched an Activity in new Task, * but didn't finish itself. * - Pattern: * - There is an opening Task in TransitionInfo, which is Bubbled. * - (Optional) That Bubbled Task can be change/move-to-front type if it was in a different * windowing mode before the transition. * - A different Task was expanded Bubbled, but it may not be in TransitionInfo, as it may * be closed later. * - Expected Behavior: * - Play Bubble switch animation. */ static boolean animateEnterBubblesFromBubble( @NonNull IBinder transition, @NonNull TransitionInfo info, Loading @@ -422,53 +476,46 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { @NonNull Transitions.TransitionFinishCallback finishCallback, @NonNull BubbleTransitions bubbleTransitions) { // Identify the task being launched into a bubble final TransitionInfo.Change change = getChangeForBubblingTask(info, bubbleTransitions); if (change == null) { // Fallback to remote transition scenarios, ex: // 1. Move bubble'd app to fullscreen for launcher icon clicked // 2. Launch activity in expanded and selected bubble for notification clicked final TransitionInfo.Change enterBubbleTask = bubbleTransitions.getEnterBubbleTask(info); if (enterBubbleTask == null) { // The trigger Task is no longer in Bubble (Case 1/2/4) ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " No bubbling task found"); if (bubbleTransitions.containsNonBubbledExpandedTaskInStack(info)) { // The expanded Bubbled Task is no longer Bubbled (Case 2) ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " An activity launch converted the expanded Bubbled Task to non-Bubbled"); } return false; } final TransitionInfo.Change closingBubble = bubbleTransitions.getClosingBubbleTask(info); // Task transition scenarios, ex: // 1. Start a new task from a bubbled task // 2. Expand the collapsed bubble for notification launch // 3. Switch the expanded bubble for notification launch ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " + "entering bubble from another bubbled task or for an existing bubble"); bubbleTransitions.startBubbleToBubbleLaunchOrExistingBubbleConvert( transition, change.getTaskInfo(), handler -> { final Consumer<Transitions.TransitionHandler> onInflatedCallback = handler -> { final Transitions.TransitionHandler h = bubbleTransitions .getRunningEnterTransition(transition); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation played by %s", h); h.startAnimation( transition, info, startTransaction, finishTransaction, finishCallback); }); return true; } }; static @Nullable TransitionInfo.Change getChangeForBubblingTask( @NonNull TransitionInfo info, BubbleTransitions bubbleTransitions) { for (int i = 0; i < info.getChanges().size(); i++) { final TransitionInfo.Change chg = info.getChanges().get(i); final ActivityManager.RunningTaskInfo taskInfo = chg.getTaskInfo(); // Exclude activity transition scenarios. if (taskInfo == null || taskInfo.getActivityType() != ACTIVITY_TYPE_STANDARD) { continue; } // Only process opening or change transitions. if (!TransitionUtil.isOpeningMode(chg.getMode()) && chg.getMode() != TRANSIT_CHANGE) { continue; } // Skip non-app-bubble tasks (e.g., a reused task in a bubble-to-fullscreen scenario). if (!bubbleTransitions.shouldBeAppBubble(taskInfo)) { continue; } return chg; if (closingBubble != null && isOpeningType(enterBubbleTask.getMode())) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " + "opening bubble from another closing bubbled task"); // Task Trampoline (Case 5) // TODO(b/417848405): Update the trampoline transition to jumpcut. bubbleTransitions.startBubbleToBubbleLaunchOrExistingBubbleConvert( transition, enterBubbleTask.getTaskInfo(), onInflatedCallback); } else { // Opening a Bubble Task (Case 3/6) ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " + "entering bubble from another bubbled task or for an existing bubble"); bubbleTransitions.startBubbleToBubbleLaunchOrExistingBubbleConvert( transition, enterBubbleTask.getTaskInfo(), onInflatedCallback); } return null; return true; } private boolean animateUnfold( Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java +51 −1 Original line number Diff line number Diff line Loading @@ -16,7 +16,9 @@ package com.android.wm.shell.bubbles; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; Loading Loading @@ -172,6 +174,7 @@ public class BubbleTransitionsTest extends ShellTestCase { final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); final WindowContainerToken token = new MockToken().token(); taskInfo.token = token; taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); when(taskViewTaskController.getTaskInfo()).thenReturn(taskInfo); when(taskView.getController()).thenReturn(taskViewTaskController); when(mBubble.getTaskView()).thenReturn(taskView); Loading @@ -189,7 +192,10 @@ public class BubbleTransitionsTest extends ShellTestCase { when(mBubble.isApp()).thenReturn(true); when(mBubble.getIntent()).thenReturn(new Intent()); when(mBubble.getUser()).thenReturn(new UserHandle(0)); return setupBubble(taskView, taskViewTaskController); final ActivityManager.RunningTaskInfo taskInfo = setupBubble( taskView, taskViewTaskController); when(mBubbleController.shouldBeAppBubble(taskInfo)).thenReturn(true); return taskInfo; } private TransitionInfo setupFullscreenTaskTransition(ActivityManager.RunningTaskInfo taskInfo, Loading Loading @@ -1073,4 +1079,48 @@ public class BubbleTransitionsTest extends ShellTestCase { // Now the queue should be empty assertThat(mTaskViewTransitions.hasPending()).isFalse(); } @Test public void testGetEnterBubbleTask() { final SurfaceControl leash = new SurfaceControl.Builder().setName("testLeash").build(); final ActivityManager.RunningTaskInfo taskInfo0 = setupAppBubble(); final ActivityManager.RunningTaskInfo taskInfo1 = setupAppBubble(); final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0); final TransitionInfo.Change openingBubble = new TransitionInfo.Change( taskInfo0.token, leash); openingBubble.setTaskInfo(taskInfo0); openingBubble.setMode(TRANSIT_OPEN); final TransitionInfo.Change closingBubble = new TransitionInfo.Change( taskInfo1.token, leash); closingBubble.setTaskInfo(taskInfo1); closingBubble.setMode(TRANSIT_CLOSE); info.addChange(closingBubble); info.addChange(openingBubble); info.addRoot(new TransitionInfo.Root(0, mock(SurfaceControl.class), 0, 0)); assertThat(mBubbleTransitions.getEnterBubbleTask(info)).isEqualTo(openingBubble); } @Test public void testGetClosingBubbleTask() { final SurfaceControl leash = new SurfaceControl.Builder().setName("testLeash").build(); final ActivityManager.RunningTaskInfo taskInfo0 = setupAppBubble(); final ActivityManager.RunningTaskInfo taskInfo1 = setupAppBubble(); final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0); final TransitionInfo.Change openingBubble = new TransitionInfo.Change( taskInfo0.token, leash); openingBubble.setTaskInfo(taskInfo0); openingBubble.setMode(TRANSIT_OPEN); final TransitionInfo.Change closingBubble = new TransitionInfo.Change( taskInfo1.token, leash); closingBubble.setTaskInfo(taskInfo1); closingBubble.setMode(TRANSIT_CLOSE); info.addChange(openingBubble); info.addChange(closingBubble); info.addRoot(new TransitionInfo.Root(0, mock(SurfaceControl.class), 0, 0)); assertThat(mBubbleTransitions.getClosingBubbleTask(info)).isEqualTo(closingBubble); } }
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultMixedHandlerTest.kt +22 −16 Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ import com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.activityembedding.ActivityEmbeddingController import com.android.wm.shell.bubbles.BubbleController import com.android.wm.shell.bubbles.BubbleTransitions import com.android.wm.shell.desktopmode.DesktopTasksController import com.android.wm.shell.keyguard.KeyguardTransitionHandler Loading @@ -50,6 +51,7 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.spy import org.mockito.kotlin.stub import org.mockito.kotlin.verify Loading @@ -70,7 +72,16 @@ class DefaultMixedHandlerTest : ShellTestCase() { private val desktopTasksController = mock<DesktopTasksController>() private val unfoldTransitionHandler = mock<UnfoldTransitionHandler>() private val activityEmbeddingController = mock<ActivityEmbeddingController>() private val bubbleTransitions = mock<BubbleTransitions>() private val bubbleController = mock<BubbleController>() private val bubbleTransitions = spy(BubbleTransitions( mContext, transitions, mock(), mock(), mock(), mock(), mock(), )) private val shellInit: ShellInit = ShellInit(TestShellExecutor()) private val mixedHandler = DefaultMixedHandler( Loading @@ -89,6 +100,7 @@ class DefaultMixedHandlerTest : ShellTestCase() { @Before fun setUp() { shellInit.init() bubbleTransitions.setBubbleController(bubbleController) } @Test Loading Loading @@ -120,7 +132,6 @@ class DefaultMixedHandlerTest : ShellTestCase() { bubbleTransitions.stub { on { hasPendingEnterTransition(request) } doReturn true on { isShowingAsBubbleBar } doReturn false } assertThat(mixedHandler.requestHasBubbleEnter(request)).isTrue() Loading @@ -134,7 +145,6 @@ class DefaultMixedHandlerTest : ShellTestCase() { bubbleTransitions.stub { on { hasPendingEnterTransition(request) } doReturn true on { isShowingAsBubbleBar } doReturn true } assertThat(mixedHandler.requestHasBubbleEnter(request)).isTrue() Loading @@ -149,8 +159,9 @@ class DefaultMixedHandlerTest : ShellTestCase() { bubbleTransitions.stub { on { hasPendingEnterTransition(request) } doReturn true on { isShowingAsBubbleBar } doReturn true } doReturn(mock<Transitions.TransitionHandler>()).`when`(bubbleTransitions) .storePendingEnterTransition(any(), any()) mixedHandler.handleRequest(Binder(), request) verify(remoteTransition).onTransitionConsumed(any(), eq(false)) Loading Loading @@ -182,8 +193,7 @@ class DefaultMixedHandlerTest : ShellTestCase() { val runningTask = createRunningTask(100) val request = createTransitionRequestInfo(runningTask) bubbleTransitions.stub { on { isShowingAsBubbleBar } doReturn false bubbleController.stub { on { shouldBeAppBubble(runningTask) } doReturn true } Loading @@ -197,8 +207,7 @@ class DefaultMixedHandlerTest : ShellTestCase() { val runningTask = createRunningTask(100) val request = createTransitionRequestInfo(runningTask) bubbleTransitions.stub { on { isShowingAsBubbleBar } doReturn true bubbleController.stub { on { shouldBeAppBubble(runningTask) } doReturn true } Loading @@ -213,8 +222,7 @@ class DefaultMixedHandlerTest : ShellTestCase() { val remoteTransition = mock<IRemoteTransition>() val request = createTransitionRequestInfo(runningTask, RemoteTransition(remoteTransition)) bubbleTransitions.stub { on { isShowingAsBubbleBar } doReturn true bubbleController.stub { on { shouldBeAppBubble(runningTask) } doReturn true } Loading @@ -228,8 +236,7 @@ class DefaultMixedHandlerTest : ShellTestCase() { fun test_startAnimation_NoBubbleEnterFromAppBubble() { val info = TransitionInfo(TRANSIT_OPEN, 0) bubbleTransitions.stub { on { isShowingAsBubbleBar } doReturn true bubbleController.stub { on { shouldBeAppBubble(any()) } doReturn true } Loading @@ -237,7 +244,7 @@ class DefaultMixedHandlerTest : ShellTestCase() { mock<SurfaceControl.Transaction>(), mock<Transitions.TransitionFinishCallback>()) verify(bubbleTransitions, never()).startBubbleToBubbleLaunchOrExistingBubbleConvert( any(), any(), any()); any(), any(), any()) } @Test Loading @@ -249,8 +256,7 @@ class DefaultMixedHandlerTest : ShellTestCase() { val info = TransitionInfo(TRANSIT_OPEN, 0) info.addChange(change) bubbleTransitions.stub { on { isShowingAsBubbleBar } doReturn true bubbleController.stub { on { shouldBeAppBubble(any()) } doReturn true } Loading @@ -258,7 +264,7 @@ class DefaultMixedHandlerTest : ShellTestCase() { mock<SurfaceControl.Transaction>(), mock<Transitions.TransitionFinishCallback>()) verify(bubbleTransitions).startBubbleToBubbleLaunchOrExistingBubbleConvert( any(), any(), any()); any(), any(), any()) } private fun createTransitionRequestInfo( Loading