Added option to pre-create user templates to optimize first user creation time.

Initial user creation is slow because the system must prepare per-user data (like storage and
permissions) whose cost is proportional to the number of pre-installed apps. On automovive's
reference implementation, it can take more than 10s, which is a bad user experience.

This change lets OEMs pre-create some users , so that high initial-creation cost is "paid" during
the initial boot. On automotive, it improves the creation of an additional user (or guest user)
in about 7s (from ~17s to 9s).

Bug: 111451156
Bug: 132111956
Bug: 140750212
Bug: 140868593

Test: manual verification
Test: atest FrameworksServicesTests:UserControllerTest#testStartTemplateUser_background

Change-Id: I81de1b5376dc9c42b63be8853d7204c88826401f
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index dace598..ca273ba 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -41,6 +41,7 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ShortcutServiceInternal;
 import android.content.pm.UserInfo;
+import android.content.pm.UserInfo.UserInfoFlag;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.os.Binder;
@@ -158,6 +159,7 @@
     private static final String ATTR_SERIAL_NO = "serialNumber";
     private static final String ATTR_NEXT_SERIAL_NO = "nextSerialNumber";
     private static final String ATTR_PARTIAL = "partial";
+    private static final String ATTR_PRE_CREATED = "preCreated";
     private static final String ATTR_GUEST_TO_REMOVE = "guestToRemove";
     private static final String ATTR_USER_VERSION = "version";
     private static final String ATTR_PROFILE_GROUP_ID = "profileGroupId";
@@ -597,7 +599,8 @@
             final int userSize = mUsers.size();
             for (int i = 0; i < userSize; i++) {
                 UserInfo ui = mUsers.valueAt(i).info;
-                if ((ui.partial || ui.guestToRemove || ui.isEphemeral()) && i != 0) {
+                if ((ui.partial || ui.guestToRemove || (ui.isEphemeral() && !ui.preCreated))
+                        && i != 0) {
                     partials.add(ui);
                     addRemovingUserIdLocked(ui.id);
                     ui.partial = true;
@@ -662,18 +665,23 @@
 
     @Override
     public @NonNull List<UserInfo> getUsers(boolean excludeDying) {
+        return getUsers(/*excludePartial= */ true, excludeDying, /* excludePreCreated= */ true);
+    }
+
+    private @NonNull List<UserInfo> getUsers(boolean excludePartial, boolean excludeDying,
+            boolean excludePreCreated) {
         checkManageOrCreateUsersPermission("query users");
         synchronized (mUsersLock) {
             ArrayList<UserInfo> users = new ArrayList<UserInfo>(mUsers.size());
             final int userSize = mUsers.size();
             for (int i = 0; i < userSize; i++) {
                 UserInfo ui = mUsers.valueAt(i).info;
-                if (ui.partial) {
+                if ((excludePartial && ui.partial)
+                        || (excludeDying && mRemovingUserIds.get(ui.id))
+                        || (excludePreCreated && ui.preCreated)) {
                     continue;
                 }
-                if (!excludeDying || !mRemovingUserIds.get(ui.id)) {
-                    users.add(userWithName(ui));
-                }
+                users.add(userWithName(ui));
             }
             return users;
         }
@@ -1196,7 +1204,7 @@
 
     private void checkManageOrInteractPermIfCallerInOtherProfileGroup(@UserIdInt int userId,
             String name) {
-        int callingUserId = UserHandle.getCallingUserId();
+        final int callingUserId = UserHandle.getCallingUserId();
         if (callingUserId == userId || isSameProfileGroupNoChecks(callingUserId, userId) ||
                 hasManageUsersPermission()) {
             return;
@@ -1210,7 +1218,7 @@
 
     @Override
     public boolean isDemoUser(@UserIdInt int userId) {
-        int callingUserId = UserHandle.getCallingUserId();
+        final int callingUserId = UserHandle.getCallingUserId();
         if (callingUserId != userId && !hasManageUsersPermission()) {
             throw new SecurityException("You need MANAGE_USERS permission to query if u=" + userId
                     + " is a demo user");
@@ -1222,6 +1230,19 @@
     }
 
     @Override
+    public boolean isPreCreated(@UserIdInt int userId) {
+        final int callingUserId = UserHandle.getCallingUserId();
+        if (callingUserId != userId && !hasManageUsersPermission()) {
+            throw new SecurityException("You need MANAGE_USERS permission to query if u=" + userId
+                    + " is pre-created");
+        }
+        synchronized (mUsersLock) {
+            UserInfo userInfo = getUserInfoLU(userId);
+            return userInfo != null && userInfo.preCreated;
+        }
+    }
+
+    @Override
     public boolean isRestricted() {
         synchronized (mUsersLock) {
             return getUserInfoLU(UserHandle.getCallingUserId()).isRestricted();
@@ -1871,7 +1892,7 @@
         // Skip over users being removed
         for (int i = 0; i < totalUserCount; i++) {
             UserInfo user = mUsers.valueAt(i).info;
-            if (!mRemovingUserIds.get(user.id) && !user.isGuest()) {
+            if (!mRemovingUserIds.get(user.id) && !user.isGuest() && !user.preCreated) {
                 aliveUserCount++;
             }
         }
@@ -2362,6 +2383,9 @@
         if (userInfo.partial) {
             serializer.attribute(null, ATTR_PARTIAL, "true");
         }
+        if (userInfo.preCreated) {
+            serializer.attribute(null, ATTR_PRE_CREATED, "true");
+        }
         if (userInfo.guestToRemove) {
             serializer.attribute(null, ATTR_GUEST_TO_REMOVE, "true");
         }
@@ -2518,6 +2542,7 @@
         int profileBadge = 0;
         int restrictedProfileParentId = UserInfo.NO_PROFILE_GROUP_ID;
         boolean partial = false;
+        boolean preCreated = false;
         boolean guestToRemove = false;
         boolean persistSeedData = false;
         String seedAccountName = null;
@@ -2562,6 +2587,10 @@
             if ("true".equals(valueString)) {
                 partial = true;
             }
+            valueString = parser.getAttributeValue(null, ATTR_PRE_CREATED);
+            if ("true".equals(valueString)) {
+                preCreated = true;
+            }
             valueString = parser.getAttributeValue(null, ATTR_GUEST_TO_REMOVE);
             if ("true".equals(valueString)) {
                 guestToRemove = true;
@@ -2615,6 +2644,7 @@
         userInfo.lastLoggedInTime = lastLoggedInTime;
         userInfo.lastLoggedInFingerprint = lastLoggedInFingerprint;
         userInfo.partial = partial;
+        userInfo.preCreated = preCreated;
         userInfo.guestToRemove = guestToRemove;
         userInfo.profileGroupId = profileGroupId;
         userInfo.profileBadge = profileBadge;
@@ -2686,7 +2716,8 @@
     public UserInfo createProfileForUserEvenWhenDisallowed(String name, int flags,
             @UserIdInt int userId, String[] disallowedPackages) {
         checkManageOrCreateUsersPermission(flags);
-        return createUserInternalUnchecked(name, flags, userId, disallowedPackages);
+        return createUserInternalUnchecked(name, flags, userId, /* preCreate= */ false,
+                disallowedPackages);
     }
 
     @Override
@@ -2701,12 +2732,25 @@
         return createUserInternal(name, flags, UserHandle.USER_NULL);
     }
 
-    private UserInfo createUserInternal(String name, int flags, int parentId) {
+    @Override
+    public UserInfo preCreateUser(int flags) {
+        checkManageOrCreateUsersPermission(flags);
+
+        Preconditions.checkArgument(!UserInfo.isManagedProfile(flags),
+                "cannot pre-create managed profiles");
+
+        return createUserInternalUnchecked(/* name= */ null, flags,
+                /* parentId= */ UserHandle.USER_NULL, /* preCreate= */ true,
+                /* disallowedPackages= */ null);
+    }
+
+    private UserInfo createUserInternal(@Nullable String name, @UserInfoFlag int flags,
+            @UserIdInt int parentId) {
         return createUserInternal(name, flags, parentId, null);
     }
 
-    private UserInfo createUserInternal(String name, int flags, int parentId,
-            String[] disallowedPackages) {
+    private UserInfo createUserInternal(@Nullable String name, @UserInfoFlag int flags,
+            @UserIdInt int parentId, @Nullable String[] disallowedPackages) {
         String restriction = ((flags & UserInfo.FLAG_MANAGED_PROFILE) != 0)
                 ? UserManager.DISALLOW_ADD_MANAGED_PROFILE
                 : UserManager.DISALLOW_ADD_USER;
@@ -2714,21 +2758,56 @@
             Log.w(LOG_TAG, "Cannot add user. " + restriction + " is enabled.");
             return null;
         }
-        return createUserInternalUnchecked(name, flags, parentId, disallowedPackages);
+        return createUserInternalUnchecked(name, flags, parentId, /* preCreate= */ false,
+                disallowedPackages);
     }
 
-    private UserInfo createUserInternalUnchecked(@Nullable String name, int flags,
-            int parentId, @Nullable String[] disallowedPackages) {
-        TimingsTraceAndSlog t = new TimingsTraceAndSlog();
-        t.traceBegin("createUser");
-        UserInfo userInfo =
-                createUserInternalUncheckedNoTracing(name, flags, parentId, disallowedPackages, t);
-        t.traceEnd();
-        return userInfo;
+    private UserInfo createUserInternalUnchecked(@Nullable String name, @UserInfoFlag int flags,
+            @UserIdInt int parentId, boolean preCreate,
+            @Nullable String[] disallowedPackages) {
+        final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
+        t.traceBegin("createUser-" + flags);
+        try {
+            return createUserInternalUncheckedNoTracing(name, flags, parentId, preCreate,
+                disallowedPackages, t);
+        } finally {
+            t.traceEnd();
+        }
     }
 
-    private UserInfo createUserInternalUncheckedNoTracing(@Nullable String name, int flags,
-            int parentId, @Nullable String[] disallowedPackages, @NonNull TimingsTraceAndSlog t) {
+    private UserInfo createUserInternalUncheckedNoTracing(@Nullable String name,
+            @UserInfoFlag int flags, @UserIdInt int parentId, boolean preCreate,
+            @Nullable String[] disallowedPackages, @NonNull TimingsTraceAndSlog t) {
+
+        // First try to use a pre-created user (if available).
+        // NOTE: currently we don't support pre-created managed profiles
+        if (!preCreate && (parentId < 0 && !UserInfo.isManagedProfile(flags))) {
+            final UserData preCreatedUserData;
+            synchronized (mUsersLock) {
+                preCreatedUserData = getPreCreatedUserLU(flags);
+            }
+            if (preCreatedUserData != null) {
+                final UserInfo preCreatedUser = preCreatedUserData.info;
+                Log.i(LOG_TAG, "Reusing pre-created user " + preCreatedUser.id + " for flags + "
+                        + UserInfo.flagsToString(flags));
+                if (DBG) {
+                    Log.d(LOG_TAG, "pre-created user flags: "
+                            + UserInfo.flagsToString(preCreatedUser.flags)
+                            + " new-user flags: " + UserInfo.flagsToString(flags));
+                }
+                preCreatedUser.name = name;
+                preCreatedUser.preCreated = false;
+                preCreatedUser.creationTime = getCreationTime();
+
+                dispatchUserAddedIntent(preCreatedUser);
+
+                writeUserLP(preCreatedUserData);
+                writeUserListLP();
+
+                return preCreatedUser;
+            }
+        }
+
         DeviceStorageMonitorInternal dsm = LocalServices
                 .getService(DeviceStorageMonitorInternal.class);
         if (dsm.isMemoryLow()) {
@@ -2736,8 +2815,8 @@
             return null;
         }
 
-        final boolean isGuest = (flags & UserInfo.FLAG_GUEST) != 0;
-        final boolean isManagedProfile = (flags & UserInfo.FLAG_MANAGED_PROFILE) != 0;
+        final boolean isGuest = UserInfo.isGuest(flags);
+        final boolean isManagedProfile = UserInfo.isManagedProfile(flags);
         final boolean isRestricted = (flags & UserInfo.FLAG_RESTRICTED) != 0;
         final boolean isDemo = (flags & UserInfo.FLAG_DEMO) != 0;
         final long ident = Binder.clearCallingIdentity();
@@ -2758,8 +2837,8 @@
                     return null;
                 }
                 if (!isGuest && !isManagedProfile && !isDemo && isUserLimitReached()) {
-                    // If we're not adding a guest/demo user or a managed profile and the limit has
-                    // been reached, cannot add a user.
+                    // If we're not adding a guest/demo user or a managed profile,
+                    // and the limit has been reached, cannot add a user.
                     Log.e(LOG_TAG, "Cannot add user. Maximum user limit is reached.");
                     return null;
                 }
@@ -2794,8 +2873,7 @@
 
                 userId = getNextAvailableId();
                 Environment.getUserSystemDirectory(userId).mkdirs();
-                boolean ephemeralGuests = Resources.getSystem()
-                        .getBoolean(com.android.internal.R.bool.config_guestUserEphemeral);
+                boolean ephemeralGuests = areGuestUsersEphemeral();
 
                 synchronized (mUsersLock) {
                     // Add ephemeral flag to guests/users if required. Also inherit it from parent.
@@ -2806,9 +2884,9 @@
 
                     userInfo = new UserInfo(userId, name, null, flags);
                     userInfo.serialNumber = mNextSerialNumber++;
-                    long now = System.currentTimeMillis();
-                    userInfo.creationTime = (now > EPOCH_PLUS_30_YEARS) ? now : 0;
+                    userInfo.creationTime = getCreationTime();
                     userInfo.partial = true;
+                    userInfo.preCreated = preCreate;
                     userInfo.lastLoggedInFingerprint = Build.FINGERPRINT;
                     if (isManagedProfile && parentId != UserHandle.USER_NULL) {
                         userInfo.profileBadge = getFreeProfileBadgeLU(parentId);
@@ -2869,17 +2947,40 @@
 
             t.traceBegin("PM.onNewUserCreated");
             mPm.onNewUserCreated(userId);
-            t.traceEnd();
 
-            Intent addedIntent = new Intent(Intent.ACTION_USER_ADDED);
-            addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
-            mContext.sendBroadcastAsUser(addedIntent, UserHandle.ALL,
-                    android.Manifest.permission.MANAGE_USERS);
-            MetricsLogger.count(mContext, isGuest ? TRON_GUEST_CREATED
-                    : (isDemo ? TRON_DEMO_CREATED : TRON_USER_CREATED), 1);
+            if (preCreate) {
+                // Must start user (which will be stopped right away, through
+                // UserController.finishUserUnlockedCompleted) so services can properly
+                // intialize it.
+                // TODO(b/140750212): in the long-term, we should add a onCreateUser() callback
+                // on SystemService instead.
+                Slog.i(LOG_TAG, "starting pre-created user " + userInfo.toFullString());
+                final IActivityManager am = ActivityManager.getService();
+                try {
+                    am.startUserInBackground(userId);
+                } catch (RemoteException e) {
+                    Slog.w(LOG_TAG, "could not start pre-created user " + userId, e);
+                }
+            } else {
+                dispatchUserAddedIntent(userInfo);
+            }
+
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
+
+        // TODO(b/140750212): it's possible to reach "max users overflow" when the user is created
+        // "from scratch" (i.e., not from a pre-created user) and reaches the maximum number of
+        // users without counting the pre-created one. Then when the pre-created is converted, the
+        // "effective" number of max users is exceeds. Example:
+        // Max: 3 Current: 2 full (u0 and u10) + 1 pre-created (u11)
+        // Step 1: create(/* flags doesn't match u11 */): u12 is created, "effective max" is now 3
+        //         (u0, u10, u12) but "real" max is 4 (u0, u10, u11, u12)
+        // Step 2: create(/* flags match u11 */): u11 is converted, now "effective max" is also 4
+        //         (u0, u10, u11, u12)
+        // One way to avoid this issue is by removing a pre-created user from the pool when the
+        // "real" max exceeds the max here.
+
         return userInfo;
     }
 
@@ -2888,6 +2989,62 @@
         return mSystemPackageInstaller.installWhitelistedSystemPackages(isFirstBoot, isUpgrade);
     }
 
+    private long getCreationTime() {
+        final long now = System.currentTimeMillis();
+        return (now > EPOCH_PLUS_30_YEARS) ? now : 0;
+    }
+
+    private void dispatchUserAddedIntent(@NonNull UserInfo userInfo) {
+        Intent addedIntent = new Intent(Intent.ACTION_USER_ADDED);
+        addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, userInfo.id);
+        mContext.sendBroadcastAsUser(addedIntent, UserHandle.ALL,
+                android.Manifest.permission.MANAGE_USERS);
+        MetricsLogger.count(mContext, userInfo.isGuest() ? TRON_GUEST_CREATED
+                : (userInfo.isDemo() ? TRON_DEMO_CREATED : TRON_USER_CREATED), 1);
+    }
+
+    private boolean areGuestUsersEphemeral() {
+        return Resources.getSystem()
+                .getBoolean(com.android.internal.R.bool.config_guestUserEphemeral);
+    }
+
+    /**
+     * Gets a pre-created user for the given flag.
+     *
+     * <p>Should be used only during user creation, so the pre-created user can be used (instead of
+     * creating and initializing a new user from scratch).
+     */
+    // TODO(b/140750212): add unit test
+    @GuardedBy("mUsersLock")
+    private @Nullable UserData getPreCreatedUserLU(@UserInfoFlag int flags) {
+        if (DBG) {
+            Slog.d(LOG_TAG, "getPreCreatedUser(): initialFlags= " + UserInfo.flagsToString(flags));
+        }
+        flags |= UserInfo.FLAG_FULL;
+        if (UserInfo.isGuest(flags) && areGuestUsersEphemeral()) {
+            flags |= UserInfo.FLAG_EPHEMERAL;
+        }
+        if (DBG) {
+            Slog.d(LOG_TAG, "getPreCreatedUser(): targetFlags= " + UserInfo.flagsToString(flags));
+        }
+        final int userSize = mUsers.size();
+        for (int i = 0; i < userSize; i++) {
+            final UserData user = mUsers.valueAt(i);
+            if (DBG) Slog.d(LOG_TAG, i + ":" + user.info.toFullString());
+            if (user.info.preCreated
+                    && (user.info.flags & ~UserInfo.FLAG_INITIALIZED) == flags) {
+                if (!user.info.isInitialized()) {
+                    Slog.w(LOG_TAG, "found pre-created user for flags "
+                            + "" + UserInfo.flagsToString(flags)
+                            + ", but it's not initialized yet: " + user.info.toFullString());
+                    continue;
+                }
+                return user;
+            }
+        }
+        return null;
+    }
+
     @VisibleForTesting
     UserData putUserInfo(UserInfo userInfo) {
         final UserData userData = new UserData();
@@ -3728,7 +3885,7 @@
         try {
             switch(cmd) {
                 case "list":
-                    return runList(pw);
+                    return runList(pw, shell);
                 default:
                     return shell.handleDefaultCommands(cmd);
             }
@@ -3738,17 +3895,58 @@
         return -1;
     }
 
-    private int runList(PrintWriter pw) throws RemoteException {
+    private int runList(PrintWriter pw, Shell shell) throws RemoteException {
+        boolean all = false;
+        boolean verbose = false;
+        String opt;
+        while ((opt = shell.getNextOption()) != null) {
+            switch (opt) {
+                case "-v":
+                    verbose = true;
+                    break;
+                case "--all":
+                    all = true;
+                    break;
+                default:
+                    pw.println("Invalid option: " + opt);
+                    return -1;
+            }
+        }
         final IActivityManager am = ActivityManager.getService();
-        final List<UserInfo> users = getUsers(false);
+        final List<UserInfo> users = getUsers(/* excludePartial= */ !all,
+                /* excludingDying=*/ false, /* excludePreCreated= */ !all);
         if (users == null) {
             pw.println("Error: couldn't get users");
             return 1;
         } else {
-            pw.println("Users:");
-            for (int i = 0; i < users.size(); i++) {
-                String running = am.isUserRunning(users.get(i).id, 0) ? " running" : "";
-                pw.println("\t" + users.get(i).toString() + running);
+            final int size = users.size();
+            int currentUser = UserHandle.USER_NULL;
+            if (verbose) {
+                pw.printf("%d users:\n\n", size);
+                currentUser = am.getCurrentUser().id;
+            } else {
+                // NOTE: the standard "list users" command is used by integration tests and
+                // hence should not be changed. If you need to add more info, use the
+                // verbose option.
+                pw.println("Users:");
+            }
+            for (int i = 0; i < size; i++) {
+                final UserInfo user = users.get(i);
+                final boolean running = am.isUserRunning(user.id, 0);
+                final boolean current = user.id == currentUser;
+                if (verbose) {
+                    pw.printf("%d: id=%d, name=%s, flags=%s%s%s%s%s\n", i, user.id, user.name,
+                            UserInfo.flagsToString(user.flags),
+                            running ? " (running)" : "",
+                            user.partial ? " (partial)" : "",
+                            user.preCreated ? " (pre-created)" : "",
+                            current ? " (current)" : "");
+                } else {
+                    // NOTE: the standard "list users" command is used by integration tests and
+                    // hence should not be changed. If you need to add more info, use the
+                    // verbose option.
+                    pw.printf("\t%s%s\n", user, running ? " running" : "");
+                }
             }
             return 0;
         }
@@ -3785,6 +3983,9 @@
                     if (userInfo.partial) {
                         pw.print(" <partial>");
                     }
+                    if (userInfo.preCreated) {
+                        pw.print(" <pre-created>");
+                    }
                     pw.println();
                     pw.print("    Flags: "); pw.print(userInfo.flags); pw.print(" (");
                     pw.print(UserInfo.flagsToString(userInfo.flags)); pw.println(")");
@@ -3867,10 +4068,10 @@
 
         // Dump some capabilities
         pw.println();
-        pw.println("  Max users: " + UserManager.getMaxSupportedUsers());
+        pw.print("  Max users: " + UserManager.getMaxSupportedUsers());
+        pw.println(" (limit reached: " + isUserLimitReached() + ")");
         pw.println("  Supports switchable users: " + UserManager.supportsMultipleUsers());
-        pw.println("  All guests ephemeral: " + Resources.getSystem().getBoolean(
-                com.android.internal.R.bool.config_guestUserEphemeral));
+        pw.println("  All guests ephemeral: " + areGuestUsersEphemeral());
         pw.println("  Is split-system user: " + UserManager.isSplitSystemUser());
         pw.println("  Is headless-system mode: " + UserManager.isHeadlessSystemUserMode());
         pw.println("  User version: " + mUserVersion);
@@ -4076,7 +4277,7 @@
         public UserInfo createUserEvenWhenDisallowed(String name, int flags,
                 String[] disallowedPackages) {
             UserInfo user = createUserInternalUnchecked(name, flags, UserHandle.USER_NULL,
-                    disallowedPackages);
+                    /* preCreated= */ false, disallowedPackages);
             // Keep this in sync with UserManager.createUser
             if (user != null && !user.isAdmin() && !user.isDemo()) {
                 setUserRestriction(UserManager.DISALLOW_SMS, true, user.id);
@@ -4270,7 +4471,7 @@
             pw.println("  help");
             pw.println("    Print this help text.");
             pw.println("");
-            pw.println("  list");
+            pw.println("  list [-v] [-all]");
             pw.println("    Prints all users on the system.");
         }
     }