diff options
7 files changed, 285 insertions, 140 deletions
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index df5f921f3a62..c6197c8a730b 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -111,4 +111,8 @@ <!-- Whether to dim a split-screen task when the other is the IME target --> <bool name="config_dimNonImeAttachedSide">true</bool> + + <!-- Components support to launch multiple instances into split-screen --> + <string-array name="config_componentsSupportMultiInstancesSplit"> + </string-array> </resources> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 839edc8c174a..3de1045bfbda 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -601,7 +601,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange animator.start(); } - /** Swich both surface position with animation. */ + /** Switch both surface position with animation. */ public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, Consumer<Rect> finishCallback) { final boolean isLandscape = isLandscape(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl index eb08d0ecbd06..5533ad56d17c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl @@ -86,8 +86,8 @@ interface ISplitScreen { /** * Starts a pair of intent and task in one transition. */ - oneway void startIntentAndTask(in PendingIntent pendingIntent, in Intent fillInIntent, - in Bundle options1, int taskId, in Bundle options2, int sidePosition, float splitRatio, + oneway void startIntentAndTask(in PendingIntent pendingIntent, in Bundle options1, int taskId, + in Bundle options2, int sidePosition, float splitRatio, in RemoteTransition remoteTransition, in InstanceId instanceId) = 16; /** @@ -108,9 +108,8 @@ interface ISplitScreen { * Starts a pair of intent and task using legacy transition system. */ oneway void startIntentAndTaskWithLegacyTransition(in PendingIntent pendingIntent, - in Intent fillInIntent, in Bundle options1, int taskId, in Bundle options2, - int splitPosition, float splitRatio, in RemoteAnimationAdapter adapter, - in InstanceId instanceId) = 12; + in Bundle options1, int taskId, in Bundle options2, int splitPosition, float splitRatio, + in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 12; /** * Starts a pair of shortcut and task using legacy transition system. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index c6a2b8312ebd..cdc8cdd2c28d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -18,6 +18,7 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityManager.START_SUCCESS; import static android.app.ActivityManager.START_TASK_TO_FRONT; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; import static android.view.Display.DEFAULT_DISPLAY; @@ -32,6 +33,8 @@ import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ActivityTaskManager; @@ -60,13 +63,12 @@ import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; import androidx.annotation.IntDef; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; import com.android.internal.protolog.common.ProtoLog; import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; @@ -166,8 +168,11 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private final IconProvider mIconProvider; private final Optional<RecentTasksController> mRecentTasksOptional; private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler; + private final String[] mMultiInstancesComponents; + + @VisibleForTesting + StageCoordinator mStageCoordinator; - private StageCoordinator mStageCoordinator; // Only used for the legacy recents animation from splitscreen to allow the tasks to be animated // outside the bounds of the roots by being reparented into a higher level fullscreen container private SurfaceControl mGoingToRecentsTasksLayer; @@ -210,6 +215,51 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) { shellInit.addInitCallback(this::onInit, this); } + + // TODO(255224696): Remove the config once having a way for client apps to opt-in + // multi-instances split. + mMultiInstancesComponents = mContext.getResources() + .getStringArray(R.array.config_componentsSupportMultiInstancesSplit); + } + + @VisibleForTesting + SplitScreenController(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, + ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue, + RootTaskDisplayAreaOrganizer rootTDAOrganizer, + DisplayController displayController, + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, + DragAndDropController dragAndDropController, + Transitions transitions, + TransactionPool transactionPool, + IconProvider iconProvider, + RecentTasksController recentTasks, + ShellExecutor mainExecutor, + StageCoordinator stageCoordinator) { + mShellCommandHandler = shellCommandHandler; + mShellController = shellController; + mTaskOrganizer = shellTaskOrganizer; + mSyncQueue = syncQueue; + mContext = context; + mRootTDAOrganizer = rootTDAOrganizer; + mMainExecutor = mainExecutor; + mDisplayController = displayController; + mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; + mDragAndDropController = dragAndDropController; + mTransitions = transitions; + mTransactionPool = transactionPool; + mIconProvider = iconProvider; + mRecentTasksOptional = Optional.of(recentTasks); + mStageCoordinator = stageCoordinator; + mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this); + shellInit.addInitCallback(this::onInit, this); + mMultiInstancesComponents = mContext.getResources() + .getStringArray(R.array.config_componentsSupportMultiInstancesSplit); } public SplitScreen asSplitScreen() { @@ -471,72 +521,116 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, startIntent(intent, fillInIntent, position, options); } + private void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, + @Nullable Bundle options1, int taskId, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, + InstanceId instanceId) { + Intent fillInIntent = null; + if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId) + && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) { + fillInIntent = new Intent(); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + } + mStageCoordinator.startIntentAndTaskWithLegacyTransition(pendingIntent, fillInIntent, + options1, taskId, options2, splitPosition, splitRatio, adapter, instanceId); + } + + private void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1, + int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, + float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + Intent fillInIntent = null; + if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId) + && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) { + fillInIntent = new Intent(); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + } + mStageCoordinator.startIntentAndTask(pendingIntent, fillInIntent, options1, taskId, + options2, splitPosition, splitRatio, remoteTransition, instanceId); + } + @Override public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options) { - if (fillInIntent == null) { - fillInIntent = new Intent(); - } - // Flag this as a no-user-action launch to prevent sending user leaving event to the - // current top activity since it's going to be put into another side of the split. This - // prevents the current top activity from going into pip mode due to user leaving event. + // Flag this as a no-user-action launch to prevent sending user leaving event to the current + // top activity since it's going to be put into another side of the split. This prevents the + // current top activity from going into pip mode due to user leaving event. + if (fillInIntent == null) fillInIntent = new Intent(); fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION); - // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of the - // split and there is no reusable background task. - if (shouldAddMultipleTaskFlag(intent.getIntent(), position)) { - final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional.isPresent() - ? mRecentTasksOptional.get().findTaskInBackground( - intent.getIntent().getComponent()) - : null; - if (taskInfo != null) { - startTask(taskInfo.taskId, position, options); + if (launchSameComponentAdjacently(intent, position, INVALID_TASK_ID)) { + final ComponentName launching = intent.getIntent().getComponent(); + if (supportMultiInstancesSplit(launching)) { + // To prevent accumulating large number of instances in the background, reuse task + // in the background with priority. + final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional + .map(recentTasks -> recentTasks.findTaskInBackground(launching)) + .orElse(null); + if (taskInfo != null) { + startTask(taskInfo.taskId, position, options); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Start task in background"); + return; + } + + // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of + // the split and there is no reusable background task. + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); + } else if (isSplitScreenVisible()) { + mStageCoordinator.switchSplitPosition("startIntent"); return; } - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); } - if (!ENABLE_SHELL_TRANSITIONS) { - mStageCoordinator.startIntentLegacy(intent, fillInIntent, position, options); - return; - } mStageCoordinator.startIntent(intent, fillInIntent, position, options); } /** Returns {@code true} if it's launching the same component on both sides of the split. */ - @VisibleForTesting - boolean shouldAddMultipleTaskFlag(@Nullable Intent startIntent, @SplitPosition int position) { - if (startIntent == null) { - return false; - } + private boolean launchSameComponentAdjacently(@Nullable PendingIntent pendingIntent, + @SplitPosition int position, int taskId) { + if (pendingIntent == null || pendingIntent.getIntent() == null) return false; + + final ComponentName launchingActivity = pendingIntent.getIntent().getComponent(); + if (launchingActivity == null) return false; - final ComponentName launchingActivity = startIntent.getComponent(); - if (launchingActivity == null) { + if (taskId != INVALID_TASK_ID) { + final ActivityManager.RunningTaskInfo taskInfo = + mTaskOrganizer.getRunningTaskInfo(taskId); + if (taskInfo != null) { + return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity); + } return false; } - if (isSplitScreenVisible()) { - // To prevent users from constantly dropping the same app to the same side resulting in - // a large number of instances in the background. - final ActivityManager.RunningTaskInfo targetTaskInfo = getTaskInfo(position); - final ComponentName targetActivity = targetTaskInfo != null - ? targetTaskInfo.baseIntent.getComponent() : null; - if (Objects.equals(launchingActivity, targetActivity)) { - return false; + if (!isSplitScreenVisible()) { + // Split screen is not yet activated, check if the current top running task is valid to + // split together. + final ActivityManager.RunningTaskInfo taskInfo = getFocusingTaskInfo(); + if (taskInfo != null && isValidToEnterSplitScreen(taskInfo)) { + return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity); } - - // Allow users to start a new instance the same to adjacent side. - final ActivityManager.RunningTaskInfo pairedTaskInfo = - getTaskInfo(SplitLayout.reversePosition(position)); - final ComponentName pairedActivity = pairedTaskInfo != null - ? pairedTaskInfo.baseIntent.getComponent() : null; - return Objects.equals(launchingActivity, pairedActivity); + return false; } - final ActivityManager.RunningTaskInfo taskInfo = getFocusingTaskInfo(); - if (taskInfo != null && isValidToEnterSplitScreen(taskInfo)) { - return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity); + // Compare to the adjacent side of the split to determine if this is launching the same + // component adjacently. + final ActivityManager.RunningTaskInfo pairedTaskInfo = + getTaskInfo(SplitLayout.reversePosition(position)); + final ComponentName pairedActivity = pairedTaskInfo != null + ? pairedTaskInfo.baseIntent.getComponent() : null; + return Objects.equals(launchingActivity, pairedActivity); + } + + @VisibleForTesting + /** Returns {@code true} if the component supports multi-instances split. */ + boolean supportMultiInstancesSplit(@Nullable ComponentName launching) { + if (launching == null) return false; + + final String componentName = launching.flattenToString(); + for (int i = 0; i < mMultiInstancesComponents.length; i++) { + if (mMultiInstancesComponents[i].equals(componentName)) { + return true; + } } return false; @@ -839,14 +933,13 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @Override public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, - Intent fillInIntent, Bundle options1, int taskId, Bundle options2, - int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, - InstanceId instanceId) { + Bundle options1, int taskId, Bundle options2, int splitPosition, float splitRatio, + RemoteAnimationAdapter adapter, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startIntentAndTaskWithLegacyTransition", (controller) -> - controller.mStageCoordinator.startIntentAndTaskWithLegacyTransition( - pendingIntent, fillInIntent, options1, taskId, options2, - splitPosition, splitRatio, adapter, instanceId)); + controller.startIntentAndTaskWithLegacyTransition(pendingIntent, + options1, taskId, options2, splitPosition, splitRatio, adapter, + instanceId)); } @Override @@ -872,14 +965,13 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override - public void startIntentAndTask(PendingIntent pendingIntent, Intent fillInIntent, - @Nullable Bundle options1, int taskId, @Nullable Bundle options2, - @SplitPosition int splitPosition, float splitRatio, - @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + public void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1, + int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, + float splitRatio, @Nullable RemoteTransition remoteTransition, + InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startIntentAndTask", - (controller) -> controller.mStageCoordinator.startIntentAndTask(pendingIntent, - fillInIntent, options1, taskId, options2, splitPosition, splitRatio, - remoteTransition, instanceId)); + (controller) -> controller.startIntentAndTask(pendingIntent, options1, taskId, + options2, splitPosition, splitRatio, remoteTransition, instanceId)); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index e888c6f8b0f9..c2ab7ef7e7bf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -24,7 +24,6 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.RemoteAnimationTarget.MODE_OPENING; @@ -428,6 +427,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** Launches an activity into split. */ void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options) { + if (!ENABLE_SHELL_TRANSITIONS) { + startIntentLegacy(intent, fillInIntent, position, options); + return; + } + final WindowContainerTransaction wct = new WindowContainerTransaction(); final WindowContainerTransaction evictWct = new WindowContainerTransaction(); prepareEvictChildTasks(position, evictWct); @@ -441,13 +445,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareEnterSplitScreen(wct, null /* taskInfo */, position); mSplitTransitions.startEnterTransition(transitType, wct, null, this, - aborted -> { - // Switch the split position if launching as MULTIPLE_TASK failed. - if (aborted && (fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) { - setSideStagePositionAnimated( - SplitLayout.reversePosition(mSideStagePosition)); - } - } /* consumedCallback */, + null /* consumedCallback */, (finishWct, finishT) -> { if (!evictWct.isEmpty()) { finishWct.merge(evictWct, true); @@ -457,7 +455,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** Launches an activity into split by legacy transition. */ void startIntentLegacy(PendingIntent intent, Intent fillInIntent, - @SplitPosition int position, @androidx.annotation.Nullable Bundle options) { + @SplitPosition int position, @Nullable Bundle options) { final WindowContainerTransaction evictWct = new WindowContainerTransaction(); prepareEvictChildTasks(position, evictWct); @@ -473,12 +471,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, exitSplitScreen(mMainStage.getChildCount() == 0 ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); mSplitUnsupportedToast.show(); - } else { - // Switch the split position if launching as MULTIPLE_TASK failed. - if ((fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) { - setSideStagePosition(SplitLayout.reversePosition( - getSideStagePosition()), null); - } } // Do nothing when the animation was cancelled. @@ -771,9 +763,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.evictInvisibleChildren(wct); } - Bundle resolveStartStage(@StageType int stage, - @SplitPosition int position, @androidx.annotation.Nullable Bundle options, - @androidx.annotation.Nullable WindowContainerTransaction wct) { + Bundle resolveStartStage(@StageType int stage, @SplitPosition int position, + @Nullable Bundle options, @Nullable WindowContainerTransaction wct) { switch (stage) { case STAGE_TYPE_UNDEFINED: { if (position != SPLIT_POSITION_UNDEFINED) { @@ -844,9 +835,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, : mMainStage.getTopVisibleChildTaskId(); } - void setSideStagePositionAnimated(@SplitPosition int sideStagePosition) { - if (mSideStagePosition == sideStagePosition) return; - SurfaceControl.Transaction t = mTransactionPool.acquire(); + void switchSplitPosition(String reason) { + final SurfaceControl.Transaction t = mTransactionPool.acquire(); mTempRect1.setEmpty(); final StageTaskListener topLeftStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; @@ -886,6 +876,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, va.start(); }); }); + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Switch split position: %s", reason); + mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(), + getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); } void setSideStagePosition(@SplitPosition int sideStagePosition, @@ -1617,10 +1612,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void onDoubleTappedDivider() { - setSideStagePositionAnimated(SplitLayout.reversePosition(mSideStagePosition)); - mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(), - getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); + switchSplitPosition("double tap"); } @Override diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index d01f3d310fc3..38b75f81171f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -16,18 +16,24 @@ package com.android.wm.shell.splitscreen; +import static android.app.PendingIntent.FLAG_IMMUTABLE; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -35,6 +41,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -65,11 +73,11 @@ import com.android.wm.shell.transition.Transitions; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.Optional; - /** * Tests for {@link SplitScreenController} */ @@ -91,18 +99,21 @@ public class SplitScreenControllerTests extends ShellTestCase { @Mock Transitions mTransitions; @Mock TransactionPool mTransactionPool; @Mock IconProvider mIconProvider; - @Mock Optional<RecentTasksController> mRecentTasks; + @Mock StageCoordinator mStageCoordinator; + @Mock RecentTasksController mRecentTasks; + @Captor ArgumentCaptor<Intent> mIntentCaptor; private SplitScreenController mSplitScreenController; @Before public void setup() { + assumeTrue(ActivityTaskManager.supportsSplitScreenMultiWindow(mContext)); MockitoAnnotations.initMocks(this); mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit, mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue, mRootTDAOrganizer, mDisplayController, mDisplayImeController, mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, - mIconProvider, mRecentTasks, mMainExecutor)); + mIconProvider, mRecentTasks, mMainExecutor, mStageCoordinator)); } @Test @@ -148,58 +159,100 @@ public class SplitScreenControllerTests extends ShellTestCase { } @Test - public void testShouldAddMultipleTaskFlag_notInSplitScreen() { - doReturn(false).when(mSplitScreenController).isSplitScreenVisible(); - doReturn(true).when(mSplitScreenController).isValidToEnterSplitScreen(any()); + public void testStartIntent_appendsNoUserActionFlag() { + Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); - // Verify launching the same activity returns true. + mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + + verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(), + eq(SPLIT_POSITION_TOP_OR_LEFT), isNull()); + assertEquals(FLAG_ACTIVITY_NO_USER_ACTION, + mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_NO_USER_ACTION); + } + + @Test + public void startIntent_multiInstancesSupported_appendsMultipleTaskFag() { + doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any()); Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); + // Put the same component into focus task ActivityManager.RunningTaskInfo focusTaskInfo = createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent); - doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo(); - assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag( - startIntent, SPLIT_POSITION_TOP_OR_LEFT)); - - // Verify launching different activity returns false. - Intent diffIntent = createStartIntent("diffActivity"); - focusTaskInfo = - createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, diffIntent); - doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo(); - assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag( - startIntent, SPLIT_POSITION_TOP_OR_LEFT)); + doReturn(focusTaskInfo).when(mStageCoordinator).getFocusingTaskInfo(); + doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any()); + + mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + + verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(), + eq(SPLIT_POSITION_TOP_OR_LEFT), isNull()); + assertEquals(FLAG_ACTIVITY_MULTIPLE_TASK, + mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK); } @Test - public void testShouldAddMultipleTaskFlag_inSplitScreen() { + public void startIntent_multiInstancesSupported_startTaskInBackgroundBeforeSplitActivated() { + doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any()); + doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any()); + Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); + // Put the same component into focus task + ActivityManager.RunningTaskInfo focusTaskInfo = + createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent); + doReturn(focusTaskInfo).when(mStageCoordinator).getFocusingTaskInfo(); + doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any()); + // Put the same component into a task in the background + ActivityManager.RecentTaskInfo sameTaskInfo = new ActivityManager.RecentTaskInfo(); + doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any()); + + mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + + verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT), + isNull()); + } + + @Test + public void startIntent_multiInstancesSupported_startTaskInBackgroundAfterSplitActivated() { + doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any()); + doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any()); + Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); + // Put the same component into another side of the split doReturn(true).when(mSplitScreenController).isSplitScreenVisible(); + ActivityManager.RunningTaskInfo sameTaskInfo = + createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent); + doReturn(sameTaskInfo).when(mSplitScreenController).getTaskInfo( + SPLIT_POSITION_BOTTOM_OR_RIGHT); + // Put the same component into a task in the background + doReturn(new ActivityManager.RecentTaskInfo()).when(mRecentTasks) + .findTaskInBackground(any()); + + mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + + verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT), + isNull()); + } + + @Test + public void startIntent_multiInstancesNotSupported_switchesPositionAfterSplitActivated() { + doReturn(false).when(mSplitScreenController).supportMultiInstancesSplit(any()); Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); + // Put the same component into another side of the split + doReturn(true).when(mSplitScreenController).isSplitScreenVisible(); ActivityManager.RunningTaskInfo sameTaskInfo = createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent); - Intent diffIntent = createStartIntent("diffActivity"); - ActivityManager.RunningTaskInfo differentTaskInfo = - createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, diffIntent); - - // Verify launching the same activity return false. - doReturn(sameTaskInfo).when(mSplitScreenController) - .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT); - assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag( - startIntent, SPLIT_POSITION_TOP_OR_LEFT)); - - // Verify launching the same activity as adjacent returns true. - doReturn(differentTaskInfo).when(mSplitScreenController) - .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT); - doReturn(sameTaskInfo).when(mSplitScreenController) - .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT); - assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag( - startIntent, SPLIT_POSITION_TOP_OR_LEFT)); - - // Verify launching different activity from adjacent returns false. - doReturn(differentTaskInfo).when(mSplitScreenController) - .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT); - doReturn(differentTaskInfo).when(mSplitScreenController) - .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT); - assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag( - startIntent, SPLIT_POSITION_TOP_OR_LEFT)); + doReturn(sameTaskInfo).when(mSplitScreenController).getTaskInfo( + SPLIT_POSITION_BOTTOM_OR_RIGHT); + + mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + + verify(mStageCoordinator).switchSplitPosition(anyString()); } private Intent createStartIntent(String activityName) { diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index fe691c61a96b..54664f5fbe3d 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -27,6 +27,7 @@ import static android.app.ActivityManager.START_FLAG_DEBUG; import static android.app.ActivityManager.START_FLAG_NATIVE_DEBUGGING; import static android.app.ActivityManager.START_FLAG_TRACK_ALLOCATION; import static android.app.ActivityManager.START_TASK_TO_FRONT; +import static android.app.ActivityOptions.ANIM_REMOTE_ANIMATION; import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY; import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN; import static android.app.WaitResult.INVALID_DELAY; @@ -2592,6 +2593,10 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { // Apply options to prevent pendingOptions be taken when scheduling // activity lifecycle transaction to make sure the override pending app // transition will be applied immediately. + if (activityOptions.getAnimationType() == ANIM_REMOTE_ANIMATION) { + targetActivity.mPendingRemoteAnimation = + activityOptions.getRemoteAnimationAdapter(); + } targetActivity.applyOptionsAnimation(); if (activityOptions != null && activityOptions.getLaunchCookie() != null) { targetActivity.mLaunchCookie = activityOptions.getLaunchCookie(); |