Merge "Add API to launch activities on secondary displays"
diff --git a/api/current.txt b/api/current.txt
index 2989145..f43ce6d 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -3892,6 +3892,7 @@
public class ActivityOptions {
method public android.graphics.Rect getLaunchBounds();
+ method public int getLaunchDisplayId();
method public static android.app.ActivityOptions makeBasic();
method public static android.app.ActivityOptions makeClipRevealAnimation(android.view.View, int, int, int, int);
method public static android.app.ActivityOptions makeCustomAnimation(android.content.Context, int, int);
@@ -3902,6 +3903,7 @@
method public static android.app.ActivityOptions makeThumbnailScaleUpAnimation(android.view.View, android.graphics.Bitmap, int, int);
method public void requestUsageTimeReport(android.app.PendingIntent);
method public android.app.ActivityOptions setLaunchBounds(android.graphics.Rect);
+ method public android.app.ActivityOptions setLaunchDisplayId(int);
method public android.os.Bundle toBundle();
method public void update(android.app.ActivityOptions);
field public static final java.lang.String EXTRA_USAGE_TIME_REPORT = "android.activity.usage_time";
diff --git a/api/system-current.txt b/api/system-current.txt
index ab5acf5..f813204 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -4024,6 +4024,7 @@
public class ActivityOptions {
method public android.graphics.Rect getLaunchBounds();
+ method public int getLaunchDisplayId();
method public static android.app.ActivityOptions makeBasic();
method public static android.app.ActivityOptions makeClipRevealAnimation(android.view.View, int, int, int, int);
method public static android.app.ActivityOptions makeCustomAnimation(android.content.Context, int, int);
@@ -4034,6 +4035,7 @@
method public static android.app.ActivityOptions makeThumbnailScaleUpAnimation(android.view.View, android.graphics.Bitmap, int, int);
method public void requestUsageTimeReport(android.app.PendingIntent);
method public android.app.ActivityOptions setLaunchBounds(android.graphics.Rect);
+ method public android.app.ActivityOptions setLaunchDisplayId(int);
method public android.os.Bundle toBundle();
method public void update(android.app.ActivityOptions);
field public static final java.lang.String EXTRA_USAGE_TIME_REPORT = "android.activity.usage_time";
diff --git a/api/test-current.txt b/api/test-current.txt
index 76af8c5..da025ff 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -3901,6 +3901,7 @@
public class ActivityOptions {
method public android.graphics.Rect getLaunchBounds();
+ method public int getLaunchDisplayId();
method public static android.app.ActivityOptions makeBasic();
method public static android.app.ActivityOptions makeClipRevealAnimation(android.view.View, int, int, int, int);
method public static android.app.ActivityOptions makeCustomAnimation(android.content.Context, int, int);
@@ -3911,6 +3912,7 @@
method public static android.app.ActivityOptions makeThumbnailScaleUpAnimation(android.view.View, android.graphics.Bitmap, int, int);
method public void requestUsageTimeReport(android.app.PendingIntent);
method public android.app.ActivityOptions setLaunchBounds(android.graphics.Rect);
+ method public android.app.ActivityOptions setLaunchDisplayId(int);
method public void setLaunchStackId(int);
method public android.os.Bundle toBundle();
method public void update(android.app.ActivityOptions);
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index d9a4690..1e7f4f0 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -18,6 +18,7 @@
import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT;
import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
+import static android.view.Display.INVALID_DISPLAY;
import android.annotation.Nullable;
import android.annotation.TestApi;
@@ -152,6 +153,12 @@
private static final String KEY_ANIM_SPECS = "android:activity.animSpecs";
/**
+ * The display id the activity should be launched into.
+ * @hide
+ */
+ private static final String KEY_LAUNCH_DISPLAY_ID = "android.activity.launchDisplayId";
+
+ /**
* The stack id the activity should be launched into.
* @hide
*/
@@ -240,6 +247,7 @@
private int mResultCode;
private int mExitCoordinatorIndex;
private PendingIntent mUsageTimeReport;
+ private int mLaunchDisplayId = INVALID_DISPLAY;
private int mLaunchStackId = INVALID_STACK_ID;
private int mLaunchTaskId = -1;
private int mDockCreateMode = DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT;
@@ -850,6 +858,7 @@
mExitCoordinatorIndex = opts.getInt(KEY_EXIT_COORDINATOR_INDEX);
break;
}
+ mLaunchDisplayId = opts.getInt(KEY_LAUNCH_DISPLAY_ID, INVALID_DISPLAY);
mLaunchStackId = opts.getInt(KEY_LAUNCH_STACK_ID, INVALID_STACK_ID);
mLaunchTaskId = opts.getInt(KEY_LAUNCH_TASK_ID, -1);
mTaskOverlay = opts.getBoolean(KEY_TASK_OVERLAY, false);
@@ -1015,6 +1024,25 @@
}
}
+ /**
+ * Gets the id of the display where activity should be launched.
+ * @return The id of the display where activity should be launched,
+ * {@link android.view.Display#INVALID_DISPLAY} if not set.
+ */
+ public int getLaunchDisplayId() {
+ return mLaunchDisplayId;
+ }
+
+ /**
+ * Sets the id of the display where activity should be launched.
+ * @param launchDisplayId The id of the display where the activity should be launched.
+ * @return {@code this} {@link ActivityOptions} instance.
+ */
+ public ActivityOptions setLaunchDisplayId(int launchDisplayId) {
+ mLaunchDisplayId = launchDisplayId;
+ return this;
+ }
+
/** @hide */
public int getLaunchStackId() {
return mLaunchStackId;
@@ -1209,6 +1237,7 @@
b.putInt(KEY_EXIT_COORDINATOR_INDEX, mExitCoordinatorIndex);
break;
}
+ b.putInt(KEY_LAUNCH_DISPLAY_ID, mLaunchDisplayId);
b.putInt(KEY_LAUNCH_STACK_ID, mLaunchStackId);
b.putInt(KEY_LAUNCH_TASK_ID, mLaunchTaskId);
b.putBoolean(KEY_TASK_OVERLAY, mTaskOverlay);
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 52ad72d..14b843a 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -80,6 +80,7 @@
import static android.app.ActivityManager.RESIZE_MODE_USER;
import static android.app.ActivityManager.StackId.DOCKED_STACK_ID;
import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
+import static android.view.Display.INVALID_DISPLAY;
final class ActivityManagerShellCommand extends ShellCommand {
public static final String NO_CLASS_ERROR_CODE = "Error type 3";
@@ -114,6 +115,7 @@
private String mProfileFile;
private int mSamplingInterval;
private boolean mAutoStop;
+ private int mDisplayId;
private int mStackId;
final boolean mDumping;
@@ -249,6 +251,7 @@
mSamplingInterval = 0;
mAutoStop = false;
mUserId = defUser;
+ mDisplayId = INVALID_DISPLAY;
mStackId = INVALID_STACK_ID;
return Intent.parseCommandArgs(this, new Intent.CommandOptionHandler() {
@@ -278,6 +281,8 @@
mUserId = UserHandle.parseUserArg(getNextArgRequired());
} else if (opt.equals("--receiver-permission")) {
mReceiverPermission = getNextArgRequired();
+ } else if (opt.equals("--display")) {
+ mDisplayId = Integer.parseInt(getNextArgRequired());
} else if (opt.equals("--stack")) {
mStackId = Integer.parseInt(getNextArgRequired());
} else {
@@ -354,6 +359,10 @@
int res;
final long startTime = SystemClock.uptimeMillis();
ActivityOptions options = null;
+ if (mDisplayId != INVALID_DISPLAY) {
+ options = ActivityOptions.makeBasic();
+ options.setLaunchDisplayId(mDisplayId);
+ }
if (mStackId != INVALID_STACK_ID) {
options = ActivityOptions.makeBasic();
options.setLaunchStackId(mStackId);
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 282ec50..1cbb52f 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -38,6 +38,8 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.FLAG_PRIVATE;
+import static android.view.Display.INVALID_DISPLAY;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ALL;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_CONTAINERS;
@@ -90,6 +92,7 @@
import static com.android.server.wm.AppTransition.TRANSIT_DOCK_TASK_FROM_RECENTS;
import android.Manifest;
+import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.Activity;
import android.app.ActivityManager;
@@ -1483,16 +1486,35 @@
Slog.w(TAG, message);
return false;
}
- if (options != null && options.getLaunchTaskId() != -1) {
- final int startInTaskPerm = mService.checkPermission(START_TASKS_FROM_RECENTS,
- callingPid, callingUid);
- if (startInTaskPerm != PERMISSION_GRANTED) {
- final String msg = "Permission Denial: starting " + intent.toString()
- + " from " + callerApp + " (pid=" + callingPid
- + ", uid=" + callingUid + ") with launchTaskId="
- + options.getLaunchTaskId();
- Slog.w(TAG, msg);
- throw new SecurityException(msg);
+ if (options != null) {
+ if (options.getLaunchTaskId() != INVALID_STACK_ID) {
+ final int startInTaskPerm = mService.checkPermission(START_TASKS_FROM_RECENTS,
+ callingPid, callingUid);
+ if (startInTaskPerm != PERMISSION_GRANTED) {
+ final String msg = "Permission Denial: starting " + intent.toString()
+ + " from " + callerApp + " (pid=" + callingPid
+ + ", uid=" + callingUid + ") with launchTaskId="
+ + options.getLaunchTaskId();
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+ }
+ }
+ // Check if someone tries to launch an activity on a private display with a different
+ // owner.
+ final int launchDisplayId = options.getLaunchDisplayId();
+ if (launchDisplayId != INVALID_DISPLAY) {
+ final ActivityDisplay activityDisplay = mActivityDisplays.get(launchDisplayId);
+ if (activityDisplay != null
+ && (activityDisplay.mDisplay.getFlags() & FLAG_PRIVATE) != 0) {
+ if (activityDisplay.mDisplay.getOwnerUid() != callingUid) {
+ final String msg = "Permission Denial: starting " + intent.toString()
+ + " from " + callerApp + " (pid=" + callingPid
+ + ", uid=" + callingUid + ") with launchDisplayId="
+ + launchDisplayId;
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+ }
+ }
}
}
@@ -1937,7 +1959,7 @@
}
ActivityStack getStack(int stackId, boolean createStaticStackIfNeeded, boolean createOnTop) {
- ActivityContainer activityContainer = mActivityContainers.get(stackId);
+ final ActivityContainer activityContainer = mActivityContainers.get(stackId);
if (activityContainer != null) {
return activityContainer.mStack;
}
@@ -1948,6 +1970,40 @@
return createStackOnDisplay(stackId, DEFAULT_DISPLAY, createOnTop);
}
+ /**
+ * Get a topmost stack on the display, that is a valid launch stack for specified activity.
+ * If there is no such stack, new dynamic stack can be created.
+ * @param displayId Target display.
+ * @param r Activity that should be launched there.
+ * @return Existing stack if there is a valid one, new dynamic stack if it is valid or null.
+ */
+ ActivityStack getValidLaunchStackOnDisplay(int displayId, @NonNull ActivityRecord r) {
+ final ActivityDisplay activityDisplay = mActivityDisplays.get(displayId);
+ if (activityDisplay == null) {
+ throw new IllegalArgumentException(
+ "Display with displayId=" + displayId + " not found.");
+ }
+
+ // Return the topmost valid stack on the display.
+ for (int i = activityDisplay.mStacks.size() - 1; i >= 0; --i) {
+ final ActivityStack stack = activityDisplay.mStacks.get(i);
+ if (mService.mActivityStarter.isValidLaunchStackId(stack.mStackId, r)) {
+ return stack;
+ }
+ }
+
+ // If there is no valid stack on the external display - check if new dynamic stack will do.
+ if (displayId != Display.DEFAULT_DISPLAY) {
+ final int newDynamicStackId = getNextStackId();
+ if (mService.mActivityStarter.isValidLaunchStackId(newDynamicStackId, r)) {
+ return createStackOnDisplay(newDynamicStackId, displayId, true /*onTop*/);
+ }
+ }
+
+ Slog.w(TAG, "getValidLaunchStackOnDisplay: can't launch on displayId " + displayId);
+ return null;
+ }
+
ArrayList<ActivityStack> getStacks() {
ArrayList<ActivityStack> allStacks = new ArrayList<>();
for (int displayNdx = mActivityDisplays.size() - 1; displayNdx >= 0; --displayNdx) {
@@ -2326,11 +2382,15 @@
* Restores a recent task to a stack
* @param task The recent task to be restored.
* @param stackId The stack to restore the task to (default launch stack will be used
- * if stackId is {@link android.app.ActivityManager.StackId#INVALID_STACK_ID}).
+ * if stackId is {@link android.app.ActivityManager.StackId#INVALID_STACK_ID}
+ * or is not a static stack).
* @return true if the task has been restored successfully.
*/
private boolean restoreRecentTaskLocked(TaskRecord task, int stackId) {
- if (stackId == INVALID_STACK_ID) {
+ if (!StackId.isStaticStack(stackId)) {
+ // If stack is not static (or stack id is invalid) - use the default one.
+ // This means that tasks that were on external displays will be restored on the
+ // primary display.
stackId = task.getLaunchStackId();
} else if (stackId == DOCKED_STACK_ID && !task.canGoInDockedStack()) {
// Preferred stack is the docked stack, but the task can't go in the docked stack.
diff --git a/services/core/java/com/android/server/am/ActivityStarter.java b/services/core/java/com/android/server/am/ActivityStarter.java
index 691d6b9..c96e74f 100644
--- a/services/core/java/com/android/server/am/ActivityStarter.java
+++ b/services/core/java/com/android/server/am/ActivityStarter.java
@@ -31,6 +31,7 @@
import static android.app.ActivityManager.StackId.HOME_STACK_ID;
import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
+import static android.app.ActivityManager.StackId.isStaticStack;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
@@ -50,6 +51,8 @@
import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;
import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK;
import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TOP;
+import static android.view.Display.INVALID_DISPLAY;
+
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_CONFIGURATION;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_FOCUS;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW;
@@ -1952,12 +1955,14 @@
// The fullscreen stack can contain any task regardless of if the task is resizeable
// or not. So, we let the task go in the fullscreen task if it is the focus stack.
+ // Same also applies to dynamic stacks, as they behave similar to fullscreen stack.
// If the freeform or docked stack has focus, and the activity to be launched is resizeable,
// we can also put it in the focused stack.
final int focusedStackId = mSupervisor.mFocusedStack.mStackId;
final boolean canUseFocusedStack = focusedStackId == FULLSCREEN_WORKSPACE_STACK_ID
|| (focusedStackId == DOCKED_STACK_ID && r.canGoInDockedStack())
- || (focusedStackId == FREEFORM_WORKSPACE_STACK_ID && r.isResizeableOrForced());
+ || (focusedStackId == FREEFORM_WORKSPACE_STACK_ID && r.isResizeableOrForced())
+ || !isStaticStack(focusedStackId);
if (canUseFocusedStack && (!newTask
|| mSupervisor.mFocusedStack.mActivityContainer.isEligibleForNewTasks())) {
if (DEBUG_FOCUS || DEBUG_STACK) Slog.d(TAG_FOCUS,
@@ -1965,7 +1970,7 @@
return mSupervisor.mFocusedStack;
}
- // We first try to put the task in the first dynamic stack.
+ // We first try to put the task in the first dynamic stack on home display.
final ArrayList<ActivityStack> homeDisplayStacks = mSupervisor.mHomeStack.mStacks;
for (int stackNdx = homeDisplayStacks.size() - 1; stackNdx >= 0; --stackNdx) {
stack = homeDisplayStacks.get(stackNdx);
@@ -1994,16 +1999,29 @@
return mReuseTask.getStack();
}
+ final int launchDisplayId =
+ (aOptions != null) ? aOptions.getLaunchDisplayId() : INVALID_DISPLAY;
+
final int launchStackId =
(aOptions != null) ? aOptions.getLaunchStackId() : INVALID_STACK_ID;
+ if (launchStackId != INVALID_STACK_ID && launchDisplayId != INVALID_DISPLAY) {
+ throw new IllegalArgumentException(
+ "Stack and display id can't be set at the same time.");
+ }
+
if (isValidLaunchStackId(launchStackId, r)) {
return mSupervisor.getStack(launchStackId, CREATE_IF_NEEDED, ON_TOP);
- } else if (launchStackId == DOCKED_STACK_ID) {
+ }
+ if (launchStackId == DOCKED_STACK_ID) {
// The preferred launch stack is the docked stack, but it isn't a valid launch stack
// for this activity, so we put the activity in the fullscreen stack.
return mSupervisor.getStack(FULLSCREEN_WORKSPACE_STACK_ID, CREATE_IF_NEEDED, ON_TOP);
}
+ if (launchDisplayId != INVALID_DISPLAY) {
+ // Stack id has higher priority than display id.
+ return mSupervisor.getValidLaunchStackOnDisplay(launchDisplayId, r);
+ }
if ((launchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) == 0) {
return null;
@@ -2047,9 +2065,8 @@
}
}
- private boolean isValidLaunchStackId(int stackId, ActivityRecord r) {
- if (stackId == INVALID_STACK_ID || stackId == HOME_STACK_ID
- || !StackId.isStaticStack(stackId)) {
+ boolean isValidLaunchStackId(int stackId, ActivityRecord r) {
+ if (stackId == INVALID_STACK_ID || stackId == HOME_STACK_ID) {
return false;
}