listener,
@NonNull LockPatternUtils lockPatternUtils, @Nullable String newUserName,
@Nullable String newGuestName) {
mContext = context;
mUm = um;
mCarUserService = carUserService;
mListener = listener;
mLockPatternUtils = lockPatternUtils;
mNewUserName = newUserName;
mNewGuestName = newGuestName;
}
/**
* 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;
/**
* 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 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.V2_0.UserFlags})
* 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 {@link 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;
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;
}
@Override
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");
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();
UserInfo currentUser = mUm.getUserInfo(currentUserId);
UserInfo newUser = replaceGuestIfNeeded(currentUser);
if (newUser == null) {
fallbackDefaultBehavior(info, fallback,
"could not replace guest " + currentUser.toFullString());
return;
}
switchUser(new Builder(TYPE_SWITCH)
.setSwitchUserId(newUser.id)
.build(), fallback);
if (newUser.id != currentUser.id) {
Slog.i(TAG, "Removing old guest " + currentUser.id);
if (!mUm.removeUser(currentUser.id)) {
Slog.w(TAG, "Could not remove old guest " + currentUser.id);
}
}
}
private void executeDefaultBehavior(@NonNull InitialUserInfo info, boolean fallback) {
if (!hasInitialUser()) {
if (DBG) Slog.d(TAG, "executeDefaultBehavior(): no initial user, creating it");
createAndSwitchUser(new Builder(TYPE_CREATE)
.setNewUserName(mNewUserName)
.setNewUserFlags(UserFlags.ADMIN)
.setSupportsOverrideUserIdProperty(info.supportsOverrideUserIdProperty)
.setUserLocales(info.userLocales)
.build(), fallback);
} else {
if (DBG) Slog.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
Slog.w(TAG, reason);
// Must explicitly tell listener that initial user could not be determined
notifyListener(/*initialUser= */ null);
return;
}
Slog.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) {
Slog.d(TAG, "switchUser(): userId=" + userId + ", replaceGuest=" + replaceGuest
+ ", fallback=" + fallback);
}
UserInfo user = mUm.getUserInfo(userId);
if (user == null) {
fallbackDefaultBehavior(info, fallback, "user with id " + userId + " doesn't exist");
return;
}
UserInfo actualUser = user;
if (user.isGuest() && replaceGuest) {
actualUser = replaceGuestIfNeeded(user);
if (actualUser == null) {
fallbackDefaultBehavior(info, fallback, "could not replace guest "
+ user.toFullString());
return;
}
}
int actualUserId = actualUser.id;
unlockSystemUserIfNecessary(actualUserId);
int currentUserId = ActivityManager.getCurrentUser();
if (actualUserId != currentUserId) {
if (!startForegroundUser(actualUserId)) {
fallbackDefaultBehavior(info, fallback,
"am.switchUser(" + actualUserId + ") failed");
return;
}
setLastActiveUser(actualUser.id);
}
notifyListener(actualUser);
if (actualUserId != userId) {
Slog.i(TAG, "Removing old guest " + userId);
if (!mUm.removeUser(userId)) {
Slog.w(TAG, "Could not remove old guest " + userId);
}
}
}
private void unlockSystemUserIfNecessary(@UserIdInt int userId) {
// If system user is the only user to unlock, it will be handled when boot is complete.
if (userId != UserHandle.USER_SYSTEM) {
unlockSystemUser();
}
}
/**
* Check if the user is a guest and can be replaced.
*/
public boolean canReplaceGuestUser(UserInfo user) {
if (!user.isGuest()) return false;
if (mLockPatternUtils.isSecure(user.id)) {
if (DBG) {
Slog.d(TAG, "replaceGuestIfNeeded(), skipped, since user "
+ user.id + " 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
UserInfo replaceGuestIfNeeded(@NonNull UserInfo user) {
Preconditions.checkArgument(user != null, "user cannot be null");
if (!canReplaceGuestUser(user)) {
return user;
}
Slog.i(TAG, "Replacing guest (" + user.toFullString() + ")");
int halFlags = UserFlags.GUEST;
if (user.isEphemeral()) {
halFlags |= UserFlags.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.
Slog.w(TAG, "guest being replaced is not ephemeral: " + user.toFullString());
}
if (!mUm.markGuestForDeletion(user.id)) {
// Don't need to recover in case of failure - most likely create new user will fail
// because there is already a guest
Slog.w(TAG, "failed to mark guest " + user.id + " for deletion");
}
Pair result = createNewUser(new Builder(TYPE_CREATE)
.setNewUserName(mNewGuestName)
.setNewUserFlags(halFlags)
.build());
String errorMessage = result.second;
if (errorMessage != null) {
Slog.w(TAG, "could not replace guest " + user.toFullString() + ": " + 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.id)
.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) {
Slog.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)) {
Slog.w(TAG, "Cannot create guest admin");
validAdmin = false;
}
if (UserHalHelper.isEphemeral(halFlags)) {
Slog.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) {
Slog.d(TAG, "calling am.createUser((name=" + UserHelperLite.safeName(name) + ", type="
+ type + ", flags=" + UserInfo.flagsToString(flags) + ")");
}
UserInfo userInfo = mCarUserService.createUserEvenWhenDisallowed(name, type, flags);
if (userInfo == null) {
return new Pair<>(null, "createUser(name=" + UserHelperLite.safeName(name) + ", flags="
+ userFlagsToString(halFlags) + "): failed to create user");
}
if (DBG) Slog.d(TAG, "user created: " + userInfo.id);
if (info.userLocales != null) {
if (DBG) {
Slog.d(TAG, "setting locale for user " + userInfo.id + " to " + info.userLocales);
}
Settings.System.putStringForUser(mContext.getContentResolver(),
Settings.System.SYSTEM_LOCALES, info.userLocales, userInfo.id);
}
return new Pair<>(userInfo, null);
}
@VisibleForTesting
void unlockSystemUser() {
Slog.i(TAG, "unlocking system user");
IActivityManager am = ActivityManager.getService();
TimingsTraceLog t = new TimingsTraceLog(TAG, Trace.TRACE_TAG_SYSTEM_SERVER);
t.traceBegin("UnlockSystemUser");
try {
// This is for force changing state into RUNNING_LOCKED. Otherwise unlock does not
// update the state and USER_SYSTEM unlock happens twice.
t.traceBegin("am.startUser");
boolean started = am.startUserInBackground(UserHandle.USER_SYSTEM);
t.traceEnd();
if (!started) {
Slog.w(TAG, "could not restart system user in foreground; trying unlock instead");
t.traceBegin("am.unlockUser");
boolean unlocked = am.unlockUser(UserHandle.USER_SYSTEM, /* token= */ null,
/* secret= */ null, /* listener= */ null);
t.traceEnd();
if (!unlocked) {
Slog.w(TAG, "could not unlock system user neither");
return;
}
}
} catch (RemoteException e) {
// should not happen for local call.
Slog.wtf("RemoteException from AMS", e);
} finally {
t.traceEnd();
}
}
@VisibleForTesting
boolean startForegroundUser(@UserIdInt int userId) {
if (UserHelperLite.isHeadlessSystemUser(userId)) {
// System User doesn't associate with real person, can not be switched to.
return false;
}
try {
return ActivityManager.getService().startUserInForegroundWithListener(userId, null);
} catch (RemoteException e) {
Slog.w(TAG, "failed to start user " + userId, e);
return false;
}
}
private void notifyListener(@Nullable UserInfo initialUser) {
if (DBG) Slog.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) {
if (UserHelperLite.isHeadlessSystemUser(userId)) {
if (DBG) Slog.d(TAG, "setLastActiveUser(): ignoring headless system user " + userId);
return;
}
setUserIdGlobalProperty(CarSettings.Global.LAST_ACTIVE_USER_ID, userId);
// TODO(b/155918094): change method to receive a UserInfo instead
UserInfo user = mUm.getUserInfo(userId);
if (user == null) {
Slog.w(TAG, "setLastActiveUser(): user " + userId + " doesn't exist");
return;
}
if (!user.isEphemeral()) {
setUserIdGlobalProperty(CarSettings.Global.LAST_ACTIVE_PERSISTENT_USER_ID, userId);
}
}
private void setUserIdGlobalProperty(@NonNull String name, @UserIdInt int userId) {
if (DBG) Slog.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 {@link 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.USER_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 = userInfoListToUserIdList(getAllUsers());
if (allUsers.isEmpty()) {
return UserHandle.USER_NULL;
}
//TODO(b/150416512): Check if it is still supported, if not remove it.
if (usesOverrideUserIdProperty) {
int bootUserOverride = CarProperties.boot_user_override_id()
.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)) {
Slog.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)) {
Slog.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)) {
Slog.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)
Slog.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 UserInfo} for users that associated with a real person.
*/
private List getAllUsers() {
if (UserManager.isHeadlessSystemUserMode()) {
return getAllUsersExceptSystemUserAndSpecifiedUser(UserHandle.USER_SYSTEM);
} else {
return mUm.getAliveUsers();
}
}
/**
* 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 = mUm.getAliveUsers();
for (Iterator iterator = users.iterator(); iterator.hasNext(); ) {
UserInfo userInfo = iterator.next();
if (userInfo.id == userId || userInfo.id == UserHandle.USER_SYSTEM) {
// Remove user with userId from the list.
iterator.remove();
}
}
return users;
}
/**
* 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++) {
UserInfo user = allUsers.get(i);
if (user.isManagedProfile()) continue;
return true;
}
return false;
}
private static List userInfoListToUserIdList(List allUsers) {
ArrayList list = new ArrayList<>(allUsers.size());
for (int i = 0; i < allUsers.size(); i++) {
list.add(allUsers.get(i).id);
}
return list;
}
private void resetUserIdGlobalProperty(@NonNull String name) {
if (DBG) Slog.d(TAG, "resetting global property " + name);
Settings.Global.putInt(mContext.getContentResolver(), name, UserHandle.USER_NULL);
}
private int getUserIdGlobalProperty(@NonNull String name) {
int userId = Settings.Global.getInt(mContext.getContentResolver(), name,
UserHandle.USER_NULL);
if (DBG) Slog.d(TAG, "getting global property " + name + ": " + userId);
return userId;
}
}