diff --git a/core/api/test-current.txt b/core/api/test-current.txt index fb2c9e43497f1..e08cef92fc6c6 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -121,6 +121,7 @@ package android.app { public class ActivityManager { method @RequiresPermission(android.Manifest.permission.SET_ACTIVITY_WATCHER) public void addHomeVisibilityListener(@NonNull java.util.concurrent.Executor, @NonNull android.app.HomeVisibilityListener); method public void alwaysShowUnsupportedCompileSdkWarning(android.content.ComponentName); + method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public int[] getSecondaryDisplayIdsForStartingBackgroundUsers(); method public long getTotalRam(); method @RequiresPermission(allOf={android.Manifest.permission.PACKAGE_USAGE_STATS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}, conditional=true) public int getUidProcessCapabilities(int); method @RequiresPermission(allOf={android.Manifest.permission.PACKAGE_USAGE_STATS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}, conditional=true) public int getUidProcessState(int); diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index da2a76a7b3196..576b572dcc9ac 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -4387,13 +4387,15 @@ public class ActivityManager { * a profile, the {@link #getCurrentUser()}, the {@link UserHandle#SYSTEM system user}, or * does not exist. * - * @param displayId id of the display, it must exist. + * @param displayId id of the display. * * @return whether the operation succeeded. Notice that if the user was already started in such * display before, it will return {@code false}. * * @throws UnsupportedOperationException if the device does not support background users on * secondary displays. + * @throws IllegalArgumentException if the display doesn't exist or is not a valid display to + * start secondary users on. * * @hide */ @@ -4413,6 +4415,24 @@ public class ActivityManager { } } + /** + * Gets the id of displays that can be used by + * {@link #startUserInBackgroundOnSecondaryDisplay(int, int)}. + * + * @hide + */ + @TestApi + @Nullable + @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS, + android.Manifest.permission.INTERACT_ACROSS_USERS}) + public int[] getSecondaryDisplayIdsForStartingBackgroundUsers() { + try { + return getService().getSecondaryDisplayIdsForStartingBackgroundUsers(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Gets the message that is shown when a user is switched from. * diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 980b79ba01e87..b4abd3c69482b 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -770,4 +770,10 @@ interface IActivityManager { "@android.annotation.RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}, conditional = true)") boolean startUserInBackgroundOnSecondaryDisplay(int userid, int displayId); + /** + * Gets the ids of displays that can be used on {@link #startUserInBackgroundOnSecondaryDisplay(int userId, int displayId)}. + * + *
Typically used only by automotive builds when the vehicle has multiple displays.
+ */
+ @nullable int[] getSecondaryDisplayIdsForStartingBackgroundUsers();
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 50b47a6b22f14..7d85c1333b1b3 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -22,6 +22,7 @@ import static android.Manifest.permission.FILTER_EVENTS;
import static android.Manifest.permission.INTERACT_ACROSS_USERS;
import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
import static android.Manifest.permission.MANAGE_ACTIVITY_TASKS;
+import static android.Manifest.permission.MANAGE_USERS;
import static android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND;
import static android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND;
import static android.app.ActivityManager.INSTR_FLAG_ALWAYS_CHECK_SIGNATURE;
@@ -258,6 +259,7 @@ import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerInternal;
import android.media.audiofx.AudioEffect;
import android.net.ConnectivityManager;
@@ -332,6 +334,7 @@ import android.util.SparseIntArray;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
import android.util.proto.ProtoUtils;
+import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@@ -2366,6 +2369,7 @@ public class ActivityManagerService extends IActivityManager.Stub
mUiHandler = injector.getUiHandler(null /* service */);
mUidObserverController = new UidObserverController(mUiHandler);
mUserController = new UserController(this);
+ mInjector.mUserController = mUserController;
mPendingIntentController =
new PendingIntentController(handlerThread.getLooper(), mUserController, mConstants);
mAppRestrictionController = new AppRestrictionController(mContext, this);
@@ -2480,6 +2484,7 @@ public class ActivityManagerService extends IActivityManager.Stub
mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class);
mUserController = new UserController(this);
+ mInjector.mUserController = mUserController;
mPendingIntentController = new PendingIntentController(
mHandlerThread.getLooper(), mUserController, mConstants);
@@ -6057,6 +6062,24 @@ public class ActivityManagerService extends IActivityManager.Stub
* This can be called with or without the global lock held.
*/
@PermissionMethod
+ private void enforceCallingHasAtLeastOnePermission(String func, String... permissions) {
+ for (String permission : permissions) {
+ if (checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED) {
+ return;
+ }
+ }
+
+ String msg = "Permission Denial: " + func + " from pid="
+ + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid()
+ + " requires one of " + Arrays.toString(permissions);
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+ }
+
+ /**
+ * This can be called with or without the global lock held.
+ */
void enforcePermission(String permission, int pid, int uid, String func) {
if (checkPermission(permission, pid, uid) == PackageManager.PERMISSION_GRANTED) {
return;
@@ -16333,8 +16356,34 @@ public class ActivityManagerService extends IActivityManager.Stub
@Override
public boolean startUserInBackgroundOnSecondaryDisplay(int userId, int displayId) {
+ int[] displayIds = getSecondaryDisplayIdsForStartingBackgroundUsers();
+ boolean validDisplay = false;
+ if (displayIds != null) {
+ for (int i = 0; i < displayIds.length; i++) {
+ if (displayId == displayIds[i]) {
+ validDisplay = true;
+ break;
+ }
+ }
+ }
+ if (!validDisplay) {
+ throw new IllegalArgumentException("Invalid display (" + displayId + ") to start user. "
+ + "Valid options are: " + Arrays.toString(displayIds));
+ }
+
+ if (DEBUG_MU) {
+ Slogf.d(TAG_MU, "Calling startUserOnSecondaryDisplay(%d, %d) using injector %s", userId,
+ displayId, mInjector);
+ }
// Permission check done inside UserController.
- return mUserController.startUserOnSecondaryDisplay(userId, displayId);
+ return mInjector.startUserOnSecondaryDisplay(userId, displayId);
+ }
+
+ @Override
+ public int[] getSecondaryDisplayIdsForStartingBackgroundUsers() {
+ enforceCallingHasAtLeastOnePermission("getSecondaryDisplayIdsForStartingBackgroundUsers()",
+ MANAGE_USERS, INTERACT_ACROSS_USERS);
+ return mInjector.getSecondaryDisplayIdsForStartingBackgroundUsers();
}
/**
@@ -18343,8 +18392,10 @@ public class ActivityManagerService extends IActivityManager.Stub
@VisibleForTesting
public static class Injector {
+ private final Context mContext;
private NetworkManagementInternal mNmi;
- private Context mContext;
+
+ private UserController mUserController;
public Injector(Context context) {
mContext = context;
@@ -18369,6 +18420,103 @@ public class ActivityManagerService extends IActivityManager.Stub
return false;
}
+ /**
+ * Called by {@code AMS.getSecondaryDisplayIdsForStartingBackgroundUsers()}.
+ */
+ // NOTE: ideally Injector should have no complex logic, but if this logic was moved to AMS,
+ // it could not be tested with the existing ActivityManagerServiceTest (as DisplayManager,
+ // DisplayInfo, etc... are final and UserManager.isUsersOnSecondaryDisplaysEnabled is
+ // static).
+ // So, the logic was added here, and tested on ActivityManagerServiceInjectorTest (which
+ // was added on FrameworksMockingServicesTests and hence uses Extended Mockito to mock
+ // final and static stuff)
+ @Nullable
+ public int[] getSecondaryDisplayIdsForStartingBackgroundUsers() {
+ if (!UserManager.isUsersOnSecondaryDisplaysEnabled()) {
+ Slogf.w(TAG, "getSecondaryDisplayIdsForStartingBackgroundUsers(): not supported");
+ return null;
+ }
+
+ // NOTE: DisplayManagerInternal doesn't have a method to list all displays
+ DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+
+ Display[] allDisplays = displayManager.getDisplays();
+
+ // allDisplays should contain at least Display.DEFAULT_DISPLAY, but it's better to
+ // double check, just in case...
+ if (allDisplays == null || allDisplays.length == 0) {
+ Slogf.wtf(TAG, "displayManager (%s) returned no displays", displayManager);
+ return null;
+ }
+ boolean hasDefaultDisplay = false;
+ for (Display display : allDisplays) {
+ if (display.getDisplayId() == Display.DEFAULT_DISPLAY) {
+ hasDefaultDisplay = true;
+ break;
+ }
+ }
+ if (!hasDefaultDisplay) {
+ Slogf.wtf(TAG, "displayManager (%s) has %d displays (%s), but none has id "
+ + "DEFAULT_DISPLAY (%d)", displayManager, allDisplays.length,
+ Arrays.toString(allDisplays), Display.DEFAULT_DISPLAY);
+ return null;
+ }
+
+ // Starts with all displays but DEFAULT_DISPLAY
+ int[] displayIds = new int[allDisplays.length - 1];
+
+ // TODO(b/247592632): check for other properties like isSecure or proper display type
+ int numberValidDisplays = 0;
+ for (Display display : allDisplays) {
+ int displayId = display.getDisplayId();
+ if (display.isValid() && displayId != Display.DEFAULT_DISPLAY) {
+ displayIds[numberValidDisplays++] = displayId;
+ }
+ }
+
+ if (numberValidDisplays == 0) {
+ // TODO(b/247580038): remove this workaround once a virtual display on Car's
+ // KitchenSink (or other app) can be used while running CTS tests on devices that
+ // don't have a real display.
+ // STOPSHIP: if not removed, it should at least be unit tested
+ String testingProp = "fw.secondary_display_for_starting_users_for_testing_purposes";
+ int displayId = SystemProperties.getInt(testingProp, Display.DEFAULT_DISPLAY);
+ if (displayId != Display.DEFAULT_DISPLAY && displayId > 0) {
+ Slogf.w(TAG, "getSecondaryDisplayIdsForStartingBackgroundUsers(): no valid "
+ + "display found, but returning %d as set by property %s", displayId,
+ testingProp);
+ return new int[] { displayId };
+ }
+ Slogf.e(TAG, "getSecondaryDisplayIdsForStartingBackgroundUsers(): no valid display"
+ + " on %s", Arrays.toString(allDisplays));
+ return null;
+ }
+
+ if (numberValidDisplays != displayIds.length) {
+ int[] validDisplayIds = new int[numberValidDisplays];
+ System.arraycopy(displayIds, 0, validDisplayIds, 0, numberValidDisplays);
+ if (DEBUG_MU) {
+ Slogf.d(TAG, "getSecondaryDisplayIdsForStartingBackgroundUsers(): returning "
+ + "only valid displays (%d instead of %d): %s", numberValidDisplays,
+ displayIds.length, Arrays.toString(validDisplayIds));
+ }
+ return validDisplayIds;
+ }
+
+ if (DEBUG_MU) {
+ Slogf.d(TAG, "getSecondaryDisplayIdsForStartingBackgroundUsers(): returning all "
+ + "(but DEFAULT_DISPLAY) displays : %s", Arrays.toString(displayIds));
+ }
+ return displayIds;
+ }
+
+ /**
+ * Called by {@code AMS.startUserOnSecondaryDisplay()}.
+ */
+ public boolean startUserOnSecondaryDisplay(int userId, int displayId) {
+ return mUserController.startUserOnSecondaryDisplay(userId, displayId);
+ }
+
/**
* Return the process list instance
*/
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index b4f6e35b3df36..10e2aae9a8d14 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -367,6 +367,8 @@ final class ActivityManagerShellCommand extends ShellCommand {
return runGetCurrentForegroundProcess(pw, mInternal, mTaskInterface);
case "reset-dropbox-rate-limiter":
return runResetDropboxRateLimiter();
+ case "list-secondary-displays-for-starting-users":
+ return runListSecondaryDisplaysForStartingUsers(pw);
default:
return handleDefaultCommands(cmd);
}
@@ -2068,6 +2070,10 @@ final class ActivityManagerShellCommand extends ShellCommand {
success = mInterface.startUserInBackgroundWithListener(userId, waiter);
displaySuffix = "";
} else {
+ if (!UserManager.isUsersOnSecondaryDisplaysEnabled()) {
+ pw.println("Not supported");
+ return -1;
+ }
success = mInterface.startUserInBackgroundOnSecondaryDisplay(userId, displayId);
displaySuffix = " on display " + displayId;
}
@@ -3591,6 +3597,14 @@ final class ActivityManagerShellCommand extends ShellCommand {
return 0;
}
+ int runListSecondaryDisplaysForStartingUsers(PrintWriter pw) throws RemoteException {
+ int[] displayIds = mInterface.getSecondaryDisplayIdsForStartingBackgroundUsers();
+ pw.println(displayIds == null || displayIds.length == 0
+ ? "none"
+ : Arrays.toString(displayIds));
+ return 0;
+ }
+
private Resources getResources(PrintWriter pw) throws RemoteException {
// system resources does not contain all the device configuration, construct it manually.
Configuration config = mInterface.getConfiguration();
@@ -3951,6 +3965,9 @@ final class ActivityManagerShellCommand extends ShellCommand {
pw.println(" Set an app's background restriction level which in turn map to a app standby bucket.");
pw.println(" get-bg-restriction-level [--user