/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.user; import static com.android.car.CarServiceUtils.getContentResolverForUser; import static com.android.car.CarServiceUtils.isVisibleBackgroundUsersOnDefaultDisplaySupported; import static com.android.car.hal.UserHalHelper.userFlagsToString; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.car.builtin.app.ActivityManagerHelper; import android.car.builtin.os.UserManagerHelper; import android.car.builtin.provider.SettingsHelper; import android.car.builtin.util.EventLogHelper; import android.car.builtin.util.Slogf; import android.car.builtin.widget.LockPatternHelper; import android.car.settings.CarSettings; import android.content.Context; import android.hardware.automotive.vehicle.InitialUserInfoRequestType; import android.hardware.automotive.vehicle.UserInfo; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.util.Log; import android.util.Pair; import com.android.car.CarLog; import com.android.car.R; import com.android.car.hal.UserHalHelper; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import com.android.car.internal.common.UserHelperLite; import com.android.car.internal.os.CarSystemProperties; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.function.Consumer; /** * Helper used to set the initial Android user on boot or when resuming from RAM. */ final class InitialUserSetter { @VisibleForTesting static final String TAG = CarLog.tagFor(InitialUserSetter.class); private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG); private static final int BOOT_USER_NOT_FOUND = -1; /** * Sets the initial user using the default behavior. *

The default behavior is: * *

    *
  1. On first boot, it creates and switches to a new user. *
  2. Otherwise, it will switch to either: *
      *
    1. User defined by {@code android.car.systemuser.bootuseroverrideid} (when it was * constructed with such option enabled). *
    2. Last active user (as defined by * {@link android.provider.Settings.Global.LAST_ACTIVE_USER_ID}. *
    *
*/ public static final int TYPE_DEFAULT_BEHAVIOR = 0; /** * Switches to the given user, falling back to {@link #fallbackDefaultBehavior(String)} if it * fails. */ public static final int TYPE_SWITCH = 1; /** * Creates a new user and switches to it, falling back to * {@link #fallbackDefaultBehavior(String) if any of these steps fails. * * @param name (optional) name of the new user * @param halFlags user flags as defined by Vehicle HAL ({@code UserFlags} enum). */ public static final int TYPE_CREATE = 2; /** * Creates a new guest user and switches to it, if current user is unlocked guest user. Does not * fallback if any of these steps fails. falling back to {@link #fallbackDefaultBehavior(String) * if any of these steps fails */ public static final int TYPE_REPLACE_GUEST = 3; @IntDef(prefix = { "TYPE_" }, value = { TYPE_DEFAULT_BEHAVIOR, TYPE_SWITCH, TYPE_CREATE, TYPE_REPLACE_GUEST }) @Retention(RetentionPolicy.SOURCE) public @interface InitialUserInfoType { } private final Context mContext; // TODO(b/150413304): abstract AM / UM into interfaces, then provide local and remote // implementation (where local is implemented by ActivityManagerInternal / UserManagerInternal) private final UserManager mUm; private final CarUserService mCarUserService; private final String mNewUserName; private final String mNewGuestName; private final Consumer mListener; private final UserHandleHelper mUserHandleHelper; private final boolean mIsVisibleBackgroundUsersOnDefaultDisplaySupported; InitialUserSetter(@NonNull Context context, @NonNull CarUserService carUserService, @NonNull Consumer listener, @NonNull UserHandleHelper userHandleHelper) { this(context, carUserService, listener, userHandleHelper, context.getString(R.string.default_guest_name)); } InitialUserSetter(@NonNull Context context, @NonNull CarUserService carUserService, @NonNull Consumer listener, @NonNull UserHandleHelper userHandleHelper, @Nullable String newGuestName) { this(context, context.getSystemService(UserManager.class), carUserService, listener, userHandleHelper, UserManagerHelper.getDefaultUserName(context), newGuestName); } @VisibleForTesting InitialUserSetter(@NonNull Context context, @NonNull UserManager um, @NonNull CarUserService carUserService, @NonNull Consumer listener, @NonNull UserHandleHelper userHandleHelper, @Nullable String newUserName, @Nullable String newGuestName) { mContext = context; mUm = um; mCarUserService = carUserService; mListener = listener; mUserHandleHelper = userHandleHelper; mNewUserName = newUserName; mNewGuestName = newGuestName; mIsVisibleBackgroundUsersOnDefaultDisplaySupported = isVisibleBackgroundUsersOnDefaultDisplaySupported(mUm); } /** * Builder for {@link InitialUserInfo} objects. */ public static final class Builder { private final @InitialUserInfoType int mType; private boolean mReplaceGuest; private @UserIdInt int mSwitchUserId; private @Nullable String mNewUserName; private int mNewUserFlags; private boolean mSupportsOverrideUserIdProperty; private @Nullable String mUserLocales; private int mRequestType; /** * Constructor for the given type. * * @param type {@link #TYPE_DEFAULT_BEHAVIOR}, {@link #TYPE_SWITCH}, {@link #TYPE_CREATE} or * {@link #TYPE_REPLACE_GUEST}. */ public Builder(@InitialUserInfoType int type) { Preconditions.checkArgument(type == TYPE_DEFAULT_BEHAVIOR || type == TYPE_SWITCH || type == TYPE_CREATE || type == TYPE_REPLACE_GUEST, "invalid builder type"); mType = type; } /** * Sets the request type for {@link InitialUserInfoRequestType}. */ public Builder setRequestType(int requestType) { mRequestType = requestType; return this; } /** * Sets the id of the user to be switched to. * * @throws IllegalArgumentException if builder is not for {@link #TYPE_SWITCH}. */ @NonNull public Builder setSwitchUserId(@UserIdInt int userId) { Preconditions.checkArgument(mType == TYPE_SWITCH, "invalid builder type: " + mType); mSwitchUserId = userId; return this; } /** * Sets whether the current user should be replaced when it's a guest. */ @NonNull public Builder setReplaceGuest(boolean value) { mReplaceGuest = value; return this; } /** * Sets the name of the new user being created. * * @throws IllegalArgumentException if builder is not for {@link #TYPE_CREATE}. */ @NonNull public Builder setNewUserName(@Nullable String name) { Preconditions.checkArgument(mType == TYPE_CREATE, "invalid builder type: " + mType); mNewUserName = name; return this; } /** * Sets the flags (as defined by {@link android.hardware.automotive.vehicle.UserInfo}) of * the new user being created. * * @throws IllegalArgumentException if builder is not for {@link #TYPE_CREATE}. */ @NonNull public Builder setNewUserFlags(int flags) { Preconditions.checkArgument(mType == TYPE_CREATE, "invalid builder type: " + mType); mNewUserFlags = flags; return this; } /** * Sets whether the {@code CarProperties#boot_user_override_id()} should be taking in * account when using the default behavior. */ @NonNull public Builder setSupportsOverrideUserIdProperty(boolean value) { mSupportsOverrideUserIdProperty = value; return this; } /** * Sets the system locales for the initial user (when it's created). */ @NonNull public Builder setUserLocales(@Nullable String userLocales) { // This string can come from a binder IPC call where empty string is the default value // for the auto-generated code. So, need to check for that. if (userLocales != null && userLocales.trim().isEmpty()) { mUserLocales = null; } else { mUserLocales = userLocales; } return this; } /** * Builds the object. */ @NonNull public InitialUserInfo build() { return new InitialUserInfo(this); } } /** * Object used to define the properties of the initial user (which can then be set by * {@link InitialUserSetter#set(InitialUserInfo)}); */ public static final class InitialUserInfo { public final @InitialUserInfoType int type; public final boolean replaceGuest; public final @UserIdInt int switchUserId; public final @Nullable String newUserName; public final int newUserFlags; public final boolean supportsOverrideUserIdProperty; public @Nullable String userLocales; public final int requestType; private InitialUserInfo(@NonNull Builder builder) { type = builder.mType; switchUserId = builder.mSwitchUserId; replaceGuest = builder.mReplaceGuest; newUserName = builder.mNewUserName; newUserFlags = builder.mNewUserFlags; supportsOverrideUserIdProperty = builder.mSupportsOverrideUserIdProperty; userLocales = builder.mUserLocales; requestType = builder.mRequestType; } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) public String toString() { StringBuilder string = new StringBuilder("InitialUserInfo[type="); switch (type) { case TYPE_DEFAULT_BEHAVIOR: string.append("DEFAULT_BEHAVIOR"); break; case TYPE_REPLACE_GUEST: string.append("REPLACE_GUEST"); break; case TYPE_SWITCH: string.append("SWITCH").append(",userId=").append(switchUserId); break; case TYPE_CREATE: string.append("CREATE").append(",flags=") .append(UserHalHelper.userFlagsToString(newUserFlags)); if (newUserName != null) { string.append(",name=" + UserHelperLite.safeName(newUserName)); } if (userLocales != null) { string.append(",locales=").append(userLocales); } break; default: string.append("UNKNOWN:").append(type); } if (replaceGuest) { string.append(",replaceGuest"); } if (supportsOverrideUserIdProperty) { string.append(",supportsOverrideUserIdProperty"); } return string.append(']').toString(); } } /** * Sets the initial user. */ public void set(@NonNull InitialUserInfo info) { Preconditions.checkArgument(info != null, "info cannot be null"); EventLogHelper.writeCarInitialUserInfo(info.type, info.replaceGuest, info.switchUserId, info.newUserName, info.newUserFlags, info.supportsOverrideUserIdProperty, info.userLocales); switch (info.type) { case TYPE_DEFAULT_BEHAVIOR: executeDefaultBehavior(info, /* fallback= */ false); break; case TYPE_SWITCH: try { switchUser(info, /* fallback= */ true); } catch (Exception e) { fallbackDefaultBehavior(info, /* fallback= */ true, "Exception switching user: " + e); } break; case TYPE_CREATE: try { createAndSwitchUser(info, /* fallback= */ true); } catch (Exception e) { fallbackDefaultBehavior(info, /* fallback= */ true, "Exception createUser user with name " + UserHelperLite.safeName(info.newUserName) + " and flags " + UserHalHelper.userFlagsToString(info.newUserFlags) + ": " + e); } break; case TYPE_REPLACE_GUEST: try { replaceUser(info, /* fallback= */ true); } catch (Exception e) { fallbackDefaultBehavior(info, /* fallback= */ true, "Exception replace guest user: " + e); } break; default: throw new IllegalArgumentException("invalid InitialUserInfo type: " + info.type); } } private void replaceUser(InitialUserInfo info, boolean fallback) { int currentUserId = ActivityManager.getCurrentUser(); UserHandle currentUser = mUserHandleHelper.getExistingUserHandle(currentUserId); if (currentUser == null) { Slogf.wtf(TAG, "Current user %d handle doesn't exits ", currentUserId); } UserHandle newUser = replaceGuestIfNeeded(currentUser); if (newUser == null) { fallbackDefaultBehavior(info, fallback, "could not replace guest " + currentUser); return; } switchUser(new Builder(TYPE_SWITCH) .setSwitchUserId(newUser.getIdentifier()) .build(), fallback); if (newUser.getIdentifier() != currentUser.getIdentifier()) { Slogf.i(TAG, "Removing old guest %d", currentUser.getIdentifier()); if (!mUm.removeUser(currentUser)) { Slogf.w(TAG, "Could not remove old guest " + currentUser.getIdentifier()); } } } private void executeDefaultBehavior(@NonNull InitialUserInfo info, boolean fallback) { if (mIsVisibleBackgroundUsersOnDefaultDisplaySupported) { if (DBG) { Slogf.d(TAG, "executeDefaultBehavior(): " + "Multi User No Driver switching to system user"); } switchUser(new Builder(TYPE_SWITCH) .setSwitchUserId(UserHandle.SYSTEM.getIdentifier()) .setSupportsOverrideUserIdProperty(info.supportsOverrideUserIdProperty) .setReplaceGuest(false) .build(), fallback); } else if (!hasValidInitialUser()) { if (DBG) { Slogf.d(TAG, "executeDefaultBehavior(): no initial user, creating it"); } createAndSwitchUser(new Builder(TYPE_CREATE) .setNewUserName(mNewUserName) .setNewUserFlags(UserInfo.USER_FLAG_ADMIN) .setSupportsOverrideUserIdProperty(info.supportsOverrideUserIdProperty) .setUserLocales(info.userLocales) .build(), fallback); } else { if (DBG) { Slogf.d(TAG, "executeDefaultBehavior(): switching to initial user"); } int userId = getInitialUser(info.supportsOverrideUserIdProperty); switchUser(new Builder(TYPE_SWITCH) .setSwitchUserId(userId) .setSupportsOverrideUserIdProperty(info.supportsOverrideUserIdProperty) .setReplaceGuest(info.replaceGuest) .build(), fallback); } } @VisibleForTesting void fallbackDefaultBehavior(@NonNull InitialUserInfo info, boolean fallback, @NonNull String reason) { if (!fallback) { // Only log the error Slogf.w(TAG, reason); // Must explicitly tell listener that initial user could not be determined notifyListener(/* initialUser= */ null); return; } EventLogHelper.writeCarInitialUserFallbackDefaultBehavior(reason); Slogf.w(TAG, "Falling back to default behavior. Reason: " + reason); executeDefaultBehavior(info, /* fallback= */ false); } private void switchUser(@NonNull InitialUserInfo info, boolean fallback) { int userId = info.switchUserId; boolean replaceGuest = info.replaceGuest; if (DBG) { Slogf.d(TAG, "switchUser(): userId=" + userId + ", replaceGuest=" + replaceGuest + ", fallback=" + fallback); } UserHandle user = mUserHandleHelper.getExistingUserHandle(userId); if (user == null) { fallbackDefaultBehavior(info, fallback, "user with id " + userId + " doesn't exist"); return; } UserHandle actualUser = user; if (mUserHandleHelper.isGuestUser(user) && replaceGuest) { actualUser = replaceGuestIfNeeded(user); if (actualUser == null) { fallbackDefaultBehavior(info, fallback, "could not replace guest " + user); return; } } int actualUserId = actualUser.getIdentifier(); int currentUserId = ActivityManager.getCurrentUser(); if (DBG) { Slogf.d(TAG, "switchUser: currentUserId = %d, actualUserId = %d", currentUserId, actualUserId); } // TODO(b/266473227): Set isMdnd on InitialUserInfo. if (actualUserId != currentUserId || mIsVisibleBackgroundUsersOnDefaultDisplaySupported) { if (!startForegroundUser(info, actualUserId)) { fallbackDefaultBehavior(info, fallback, "am.switchUser(" + actualUserId + ") failed"); return; } setLastActiveUser(actualUserId); } notifyListener(actualUser); if (actualUserId != userId) { Slogf.i(TAG, "Removing old guest " + userId); if (!mUm.removeUser(user)) { Slogf.w(TAG, "Could not remove old guest " + userId); } } } /** * Check if the user is a guest and can be replaced. */ public boolean canReplaceGuestUser(UserHandle user) { if (!mUserHandleHelper.isGuestUser(user)) { return false; } if (LockPatternHelper.isSecure(mContext, user.getIdentifier())) { if (DBG) { Slogf.d(TAG, "replaceGuestIfNeeded(), skipped, since user " + user.getIdentifier() + " has secure lock pattern"); } return false; } return true; } /** * Replaces {@code user} by a new guest, if necessary. *

If {@code user} is not a guest, it doesn't do anything and returns the same user. *

Otherwise, it marks the current guest for deletion, creates a new one, and returns * the new guest (or {@code null} if a new guest could not be created). */ @VisibleForTesting @Nullable UserHandle replaceGuestIfNeeded(@NonNull UserHandle user) { Preconditions.checkArgument(user != null, "user cannot be null"); if (!canReplaceGuestUser(user)) { return user; } EventLogHelper.writeCarInitialUserReplaceGuest(user.getIdentifier()); Slogf.i(TAG, "Replacing guest (" + user + ")"); int halFlags = UserInfo.USER_FLAG_GUEST; if (mUserHandleHelper.isEphemeralUser(user)) { halFlags |= UserInfo.USER_FLAG_EPHEMERAL; } else { // TODO(b/150413515): decide whether we should allow it or not. Right now we're // just logging, as UserManagerService will automatically set it to ephemeral if // platform is set to do so. Slogf.w(TAG, "guest being replaced is not ephemeral: " + user); } if (!UserManagerHelper.markGuestForDeletion(mUm, user)) { // Don't need to recover in case of failure - most likely create new user will fail // because there is already a guest Slogf.w(TAG, "failed to mark guest " + user.getIdentifier() + " for deletion"); } Pair result = createNewUser(new Builder(TYPE_CREATE) .setNewUserName(mNewGuestName) .setNewUserFlags(halFlags) .build()); String errorMessage = result.second; if (errorMessage != null) { Slogf.w(TAG, "could not replace guest " + user + ": " + errorMessage); return null; } return result.first; } private void createAndSwitchUser(@NonNull InitialUserInfo info, boolean fallback) { Pair result = createNewUser(info); String reason = result.second; if (reason != null) { fallbackDefaultBehavior(info, fallback, reason); return; } switchUser(new Builder(TYPE_SWITCH) .setSwitchUserId(result.first.getIdentifier()) .setSupportsOverrideUserIdProperty(info.supportsOverrideUserIdProperty) .build(), fallback); } /** * Creates a new user. * * @return on success, first element is the new user; on failure, second element contains the * error message. */ @NonNull private Pair createNewUser(@NonNull InitialUserInfo info) { String name = info.newUserName; int halFlags = info.newUserFlags; if (DBG) { Slogf.d(TAG, "createUser(name=" + UserHelperLite.safeName(name) + ", flags=" + userFlagsToString(halFlags) + ")"); } if (UserHalHelper.isSystem(halFlags)) { return new Pair<>(null, "Cannot create system user"); } if (UserHalHelper.isAdmin(halFlags)) { boolean validAdmin = true; if (UserHalHelper.isGuest(halFlags)) { Slogf.w(TAG, "Cannot create guest admin"); validAdmin = false; } if (UserHalHelper.isEphemeral(halFlags)) { Slogf.w(TAG, "Cannot create ephemeral admin"); validAdmin = false; } if (!validAdmin) { return new Pair<>(null, "Invalid flags for admin user"); } } // TODO(b/150413515): decide what to if HAL requested a non-ephemeral guest but framework // sets all guests as ephemeral - should it fail or just warn? int flags = UserHalHelper.toUserInfoFlags(halFlags); String type = UserHalHelper.isGuest(halFlags) ? UserManager.USER_TYPE_FULL_GUEST : UserManager.USER_TYPE_FULL_SECONDARY; if (DBG) { Slogf.d(TAG, "calling am.createUser((name=" + UserHelperLite.safeName(name) + ", type=" + type + ", flags=" + flags + ")"); } UserHandle user = mCarUserService.createUserEvenWhenDisallowed(name, type, flags); if (user == null) { return new Pair<>(null, "createUser(name=" + UserHelperLite.safeName(name) + ", flags=" + userFlagsToString(halFlags) + "): failed to create user"); } if (DBG) { Slogf.d(TAG, "user created: " + user.getIdentifier()); } if (info.userLocales != null) { if (DBG) { Slogf.d(TAG, "setting locale for user " + user.getIdentifier() + " to " + info.userLocales); } Settings.System.putString( getContentResolverForUser(mContext, user.getIdentifier()), SettingsHelper.SYSTEM_LOCALES, info.userLocales); } return new Pair<>(user, null); } @VisibleForTesting boolean startForegroundUser(InitialUserInfo info, @UserIdInt int userId) { EventLogHelper.writeCarInitialUserStartFgUser(userId); if (UserHelperLite.isHeadlessSystemUser(userId)) { if (!mIsVisibleBackgroundUsersOnDefaultDisplaySupported) { // System User is not associated with real person, can not be switched to. // But in Multi User No Driver mode, we'll need to put system user to foreground as // this is exactly the user model. return false; } else { if (DBG) { Slogf.d(TAG, "startForegroundUser: " + "Multi User No Driver, continue to put system user in foreground"); } } } if (info.requestType == InitialUserInfoRequestType.RESUME) { return ActivityManagerHelper.startUserInForeground(userId); } else { Slogf.i(TAG, "Setting boot user to: %d", userId); mUm.setBootUser(UserHandle.of(userId)); return true; } } private void notifyListener(@Nullable UserHandle initialUser) { if (DBG) { Slogf.d(TAG, "notifyListener(): " + initialUser); } mListener.accept(initialUser); } /** * Dumps it state. */ @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) public void dump(@NonNull PrintWriter writer) { writer.println("InitialUserSetter"); String indent = " "; writer.printf("%smNewUserName: %s\n", indent, mNewUserName); writer.printf("%smNewGuestName: %s\n", indent, mNewGuestName); } /** * Sets the last active user. */ public void setLastActiveUser(@UserIdInt int userId) { EventLogHelper.writeCarInitialUserSetLastActive(userId); if (UserHelperLite.isHeadlessSystemUser(userId)) { if (DBG) { Slogf.d(TAG, "setLastActiveUser(): ignoring headless system user " + userId); } return; } setUserIdGlobalProperty(CarSettings.Global.LAST_ACTIVE_USER_ID, userId); UserHandle user = mUserHandleHelper.getExistingUserHandle(userId); if (user == null) { Slogf.w(TAG, "setLastActiveUser(): user " + userId + " doesn't exist"); return; } if (!mUserHandleHelper.isEphemeralUser(user)) { setUserIdGlobalProperty(CarSettings.Global.LAST_ACTIVE_PERSISTENT_USER_ID, userId); } } private void setUserIdGlobalProperty(@NonNull String name, @UserIdInt int userId) { if (DBG) { Slogf.d(TAG, "setting global property " + name + " to " + userId); } Settings.Global.putInt(mContext.getContentResolver(), name, userId); } /** * Gets the user id for the initial user to boot into. This is only applicable for headless * system user model. This method checks for a system property and will only work for system * apps. This method checks for the initial user via three mechanisms in this order: *

    *
  1. Check for a boot user override via {@code CarProperties#boot_user_override_id()}
  2. *
  3. Check for the last active user in the system
  4. *
  5. Fallback to the smallest user id that is not {@link UserHandle.SYSTEM}
  6. *
* If any step fails to retrieve the stored id or the retrieved id does not exist on device, * then it will move onto the next step. * * @return user id of the initial user to boot into on the device, or * {@link UserHandle#USER_NULL} if there is no user available. */ @VisibleForTesting int getInitialUser(boolean usesOverrideUserIdProperty) { List allUsers = userListToUserIdList(getAllUsers()); if (allUsers.isEmpty()) { return UserManagerHelper.USER_NULL; } // TODO(b/150416512): Check if it is still supported, if not remove it. if (usesOverrideUserIdProperty) { int bootUserOverride = CarSystemProperties.getBootUserOverrideId() .orElse(BOOT_USER_NOT_FOUND); // If an override user is present and a real user, return it if (bootUserOverride != BOOT_USER_NOT_FOUND && allUsers.contains(bootUserOverride)) { Slogf.i(TAG, "Boot user id override found for initial user, user id: " + bootUserOverride); return bootUserOverride; } } // If the last active user is not the SYSTEM user and is a real user, return it int lastActiveUser = getUserIdGlobalProperty(CarSettings.Global.LAST_ACTIVE_USER_ID); if (allUsers.contains(lastActiveUser)) { Slogf.i(TAG, "Last active user loaded for initial user: " + lastActiveUser); return lastActiveUser; } resetUserIdGlobalProperty(CarSettings.Global.LAST_ACTIVE_USER_ID); int lastPersistentUser = getUserIdGlobalProperty( CarSettings.Global.LAST_ACTIVE_PERSISTENT_USER_ID); if (allUsers.contains(lastPersistentUser)) { Slogf.i(TAG, "Last active, persistent user loaded for initial user: " + lastPersistentUser); return lastPersistentUser; } resetUserIdGlobalProperty(CarSettings.Global.LAST_ACTIVE_PERSISTENT_USER_ID); // If all else fails, return the smallest user id int returnId = Collections.min(allUsers); // TODO(b/158101909): the smallest user id is not always the initial user; a better approach // would be looking for the first ADMIN user, or keep track of all last active users (not // just the very last) Slogf.w(TAG, "Last active user (" + lastActiveUser + ") not found. Returning smallest user " + "id instead: " + returnId); return returnId; } /** * Gets all the users that can be brought to the foreground on the system. * * @return List of {@code UserHandle} for users that associated with a real person. */ private List getAllUsers() { if (UserManager.isHeadlessSystemUserMode()) { return getAllUsersExceptSystemUserAndSpecifiedUser(UserHandle.SYSTEM.getIdentifier()); } return UserManagerHelper.getUserHandles(mUm, /* excludeDying= */ false); } /** * Gets all the users except system user and the one with userId passed in. * * @param userId of the user not to be returned. * @return All users other than system user and user with userId. */ private List getAllUsersExceptSystemUserAndSpecifiedUser(@UserIdInt int userId) { List users = UserManagerHelper.getUserHandles(mUm, /* excludeDying= */ false); for (Iterator iterator = users.iterator(); iterator.hasNext();) { UserHandle user = iterator.next(); if (user.getIdentifier() == userId || user.getIdentifier() == UserHandle.SYSTEM.getIdentifier()) { // Remove user with userId from the list. iterator.remove(); } } return users; } // TODO(b/231473748): this method should NOT be used to define if it's the first boot - we // should create a new method for that instead (which would check the proper signals) and change // CarUserService.getInitialUserInfoRequestType() to use it instead /** * Checks whether the device has an initial user that can be switched to. */ public boolean hasInitialUser() { List allUsers = getAllUsers(); for (int i = 0; i < allUsers.size(); i++) { UserHandle user = allUsers.get(i); if (mUserHandleHelper.isManagedProfile(user)) { continue; } return true; } return false; } // TODO(b/231473748): temporary method that ignores ephemeral user while hasInitialUser() is // used to define if it's first boot - once there is an isInitialBoot() for that purpose, this // method should be removed (and its logic moved to hasInitialUser()) @VisibleForTesting boolean hasValidInitialUser() { // TODO(b/231473748): should call method that ignores partial, dying, or pre-created List allUsers = getAllUsers(); for (int i = 0; i < allUsers.size(); i++) { UserHandle user = allUsers.get(i); if (mUserHandleHelper.isManagedProfile(user) || mUserHandleHelper.isEphemeralUser(user)) { continue; } return true; } return false; } private static List userListToUserIdList(List allUsers) { ArrayList list = new ArrayList<>(allUsers.size()); for (int i = 0; i < allUsers.size(); i++) { list.add(allUsers.get(i).getIdentifier()); } return list; } private void resetUserIdGlobalProperty(@NonNull String name) { EventLogHelper.writeCarInitialUserResetGlobalProperty(name); Settings.Global.putInt(mContext.getContentResolver(), name, UserManagerHelper.USER_NULL); } private int getUserIdGlobalProperty(@NonNull String name) { int userId = Settings.Global.getInt(mContext.getContentResolver(), name, UserManagerHelper.USER_NULL); if (DBG) { Slogf.d(TAG, "getting global property " + name + ": " + userId); } return userId; } }