Allow dragging to launch multiple instances of the same activity

- When dragging to launch a new app, apply MULTIPLE_TASK to the launch
  intent to allow apps that support it to show side-by-side
- This requires the pending intent resolved from LauncherAppsService to
  be mutable (should be ok since we only use this api from SysUI)
- Also remove some unused members and duplication of getting the running
  task when dragging

Bug: 207686016
Test: atest WMShellUnitTests

Change-Id: Ib233ad754a6c6e3c4e0d0e10ed788ab8e055cccc
This commit is contained in:
Winson Chung
2022-02-08 06:22:47 +00:00
parent 5f32b96cee
commit f4ed441e18
9 changed files with 142 additions and 34 deletions

View File

@@ -58,7 +58,7 @@ interface ILauncherApps {
void startActivityAsUser(in IApplicationThread caller, String callingPackage, void startActivityAsUser(in IApplicationThread caller, String callingPackage,
String callingFeatureId, in ComponentName component, in Rect sourceBounds, String callingFeatureId, in ComponentName component, in Rect sourceBounds,
in Bundle opts, in UserHandle user); in Bundle opts, in UserHandle user);
PendingIntent getActivityLaunchIntent(in ComponentName component, in Bundle opts, PendingIntent getActivityLaunchIntent(String callingPackage, in ComponentName component,
in UserHandle user); in UserHandle user);
void showAppDetailsAsUser(in IApplicationThread caller, String callingPackage, void showAppDetailsAsUser(in IApplicationThread caller, String callingPackage,
String callingFeatureId, in ComponentName component, in Rect sourceBounds, String callingFeatureId, in ComponentName component, in Rect sourceBounds,

View File

@@ -749,24 +749,29 @@ public class LauncherApps {
} }
/** /**
* Returns a PendingIntent that would start the same activity started from * Returns a mutable PendingIntent that would start the same activity started from
* {@link #startMainActivity(ComponentName, UserHandle, Rect, Bundle)}. * {@link #startMainActivity(ComponentName, UserHandle, Rect, Bundle)}. The caller needs to
* take care in ensuring that the mutable intent returned is not passed to untrusted parties.
* *
* @param component The ComponentName of the activity to launch * @param component The ComponentName of the activity to launch
* @param startActivityOptions This parameter is no longer supported * @param startActivityOptions This parameter is no longer supported
* @param user The UserHandle of the profile * @param user The UserHandle of the profile
* @hide * @hide
*/ */
@RequiresPermission(android.Manifest.permission.START_TASKS_FROM_RECENTS)
@Nullable @Nullable
public PendingIntent getMainActivityLaunchIntent(@NonNull ComponentName component, public PendingIntent getMainActivityLaunchIntent(@NonNull ComponentName component,
@Nullable Bundle startActivityOptions, @NonNull UserHandle user) { @Nullable Bundle startActivityOptions, @NonNull UserHandle user) {
if (mContext.checkSelfPermission(android.Manifest.permission.START_TASKS_FROM_RECENTS)
!= PackageManager.PERMISSION_GRANTED) {
Log.w(TAG, "Only allowed for recents.");
}
logErrorForInvalidProfileAccess(user); logErrorForInvalidProfileAccess(user);
if (DEBUG) { if (DEBUG) {
Log.i(TAG, "GetMainActivityLaunchIntent " + component + " " + user); Log.i(TAG, "GetMainActivityLaunchIntent " + component + " " + user);
} }
try { try {
// due to b/209607104, startActivityOptions will be ignored return mService.getActivityLaunchIntent(mContext.getPackageName(), component, user);
return mService.getActivityLaunchIntent(component, null /* opts */, user);
} catch (RemoteException re) { } catch (RemoteException re) {
throw re.rethrowFromSystemServer(); throw re.rethrowFromSystemServer();
} }

View File

@@ -45,10 +45,12 @@ import android.app.WindowConfiguration;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.ClipData; import android.content.ClipData;
import android.content.ClipDescription; import android.content.ClipDescription;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.content.pm.LauncherApps; import android.content.pm.LauncherApps;
import android.content.pm.ResolveInfo;
import android.graphics.Insets; import android.graphics.Insets;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Bundle; import android.os.Bundle;
@@ -62,9 +64,11 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.android.internal.logging.InstanceId; import com.android.internal.logging.InstanceId;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R; import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.splitscreen.SplitScreenController;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@@ -106,11 +110,18 @@ public class DragAndDropPolicy {
*/ */
void start(DisplayLayout displayLayout, ClipData data, InstanceId loggerSessionId) { void start(DisplayLayout displayLayout, ClipData data, InstanceId loggerSessionId) {
mLoggerSessionId = loggerSessionId; mLoggerSessionId = loggerSessionId;
mSession = new DragSession(mContext, mActivityTaskManager, displayLayout, data); mSession = new DragSession(mActivityTaskManager, displayLayout, data);
// TODO(b/169894807): Also update the session data with task stack changes // TODO(b/169894807): Also update the session data with task stack changes
mSession.update(); mSession.update();
} }
/**
* Returns the last running task.
*/
ActivityManager.RunningTaskInfo getLatestRunningTask() {
return mSession.runningTaskInfo;
}
/** /**
* Returns the target's regions based on the current state of the device and display. * Returns the target's regions based on the current state of the device and display.
*/ */
@@ -248,32 +259,68 @@ public class DragAndDropPolicy {
final UserHandle user = intent.getParcelableExtra(EXTRA_USER); final UserHandle user = intent.getParcelableExtra(EXTRA_USER);
mStarter.startShortcut(packageName, id, position, opts, user); mStarter.startShortcut(packageName, id, position, opts, user);
} else { } else {
mStarter.startIntent(intent.getParcelableExtra(EXTRA_PENDING_INTENT), final PendingIntent launchIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT);
null, position, opts); mStarter.startIntent(launchIntent, getStartIntentFillInIntent(launchIntent, position),
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.baseActivity
: null;
} else {
final int nonReplacedSplitPosition = position == SPLIT_POSITION_TOP_OR_LEFT
? SPLIT_POSITION_BOTTOM_OR_RIGHT
: SPLIT_POSITION_TOP_OR_LEFT;
ActivityManager.RunningTaskInfo nonReplacedTaskInfo =
mSplitScreen.getTaskInfo(nonReplacedSplitPosition);
currentActivity = nonReplacedTaskInfo.baseActivity;
}
if (currentActivity.equals(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. * Per-drag session data.
*/ */
private static class DragSession { private static class DragSession {
private final Context mContext;
private final ActivityTaskManager mActivityTaskManager; private final ActivityTaskManager mActivityTaskManager;
private final ClipData mInitialDragData; private final ClipData mInitialDragData;
final DisplayLayout displayLayout; final DisplayLayout displayLayout;
Intent dragData; Intent dragData;
int runningTaskId; ActivityManager.RunningTaskInfo runningTaskInfo;
@WindowConfiguration.WindowingMode @WindowConfiguration.WindowingMode
int runningTaskWinMode = WINDOWING_MODE_UNDEFINED; int runningTaskWinMode = WINDOWING_MODE_UNDEFINED;
@WindowConfiguration.ActivityType @WindowConfiguration.ActivityType
int runningTaskActType = ACTIVITY_TYPE_STANDARD; int runningTaskActType = ACTIVITY_TYPE_STANDARD;
boolean runningTaskIsResizeable;
boolean dragItemSupportsSplitscreen; boolean dragItemSupportsSplitscreen;
DragSession(Context context, ActivityTaskManager activityTaskManager, DragSession(ActivityTaskManager activityTaskManager,
DisplayLayout dispLayout, ClipData data) { DisplayLayout dispLayout, ClipData data) {
mContext = context;
mActivityTaskManager = activityTaskManager; mActivityTaskManager = activityTaskManager;
mInitialDragData = data; mInitialDragData = data;
displayLayout = dispLayout; displayLayout = dispLayout;
@@ -287,10 +334,9 @@ public class DragAndDropPolicy {
mActivityTaskManager.getTasks(1, false /* filterOnlyVisibleRecents */); mActivityTaskManager.getTasks(1, false /* filterOnlyVisibleRecents */);
if (!tasks.isEmpty()) { if (!tasks.isEmpty()) {
final ActivityManager.RunningTaskInfo task = tasks.get(0); final ActivityManager.RunningTaskInfo task = tasks.get(0);
runningTaskInfo = task;
runningTaskWinMode = task.getWindowingMode(); runningTaskWinMode = task.getWindowingMode();
runningTaskActType = task.getActivityType(); runningTaskActType = task.getActivityType();
runningTaskId = task.taskId;
runningTaskIsResizeable = task.isResizeable;
} }
final ActivityInfo info = mInitialDragData.getItemAt(0).getActivityInfo(); final ActivityInfo info = mInitialDragData.getItemAt(0).getActivityInfo();

View File

@@ -26,7 +26,6 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.StatusBarManager; import android.app.StatusBarManager;
import android.content.ClipData; import android.content.ClipData;
import android.content.Context; import android.content.Context;
@@ -35,7 +34,6 @@ import android.graphics.Color;
import android.graphics.Insets; import android.graphics.Insets;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.RemoteException;
import android.view.DragEvent; import android.view.DragEvent;
import android.view.SurfaceControl; import android.view.SurfaceControl;
import android.view.WindowInsets; import android.view.WindowInsets;
@@ -51,7 +49,6 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.splitscreen.SplitScreenController;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
/** /**
* Coordinates the visible drop targets for the current drag. * Coordinates the visible drop targets for the current drag.
@@ -166,17 +163,8 @@ public class DragLayout extends LinearLayout {
boolean alreadyInSplit = mSplitScreenController != null boolean alreadyInSplit = mSplitScreenController != null
&& mSplitScreenController.isSplitScreenVisible(); && mSplitScreenController.isSplitScreenVisible();
if (!alreadyInSplit) { if (!alreadyInSplit) {
List<ActivityManager.RunningTaskInfo> tasks = null; ActivityManager.RunningTaskInfo taskInfo1 = mPolicy.getLatestRunningTask();
// Figure out the splashscreen info for the existing task. if (taskInfo1 != null) {
try {
tasks = ActivityTaskManager.getService().getTasks(1,
false /* filterOnlyVisibleRecents */,
false /* keepIntentExtra */);
} catch (RemoteException e) {
// don't show an icon / will just use the defaults
}
if (tasks != null && !tasks.isEmpty()) {
ActivityManager.RunningTaskInfo taskInfo1 = tasks.get(0);
Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo); Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo);
int bgColor1 = getResizingBackgroundColor(taskInfo1); int bgColor1 = getResizingBackgroundColor(taskInfo1);
mDropZoneView1.setAppInfo(bgColor1, icon1); mDropZoneView1.setAppInfo(bgColor1, icon1);

View File

@@ -22,7 +22,7 @@ import android.content.Context;
import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager;
import android.testing.TestableContext; import android.testing.TestableContext;
import androidx.test.InstrumentationRegistry; import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;

View File

@@ -23,6 +23,8 @@ import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY;
import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT;
import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
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_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
@@ -32,6 +34,7 @@ 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_RIGHT;
import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP; 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.assertTrue;
import static junit.framework.Assert.fail; import static junit.framework.Assert.fail;
@@ -50,9 +53,11 @@ import android.app.ActivityTaskManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.ClipData; import android.content.ClipData;
import android.content.ClipDescription; import android.content.ClipDescription;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Insets; import android.graphics.Insets;
@@ -177,6 +182,12 @@ public class DragAndDropPolicyTest {
info.configuration.windowConfiguration.setActivityType(actType); info.configuration.windowConfiguration.setActivityType(actType);
info.configuration.windowConfiguration.setWindowingMode(winMode); info.configuration.windowConfiguration.setWindowingMode(winMode);
info.isResizeable = true; info.isResizeable = true;
info.baseActivity = new ComponentName(getInstrumentation().getContext().getPackageName(),
".ActivityWithMode" + winMode);
ActivityInfo activityInfo = new ActivityInfo();
activityInfo.packageName = info.baseActivity.getPackageName();
activityInfo.name = info.baseActivity.getClassName();
info.topActivityInfo = activityInfo;
return info; return info;
} }
@@ -252,6 +263,62 @@ 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) { private Target filterTargetByType(ArrayList<Target> targets, int type) {
for (Target t : targets) { for (Target t : targets) {
if (type == t.type) { if (type == t.type) {

View File

@@ -24,8 +24,8 @@ import android.testing.AndroidTestingRunner;
import android.testing.TestableContext; import android.testing.TestableContext;
import android.testing.TestableLooper; import android.testing.TestableLooper;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.ShellExecutor;

View File

@@ -45,8 +45,8 @@ import android.window.DisplayAreaOrganizer;
import android.window.IWindowContainerToken; import android.window.IWindowContainerToken;
import android.window.WindowContainerToken; import android.window.WindowContainerToken;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DisplayLayout;

View File

@@ -18,6 +18,7 @@ package com.android.server.pm;
import static android.app.ActivityOptions.KEY_SPLASH_SCREEN_THEME; import static android.app.ActivityOptions.KEY_SPLASH_SCREEN_THEME;
import static android.app.PendingIntent.FLAG_IMMUTABLE; import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.PendingIntent.FLAG_MUTABLE;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
@@ -1218,8 +1219,9 @@ public class LauncherAppsService extends SystemService {
} }
@Override @Override
public PendingIntent getActivityLaunchIntent(ComponentName component, Bundle opts, public PendingIntent getActivityLaunchIntent(String callingPackage, ComponentName component,
UserHandle user) { UserHandle user) {
ensureShortcutPermission(callingPackage);
if (!canAccessProfile(user.getIdentifier(), "Cannot start activity")) { if (!canAccessProfile(user.getIdentifier(), "Cannot start activity")) {
throw new ActivityNotFoundException("Activity could not be found"); throw new ActivityNotFoundException("Activity could not be found");
} }
@@ -1237,7 +1239,7 @@ public class LauncherAppsService extends SystemService {
// calling identity to mirror the startActivityAsUser() call which does not validate // calling identity to mirror the startActivityAsUser() call which does not validate
// the calling user // the calling user
return PendingIntent.getActivityAsUser(mContext, 0 /* requestCode */, launchIntent, return PendingIntent.getActivityAsUser(mContext, 0 /* requestCode */, launchIntent,
FLAG_IMMUTABLE, null /* options */, user); FLAG_MUTABLE, null /* opts */, user);
} finally { } finally {
Binder.restoreCallingIdentity(ident); Binder.restoreCallingIdentity(ident);
} }