Merge "Support to launch multi-instance when long press to split on an app icon" into tm-qpr-dev am: 312e15b0f7 am: 729976c548

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19008926

Change-Id: Iae16815d8248d4e776fdb9f3f3364ed7803ffb1e
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Jerry Chang
2022-07-07 09:37:10 +00:00
committed by Automerger Merge Worker
5 changed files with 223 additions and 118 deletions

View File

@@ -45,12 +45,10 @@ import android.app.WindowConfiguration;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.ResolveInfo;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Bundle;
@@ -64,19 +62,15 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.internal.logging.InstanceId;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.split.SplitLayout;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* The policy for handling drag and drop operations to shell.
@@ -269,46 +263,10 @@ public class DragAndDropPolicy {
mStarter.startShortcut(packageName, id, position, opts, user);
} else {
final PendingIntent launchIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT);
mStarter.startIntent(launchIntent, getStartIntentFillInIntent(launchIntent, position),
position, opts);
mStarter.startIntent(launchIntent, null /* fillIntent */, position, opts);
}
}
/**
* Returns the fill-in intent to use when starting an app from a drop.
*/
@VisibleForTesting
Intent getStartIntentFillInIntent(PendingIntent launchIntent, @SplitPosition int position) {
// Get the drag app
final List<ResolveInfo> infos = launchIntent.queryIntentComponents(0 /* flags */);
final ComponentName dragIntentActivity = !infos.isEmpty()
? infos.get(0).activityInfo.getComponentName()
: null;
// Get the current app (either fullscreen or the remaining app post-drop if in splitscreen)
final boolean inSplitScreen = mSplitScreen != null
&& mSplitScreen.isSplitScreenVisible();
final ComponentName currentActivity;
if (!inSplitScreen) {
currentActivity = mSession.runningTaskInfo != null
? mSession.runningTaskInfo.baseIntent.getComponent()
: null;
} else {
final ActivityManager.RunningTaskInfo nonReplacedTaskInfo =
mSplitScreen.getTaskInfo(SplitLayout.reversePosition(position));
currentActivity = nonReplacedTaskInfo.baseIntent.getComponent();
}
if (Objects.equals(currentActivity, dragIntentActivity)) {
// Only apply MULTIPLE_TASK if we are dragging the same activity
final Intent fillInIntent = new Intent();
fillInIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Adding MULTIPLE_TASK");
return fillInIntent;
}
return null;
}
/**
* Per-drag session data.
*/

View File

@@ -18,6 +18,9 @@ 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.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
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;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
@@ -58,7 +61,9 @@ 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.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -74,6 +79,7 @@ import com.android.wm.shell.common.annotations.ExternalThread;
import com.android.wm.shell.common.split.SplitLayout;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
import com.android.wm.shell.draganddrop.DragAndDropPolicy;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.splitscreen.SplitScreen.StageType;
import com.android.wm.shell.transition.LegacyTransitions;
@@ -95,7 +101,7 @@ import java.util.concurrent.Executor;
*/
// TODO(b/198577848): Implement split screen flicker test to consolidate CUJ of split screen.
public class SplitScreenController implements DragAndDropPolicy.Starter,
RemoteCallable<SplitScreenController> {
RemoteCallable<SplitScreenController>, ShellTaskOrganizer.FocusListener {
private static final String TAG = SplitScreenController.class.getSimpleName();
static final int EXIT_REASON_UNKNOWN = 0;
@@ -143,6 +149,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
// outside the bounds of the roots by being reparented into a higher level fullscreen container
private SurfaceControl mSplitTasksContainerLayer;
private ActivityManager.RunningTaskInfo mFocusingTaskInfo;
public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer,
SyncTransactionQueue syncQueue, Context context,
RootTaskDisplayAreaOrganizer rootTDAOrganizer,
@@ -164,6 +172,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
mLogger = new SplitscreenEventLogger();
mIconProvider = iconProvider;
mRecentTasksOptional = recentTasks;
mTaskOrganizer.addFocusListener(this);
}
public SplitScreen asSplitScreen() {
@@ -180,6 +189,11 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
return mMainExecutor;
}
@Override
public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
mFocusingTaskInfo = taskInfo;
}
public void onOrganizerRegistered() {
if (mStageCoordinator == null) {
// TODO: Multi-display
@@ -341,14 +355,21 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
null /* wct */);
// 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();
}
// 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.
fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
// Flag with MULTIPLE_TASK if this is launching the same activity into both sides of the
// split.
if (isLaunchingAdjacently(intent.getIntent(), position)) {
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
}
intent.send(mContext, 0, fillInIntent, null /* onFinished */, null /* handler */,
null /* requiredPermission */, options);
} catch (PendingIntent.CanceledException e) {
@@ -358,6 +379,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
private void startIntentLegacy(PendingIntent intent, @Nullable Intent fillInIntent,
@SplitPosition int position, @Nullable Bundle options) {
boolean startSameActivityAdjacently = isLaunchingAdjacently(intent.getIntent(), position);
final WindowContainerTransaction evictWct = new WindowContainerTransaction();
mStageCoordinator.prepareEvictChildTasks(position, evictWct);
@@ -368,14 +391,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
IRemoteAnimationFinishedCallback finishedCallback,
SurfaceControl.Transaction t) {
if (apps == null || apps.length == 0) {
final ActivityManager.RunningTaskInfo pairedTaskInfo =
getTaskInfo(SplitLayout.reversePosition(position));
final ComponentName pairedActivity = pairedTaskInfo != null
? pairedTaskInfo.baseIntent.getComponent() : null;
final ComponentName intentActivity = intent.getIntent() != null
? intent.getIntent().getComponent() : null;
if (Objects.equals(pairedActivity, intentActivity)) {
if (startSameActivityAdjacently) {
// Switch split position if dragging the same activity to another side.
setSideStagePosition(SplitLayout.reversePosition(
mStageCoordinator.getSideStagePosition()));
@@ -417,11 +433,47 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
fillInIntent = new Intent();
}
fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
if (startSameActivityAdjacently) {
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
}
wct.sendPendingIntent(intent, fillInIntent, options);
mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct);
}
/** Returns {@code true} if it's launching the same component on both sides of the split. */
@VisibleForTesting
boolean isLaunchingAdjacently(@Nullable Intent startIntent,
@SplitPosition int position) {
if (startIntent == null) {
return false;
}
final ComponentName launchingActivity = startIntent.getComponent();
if (launchingActivity == null) {
return false;
}
if (isSplitScreenVisible()) {
final ActivityManager.RunningTaskInfo pairedTaskInfo =
getTaskInfo(SplitLayout.reversePosition(position));
final ComponentName pairedActivity = pairedTaskInfo != null
? pairedTaskInfo.baseIntent.getComponent() : null;
return Objects.equals(launchingActivity, pairedActivity);
}
if (mFocusingTaskInfo != null
// TODO (b/238032411): have an API to determine whether an activity is valid for
// split screen or not.
&& mFocusingTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
&& mFocusingTaskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) {
return Objects.equals(mFocusingTaskInfo.baseIntent.getComponent(), launchingActivity);
}
return false;
}
RemoteAnimationTarget[] onGoingToRecentsLegacy(RemoteAnimationTarget[] apps) {
if (isSplitScreenVisible()) {
// Evict child tasks except the top visible one under split root to ensure it could be

View File

@@ -200,11 +200,6 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
@Override
@CallSuper
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
if (!taskInfo.supportsMultiWindow) {
// Leave split screen if the task no longer supports multi window.
mCallbacks.onNoLongerSupportMultiWindow();
return;
}
if (mRootTaskInfo.taskId == taskInfo.taskId) {
// Inflates split decor view only when the root task is visible.
if (mRootTaskInfo.isVisible != taskInfo.isVisible) {
@@ -217,6 +212,12 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
}
mRootTaskInfo = taskInfo;
} else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
if (!taskInfo.supportsMultiWindow) {
// Leave split screen if the task no longer supports multi window.
mCallbacks.onNoLongerSupportMultiWindow();
return;
}
mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);
mCallbacks.onChildTaskStatusChanged(taskInfo.taskId, true /* present */,
taskInfo.isVisible);

View File

@@ -34,7 +34,6 @@ import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPL
import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT;
import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
@@ -57,7 +56,6 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Insets;
@@ -265,62 +263,6 @@ public class DragAndDropPolicyTest {
}
}
@Test
public void testLaunchMultipleTask_differentActivity() {
setRunningTask(mFullscreenAppTask);
mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
Intent fillInIntent = mPolicy.getStartIntentFillInIntent(mock(PendingIntent.class), 0);
assertNull(fillInIntent);
}
@Test
public void testLaunchMultipleTask_differentActivity_inSplitscreen() {
setRunningTask(mFullscreenAppTask);
doReturn(true).when(mSplitScreenStarter).isSplitScreenVisible();
doReturn(mFullscreenAppTask).when(mSplitScreenStarter).getTaskInfo(anyInt());
mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
Intent fillInIntent = mPolicy.getStartIntentFillInIntent(mock(PendingIntent.class), 0);
assertNull(fillInIntent);
}
@Test
public void testLaunchMultipleTask_sameActivity() {
setRunningTask(mFullscreenAppTask);
// Replace the mocked drag pending intent and ensure it resolves to the same activity
PendingIntent launchIntent = mock(PendingIntent.class);
ResolveInfo launchInfo = new ResolveInfo();
launchInfo.activityInfo = mFullscreenAppTask.topActivityInfo;
doReturn(Collections.singletonList(launchInfo))
.when(launchIntent).queryIntentComponents(anyInt());
mActivityClipData.getItemAt(0).getIntent().putExtra(ClipDescription.EXTRA_PENDING_INTENT,
launchIntent);
mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
Intent fillInIntent = mPolicy.getStartIntentFillInIntent(launchIntent, 0);
assertTrue((fillInIntent.getFlags() & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0);
}
@Test
public void testLaunchMultipleTask_sameActivity_inSplitScreen() {
setRunningTask(mFullscreenAppTask);
// Replace the mocked drag pending intent and ensure it resolves to the same activity
PendingIntent launchIntent = mock(PendingIntent.class);
ResolveInfo launchInfo = new ResolveInfo();
launchInfo.activityInfo = mFullscreenAppTask.topActivityInfo;
doReturn(Collections.singletonList(launchInfo))
.when(launchIntent).queryIntentComponents(anyInt());
mActivityClipData.getItemAt(0).getIntent().putExtra(ClipDescription.EXTRA_PENDING_INTENT,
launchIntent);
doReturn(true).when(mSplitScreenStarter).isSplitScreenVisible();
doReturn(mFullscreenAppTask).when(mSplitScreenStarter).getTaskInfo(anyInt());
mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId);
Intent fillInIntent = mPolicy.getStartIntentFillInIntent(launchIntent, 0);
assertTrue((fillInIntent.getFlags() & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0);
}
private Target filterTargetByType(ArrayList<Target> targets, int type) {
for (Target t : targets) {
if (type == t.type) {

View File

@@ -0,0 +1,152 @@
/*
* 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.splitscreen;
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 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.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.transition.Transitions;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Optional;
/**
* Tests for {@link SplitScreenController}
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
public class SplitScreenControllerTests extends ShellTestCase {
@Mock ShellTaskOrganizer mTaskOrganizer;
@Mock SyncTransactionQueue mSyncQueue;
@Mock RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
@Mock ShellExecutor mMainExecutor;
@Mock DisplayController mDisplayController;
@Mock DisplayImeController mDisplayImeController;
@Mock DisplayInsetsController mDisplayInsetsController;
@Mock Transitions mTransitions;
@Mock TransactionPool mTransactionPool;
@Mock IconProvider mIconProvider;
@Mock Optional<RecentTasksController> mRecentTasks;
private SplitScreenController mSplitScreenController;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mSplitScreenController = spy(new SplitScreenController(mTaskOrganizer, mSyncQueue, mContext,
mRootTDAOrganizer, mMainExecutor, mDisplayController, mDisplayImeController,
mDisplayInsetsController, mTransitions, mTransactionPool, mIconProvider,
mRecentTasks));
}
@Test
public void testIsLaunchingAdjacently_notInSplitScreen() {
doReturn(false).when(mSplitScreenController).isSplitScreenVisible();
// Verify launching the same activity returns true.
Intent startIntent = createStartIntent("startActivity");
ActivityManager.RunningTaskInfo focusTaskInfo =
createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
mSplitScreenController.onFocusTaskChanged(focusTaskInfo);
assertTrue(mSplitScreenController.isLaunchingAdjacently(
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);
mSplitScreenController.onFocusTaskChanged(focusTaskInfo);
assertFalse(mSplitScreenController.isLaunchingAdjacently(
startIntent, SPLIT_POSITION_TOP_OR_LEFT));
}
@Test
public void testIsLaunchingAdjacently_inSplitScreen() {
doReturn(true).when(mSplitScreenController).isSplitScreenVisible();
// Verify launching the same activity returns true.
Intent startIntent = createStartIntent("startActivity");
ActivityManager.RunningTaskInfo pairingTaskInfo =
createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent);
doReturn(pairingTaskInfo).when(mSplitScreenController).getTaskInfo(anyInt());
assertTrue(mSplitScreenController.isLaunchingAdjacently(
startIntent, SPLIT_POSITION_TOP_OR_LEFT));
// Verify launching different activity returns false.
Intent diffIntent = createStartIntent("diffActivity");
pairingTaskInfo =
createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, diffIntent);
doReturn(pairingTaskInfo).when(mSplitScreenController).getTaskInfo(anyInt());
assertFalse(mSplitScreenController.isLaunchingAdjacently(
startIntent, SPLIT_POSITION_TOP_OR_LEFT));
}
private Intent createStartIntent(String activityName) {
Intent intent = new Intent();
intent.setComponent(new ComponentName(mContext, activityName));
return intent;
}
private ActivityManager.RunningTaskInfo createTaskInfo(int winMode, int actType,
Intent strIntent) {
ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
info.configuration.windowConfiguration.setActivityType(actType);
info.configuration.windowConfiguration.setWindowingMode(winMode);
info.supportsMultiWindow = true;
info.baseIntent = strIntent;
info.baseActivity = strIntent.getComponent();
ActivityInfo activityInfo = new ActivityInfo();
activityInfo.packageName = info.baseActivity.getPackageName();
activityInfo.name = info.baseActivity.getClassName();
info.topActivityInfo = activityInfo;
return info;
}
}