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:
*
* - Check for a boot user override via {@code CarProperties#boot_user_override_id()}
* - Check for the last active user in the system
* - Fallback to the smallest user id that is not {@link UserHandle.SYSTEM}
*
* 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;
}
}