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,
String callingFeatureId, in ComponentName component, in Rect sourceBounds,
in Bundle opts, in UserHandle user);
PendingIntent getActivityLaunchIntent(in ComponentName component, in Bundle opts,
PendingIntent getActivityLaunchIntent(String callingPackage, in ComponentName component,
in UserHandle user);
void showAppDetailsAsUser(in IApplicationThread caller, String callingPackage,
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
* {@link #startMainActivity(ComponentName, UserHandle, Rect, Bundle)}.
* Returns a mutable PendingIntent that would start the same activity started from
* {@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 startActivityOptions This parameter is no longer supported
* @param user The UserHandle of the profile
* @hide
*/
@RequiresPermission(android.Manifest.permission.START_TASKS_FROM_RECENTS)
@Nullable
public PendingIntent getMainActivityLaunchIntent(@NonNull ComponentName component,
@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);
if (DEBUG) {
Log.i(TAG, "GetMainActivityLaunchIntent " + component + " " + user);
}
try {
// due to b/209607104, startActivityOptions will be ignored
return mService.getActivityLaunchIntent(component, null /* opts */, user);
return mService.getActivityLaunchIntent(mContext.getPackageName(), component, user);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}

View File

@@ -45,10 +45,12 @@ 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;
@@ -62,9 +64,11 @@ 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.SplitScreenConstants.SplitPosition;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController;
import java.lang.annotation.Retention;
@@ -106,11 +110,18 @@ public class DragAndDropPolicy {
*/
void start(DisplayLayout displayLayout, ClipData data, InstanceId 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
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.
*/
@@ -248,32 +259,68 @@ public class DragAndDropPolicy {
final UserHandle user = intent.getParcelableExtra(EXTRA_USER);
mStarter.startShortcut(packageName, id, position, opts, user);
} else {
mStarter.startIntent(intent.getParcelableExtra(EXTRA_PENDING_INTENT),
null, position, opts);
final PendingIntent launchIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT);
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.
*/
private static class DragSession {
private final Context mContext;
private final ActivityTaskManager mActivityTaskManager;
private final ClipData mInitialDragData;
final DisplayLayout displayLayout;
Intent dragData;
int runningTaskId;
ActivityManager.RunningTaskInfo runningTaskInfo;
@WindowConfiguration.WindowingMode
int runningTaskWinMode = WINDOWING_MODE_UNDEFINED;
@WindowConfiguration.ActivityType
int runningTaskActType = ACTIVITY_TYPE_STANDARD;
boolean runningTaskIsResizeable;
boolean dragItemSupportsSplitscreen;
DragSession(Context context, ActivityTaskManager activityTaskManager,
DragSession(ActivityTaskManager activityTaskManager,
DisplayLayout dispLayout, ClipData data) {
mContext = context;
mActivityTaskManager = activityTaskManager;
mInitialDragData = data;
displayLayout = dispLayout;
@@ -287,10 +334,9 @@ public class DragAndDropPolicy {
mActivityTaskManager.getTasks(1, false /* filterOnlyVisibleRecents */);
if (!tasks.isEmpty()) {
final ActivityManager.RunningTaskInfo task = tasks.get(0);
runningTaskInfo = task;
runningTaskWinMode = task.getWindowingMode();
runningTaskActType = task.getActivityType();
runningTaskId = task.taskId;
runningTaskIsResizeable = task.isResizeable;
}
final ActivityInfo info = mInitialDragData.getItemAt(0).getActivityInfo();

View File

@@ -26,7 +26,6 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.StatusBarManager;
import android.content.ClipData;
import android.content.Context;
@@ -35,7 +34,6 @@ import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.RemoteException;
import android.view.DragEvent;
import android.view.SurfaceControl;
import android.view.WindowInsets;
@@ -51,7 +49,6 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController;
import java.util.ArrayList;
import java.util.List;
/**
* Coordinates the visible drop targets for the current drag.
@@ -166,17 +163,8 @@ public class DragLayout extends LinearLayout {
boolean alreadyInSplit = mSplitScreenController != null
&& mSplitScreenController.isSplitScreenVisible();
if (!alreadyInSplit) {
List<ActivityManager.RunningTaskInfo> tasks = null;
// Figure out the splashscreen info for the existing task.
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);
ActivityManager.RunningTaskInfo taskInfo1 = mPolicy.getLatestRunningTask();
if (taskInfo1 != null) {
Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo);
int bgColor1 = getResizingBackgroundColor(taskInfo1);
mDropZoneView1.setAppInfo(bgColor1, icon1);

View File

@@ -22,7 +22,7 @@ import android.content.Context;
import android.hardware.display.DisplayManager;
import android.testing.TestableContext;
import androidx.test.InstrumentationRegistry;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.After;
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_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_TOP_OR_LEFT;
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_TOP;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
@@ -50,9 +53,11 @@ import android.app.ActivityTaskManager;
import android.app.PendingIntent;
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.ResolveInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Insets;
@@ -177,6 +182,12 @@ public class DragAndDropPolicyTest {
info.configuration.windowConfiguration.setActivityType(actType);
info.configuration.windowConfiguration.setWindowingMode(winMode);
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;
}
@@ -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) {
for (Target t : targets) {
if (type == t.type) {

View File

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

View File

@@ -45,8 +45,8 @@ import android.window.DisplayAreaOrganizer;
import android.window.IWindowContainerToken;
import android.window.WindowContainerToken;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.wm.shell.common.DisplayController;
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.PendingIntent.FLAG_IMMUTABLE;
import static android.app.PendingIntent.FLAG_MUTABLE;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
@@ -1218,8 +1219,9 @@ public class LauncherAppsService extends SystemService {
}
@Override
public PendingIntent getActivityLaunchIntent(ComponentName component, Bundle opts,
public PendingIntent getActivityLaunchIntent(String callingPackage, ComponentName component,
UserHandle user) {
ensureShortcutPermission(callingPackage);
if (!canAccessProfile(user.getIdentifier(), "Cannot start activity")) {
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
// the calling user
return PendingIntent.getActivityAsUser(mContext, 0 /* requestCode */, launchIntent,
FLAG_IMMUTABLE, null /* options */, user);
FLAG_MUTABLE, null /* opts */, user);
} finally {
Binder.restoreCallingIdentity(ident);
}