/* * Copyright (C) 2023 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.systemui.car.userpicker; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_CREATED; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_INVISIBLE; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_REMOVED; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_STARTING; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_STOPPED; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_STOPPING; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED; import static android.car.user.CarUserManager.lifecycleEventTypeToString; import static android.os.UserHandle.USER_ALL; import static android.os.UserHandle.USER_SYSTEM; import static android.os.UserManager.SWITCHABILITY_STATUS_OK; import static android.os.UserManager.isHeadlessSystemUserMode; import android.annotation.MainThread; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.car.SyncResultCallback; import android.car.user.CarUserManager; import android.car.user.CarUserManager.UserLifecycleListener; import android.car.user.UserCreationResult; import android.car.user.UserLifecycleEventFilter; import android.car.user.UserStartRequest; import android.car.user.UserStartResponse; import android.car.user.UserStopRequest; import android.car.user.UserStopResponse; import android.car.user.UserSwitchRequest; import android.car.user.UserSwitchResult; import android.car.util.concurrent.AsyncFuture; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.UserInfo; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; import android.util.Slog; import android.util.SparseArray; import androidx.annotation.VisibleForTesting; import com.android.systemui.R; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import javax.inject.Inject; /** * Helper class for {@link UserManager}, this is meant to be used by builds that support * {@link UserManager#isVisibleBackgroundUsersEnabled() Multi-user model with Concurrent Multi * User Feature.} * *

This class handles user event such as creating, removing, unlocking, stopped, and so on. * Also, it provides methods for creating, stopping, starting users. */ @UserPickerScope public final class UserEventManager { private static final String TAG = UserEventManager.class.getSimpleName(); private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final long USER_TIMEOUT_MS = 10_000; private final UserLifecycleEventFilter mFilter = new UserLifecycleEventFilter.Builder() .addEventType(USER_LIFECYCLE_EVENT_TYPE_SWITCHING) .addEventType(USER_LIFECYCLE_EVENT_TYPE_INVISIBLE) .addEventType(USER_LIFECYCLE_EVENT_TYPE_CREATED) .addEventType(USER_LIFECYCLE_EVENT_TYPE_REMOVED) .addEventType(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED) .addEventType(USER_LIFECYCLE_EVENT_TYPE_STARTING) .addEventType(USER_LIFECYCLE_EVENT_TYPE_STOPPING) .addEventType(USER_LIFECYCLE_EVENT_TYPE_STOPPED).build(); private final Context mContext; private final UserManager mUserManager; private final CarServiceMediator mCarServiceMediator; private final UserPickerSharedState mUserPickerSharedState; /** * {@link UserPickerController} is per-display object. It adds listener to UserEventManager to * update user information, and UserEventManager will call listeners whenever user event occurs. * mUpdateListeners is used only on main thread. */ private final SparseArray mUpdateListeners; private final Handler mMainHandler; /** * This is used to wait until previous user is in invisible state. * When changing user, previous user is stopped, and new user is started. But new user can not * be started if occupant zone is not unassigned for previous user yet, and occupant zone * unassignment is processed on user invisible event. In this reason, we should wait until * previous user is in invisible state for stable user starting. */ private final UserInvisibleWaiter mUserInvisibleWaiter = new UserInvisibleWaiter(); /** * We don't use the main thread for UX responsiveness when handling user events. */ private final ExecutorService mUserLifecycleReceiver; @VisibleForTesting final UserLifecycleListener mUserLifecycleListener = event -> { onUserEvent(event); }; private final BroadcastReceiver mUserUpdateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { runUpdateUsersOnMainThread(); } }; @Inject UserEventManager(Context context, CarServiceMediator carServiceMediator, UserPickerSharedState userPickerSharedState) { mUpdateListeners = new SparseArray<>(); mContext = context.getApplicationContext(); mUserLifecycleReceiver = Executors.newSingleThreadExecutor(); mMainHandler = new Handler(Looper.getMainLooper()); mUserManager = mContext.getSystemService(UserManager.class); mUserPickerSharedState = userPickerSharedState; mCarServiceMediator = carServiceMediator; mCarServiceMediator.registerUserChangeEventsListener(mUserLifecycleReceiver, mFilter, mUserLifecycleListener); registerUserInfoChangedReceiver(); } /** * Unregisters all the listeners when the owners is being destroyed */ void onDestroy() { mCarServiceMediator.onDestroy(); mContext.unregisterReceiver(mUserUpdateReceiver); } private void onUserEvent(CarUserManager.UserLifecycleEvent event) { int eventType = event.getEventType(); int userId = event.getUserId(); if (DEBUG) { Slog.d(TAG, "event=" + lifecycleEventTypeToString(eventType) + " userId=" + userId); } if (eventType == USER_LIFECYCLE_EVENT_TYPE_STOPPING) { mUserPickerSharedState.addStoppingUserId(userId); } else if (eventType == USER_LIFECYCLE_EVENT_TYPE_STOPPED) { if (mUserPickerSharedState.isStoppingUser(userId)) { mUserPickerSharedState.removeStoppingUserId(userId); } } else if (eventType == USER_LIFECYCLE_EVENT_TYPE_INVISIBLE) { mUserInvisibleWaiter.onUserInvisible(userId); } runUpdateUsersOnMainThread(userId, eventType); } private void registerUserInfoChangedReceiver() { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_INFO_CHANGED); mContext.registerReceiverAsUser(mUserUpdateReceiver, UserHandle.ALL, filter, null, null); } void registerOnUpdateUsersListener(OnUpdateUsersListener listener, int displayId) { if (listener == null) { return; } mUpdateListeners.put(displayId, listener); } void unregisterOnUpdateUsersListener(int displayId) { mUpdateListeners.remove(displayId); } @MainThread private void updateUsers(@UserIdInt int userId, int userEvent) { for (int i = 0; i < mUpdateListeners.size(); i++) { OnUpdateUsersListener listener = mUpdateListeners.valueAt(i); if (listener != null) { listener.onUpdateUsers(userId, userEvent); } } } void runUpdateUsersOnMainThread() { runUpdateUsersOnMainThread(USER_ALL, 0); } void runUpdateUsersOnMainThread(@UserIdInt int userId, int userEvent) { if (Looper.myLooper() != Looper.getMainLooper()) { mMainHandler.post(() -> updateUsers(userId, userEvent)); } else { updateUsers(userId, userEvent); } } static int getMaxSupportedUsers() { int maxSupportedUsers = UserManager.getMaxSupportedUsers(); if (isHeadlessSystemUserMode()) { maxSupportedUsers -= 1; } return maxSupportedUsers; } UserInfo getUserInfo(@UserIdInt int userId) { return mUserManager.getUserInfo(userId); } UserInfo getCurrentForegroundUserInfo() { return mUserManager.getUserInfo(ActivityManager.getCurrentUser()); } /** * Gets alive users from user manager except guest users to create user records. * If it is headless system user mode, removes system user info from the list by * {@link UserManager#getAliveUsers}. * * @return the list of users that were created except guest users. */ List getAliveUsers() { List aliveUsers = mUserManager.getAliveUsers(); for (int i = aliveUsers.size() - 1; i >= 0; i--) { UserInfo userInfo = aliveUsers.get(i); if ((isHeadlessSystemUserMode() && userInfo.id == USER_SYSTEM) || userInfo.isGuest()) { aliveUsers.remove(i); } } return aliveUsers; } boolean isUserLimitReached() { int countNonGuestUsers = getAliveUsers().size(); int maxSupportedUsers = getMaxSupportedUsers(); if (countNonGuestUsers > maxSupportedUsers) { Slog.e(TAG, "There are more users on the device than allowed."); return true; } return countNonGuestUsers == maxSupportedUsers; } boolean canForegroundUserAddUsers() { return !mUserManager.hasUserRestrictionForUser(UserManager.DISALLOW_ADD_USER, UserHandle.of(ActivityManager.getCurrentUser())); } boolean isForegroundUserNotSwitchable(UserHandle fgUserHandle) { return mUserManager.getUserSwitchability(fgUserHandle) != SWITCHABILITY_STATUS_OK; } @Nullable UserCreationResult createNewUser() { CarUserManager carUserManager = mCarServiceMediator.getCarUserManager(); AsyncFuture future = carUserManager.createUser( mContext.getString(R.string.car_new_user), 0); return getUserCreationResult(future); } @Nullable UserCreationResult createGuest() { CarUserManager carUserManager = mCarServiceMediator.getCarUserManager(); AsyncFuture future = carUserManager.createGuest( mContext.getString(R.string.car_guest)); return getUserCreationResult(future); } @Nullable private UserCreationResult getUserCreationResult(AsyncFuture future) { UserCreationResult result = null; try { result = future.get(USER_TIMEOUT_MS, TimeUnit.MILLISECONDS); if (result == null) { Slog.e(TAG, "Timed out creating guest after " + USER_TIMEOUT_MS + "ms..."); return null; } } catch (InterruptedException e) { Slog.w(TAG, "Interrupted waiting for future " + future, e); Thread.currentThread().interrupt(); return null; } catch (Exception e) { Slog.w(TAG, "Exception getting future " + future, e); return null; } return result; } boolean isUserRunningUnlocked(@UserIdInt int userId) { return mUserManager.isUserRunning(userId) && mUserManager.isUserUnlocked(userId); } boolean isUserRunning(@UserIdInt int userId) { return mUserManager.isUserRunning(userId); } boolean startUserForDisplay(@UserIdInt int prevCurrentUser, @UserIdInt int userId, int displayId, boolean isFgUserStart) { if (DEBUG) { Slog.d(TAG, "switchToUserForDisplay " + userId + " State : Running " + mUserManager.isUserRunning(userId) + " Unlocked " + mUserManager.isUserUnlocked(userId) + " displayId=" + displayId + " prevCurrentUser=" + prevCurrentUser + " isFgUserStart=" + isFgUserStart); } UserHandle userHandle = UserHandle.of(userId); CarUserManager carUserManager = mCarServiceMediator.getCarUserManager(); if (carUserManager == null) { Slog.w(TAG, "car user manager is not available when starting user " + userId); return false; } if (isFgUserStart) { // Old user will be stopped by {@link UserController} after user switching // completed. In the case of user switching, to avoid clicking stopping user, we can // block previous current user immediately here by adding to the list of stopping // users. mUserPickerSharedState.addStoppingUserId(prevCurrentUser); try { SyncResultCallback userSwitchCallback = new SyncResultCallback<>(); carUserManager.switchUser(new UserSwitchRequest.Builder( userHandle).build(), Runnable::run, userSwitchCallback); UserSwitchResult userSwitchResult = userSwitchCallback.get(USER_TIMEOUT_MS, TimeUnit.MILLISECONDS); if (userSwitchResult.isSuccess()) { Slog.i(TAG, "Successful switchUser from " + prevCurrentUser + " to " + userId + ". Result: " + userSwitchResult); return true; } Slog.w(TAG, "Failed to switchUser from " + prevCurrentUser + " to " + userId + ". Result: " + userSwitchResult); } catch (Exception e) { Slog.e(TAG, "Exception during switchUser from " + prevCurrentUser + " to " + userId, e); return false; } } try { SyncResultCallback userStartCallback = new SyncResultCallback<>(); carUserManager.startUser( new UserStartRequest.Builder(UserHandle.of(userId)) .setDisplayId(displayId).build(), Runnable::run, userStartCallback); UserStartResponse userStartResponse = userStartCallback.get(USER_TIMEOUT_MS, TimeUnit.MILLISECONDS); if (userStartResponse.isSuccess()) { Slog.i(TAG, "Successful startUser for user " + userId + " on display " + displayId + ". Result: " + userStartResponse); return true; } Slog.w(TAG, "startUser failed for " + userId + " on display " + displayId + ". Result: " + userStartResponse); } catch (Exception e) { Slog.e(TAG, "Exception during startUser for user " + userId + " on display " + displayId, e); } return false; } boolean stopUserUnchecked(@UserIdInt int userId, int displayId) { if (DEBUG) { Slog.d(TAG, "stop user:" + userId); } mUserPickerSharedState.addStoppingUserId(userId); CarUserManager carUserManager = mCarServiceMediator.getCarUserManager(); if (carUserManager == null) { Slog.w(TAG, "car user manager is not available when stopping user " + userId); return false; } // We do not need to unassign the user from the occupant zone, because it is handled by // CarUserService#onUserInvisible(). try { mUserInvisibleWaiter.init(userId); SyncResultCallback userStopCallback = new SyncResultCallback<>(); carUserManager.stopUser(new UserStopRequest.Builder(UserHandle.of(userId)).build(), Runnable::run, userStopCallback); UserStopResponse userStopResponse = userStopCallback.get(USER_TIMEOUT_MS, TimeUnit.MILLISECONDS); if (userStopResponse.isSuccess()) { Slog.i(TAG, "Successful stopUser for user " + userId + " on display " + displayId + ". Result: " + userStopResponse); return mUserInvisibleWaiter.waitUserInvisible(); } Slog.w(TAG, "stopUser failed for user " + userId + " on display " + displayId + ". Result: " + userStopResponse); } catch (Exception e) { Slog.e(TAG, "Exception during stopUser for user " + userId + " on display " + displayId, e); } mUserPickerSharedState.removeStoppingUserId(userId); return false; } private static class UserInvisibleWaiter { private @UserIdInt int mUserId; private CountDownLatch mWaiter; void init(@UserIdInt int userId) { mUserId = userId; mWaiter = new CountDownLatch(1); } boolean waitUserInvisible() { if (mWaiter != null) { try { // This method returns false when timeout occurs so that user can re-try to // login. A timeout means that stopUser() has been called successfully, but // the user hasn't changed to invisible yet. return mWaiter.await(USER_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { mWaiter = null; } } return true; } void onUserInvisible(@UserIdInt int userId) { if (userId == mUserId && mWaiter != null) { mWaiter.countDown(); mWaiter = null; } } } /** * Interface for listeners that want to register for receiving updates to changes to the users * on the system including removing and adding users, and changing user info. */ public interface OnUpdateUsersListener { /** * Method that will get called when users list has been changed. */ void onUpdateUsers(@UserIdInt int userId, int userEvent); } }