1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.car.userpicker; 18 19 import static android.car.CarOccupantZoneManager.INVALID_USER_ID; 20 import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_STOPPED; 21 import static android.car.user.CarUserManager.lifecycleEventTypeToString; 22 23 import android.annotation.UserIdInt; 24 import android.car.Car; 25 import android.car.user.CarUserManager; 26 import android.car.user.CarUserManager.UserLifecycleListener; 27 import android.car.user.UserLifecycleEventFilter; 28 import android.util.ArraySet; 29 import android.util.Log; 30 import android.util.Slog; 31 import android.util.SparseIntArray; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.VisibleForTesting; 35 36 import com.android.internal.annotations.GuardedBy; 37 import com.android.systemui.Dumpable; 38 import com.android.systemui.car.CarServiceProvider; 39 import com.android.systemui.car.CarServiceProvider.CarServiceOnConnectedListener; 40 import com.android.systemui.dagger.SysUISingleton; 41 import com.android.systemui.dump.DumpManager; 42 43 import java.io.PrintWriter; 44 import java.util.Set; 45 import java.util.concurrent.ExecutorService; 46 import java.util.concurrent.Executors; 47 48 import javax.inject.Inject; 49 50 /** 51 * State of the UserPicker Activity. 52 * 53 * The instance is shared between all active UserPicker activities to prevent several users of doing 54 * same actions at the same time (e.g. starting the same user) 55 */ 56 @SysUISingleton 57 public class UserPickerSharedState implements Dumpable { 58 59 private static final String TAG = UserPickerSharedState.class.getSimpleName(); 60 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 61 private final Object mLock = new Object(); 62 63 /** 64 * Same user icon can be clicked from different displays, and then, different thread can be 65 * started for user login with same user id. In this case, mUsersLoginStarted will block the 66 * thread of the next clicked display. 67 */ 68 @GuardedBy("mLock") 69 private final SparseIntArray mUsersLoginStarted; 70 71 /** 72 * When changing the user to another one, the user will be stopped and new user is started. 73 * In this situation, if we click the stopping user icon in user picker on another display, 74 * that user can not login to secondary displays by the core logic for 'pending user start'. 75 * (b/254526109) To avoid this, we track the stopping users to block the stopping user is 76 * clicked until completely stopped. 77 */ 78 @GuardedBy("mLock") 79 private final Set<Integer> mStoppingUsers = new ArraySet<>(); 80 81 /** 82 * When changing user, previous user is stopped, and user picker activity is also destroyed. If 83 * user stopped event arrived after all user picker activities are destroyed, no one can remove 84 * the user from stopping user list. This listener removes completely stopped users from the 85 * list to handle that situation. 86 */ 87 private final UserLifecycleListener mUserStoppedEventListener = event -> { 88 int eventType = event.getEventType(); 89 int userId = event.getUserId(); 90 if (DEBUG) { 91 Slog.d(TAG, "event=" + lifecycleEventTypeToString(eventType) + " userId=" + userId); 92 } 93 if (isStoppingUser(userId)) { 94 removeStoppingUserId(userId); 95 } 96 }; 97 98 private final UserLifecycleEventFilter mFilter = new UserLifecycleEventFilter.Builder() 99 .addEventType(USER_LIFECYCLE_EVENT_TYPE_STOPPED).build(); 100 101 private final CarServiceOnConnectedListener mServiceOnConnectedListener = car -> { 102 onConnected(car); 103 }; 104 105 private final ExecutorService mUserStoppedEventReceiver; 106 107 /** 108 * Constructor for UserPickerSharedState 109 */ 110 @Inject UserPickerSharedState(CarServiceProvider carServiceProvider, DumpManager dumpManager)111 public UserPickerSharedState(CarServiceProvider carServiceProvider, DumpManager dumpManager) { 112 mUsersLoginStarted = new SparseIntArray(); 113 mUserStoppedEventReceiver = Executors.newSingleThreadExecutor(); 114 carServiceProvider.addListener(mServiceOnConnectedListener); 115 dumpManager.registerNormalDumpable(TAG, this); 116 } 117 118 @VisibleForTesting UserPickerSharedState()119 public UserPickerSharedState() { 120 mUsersLoginStarted = new SparseIntArray(); 121 mUserStoppedEventReceiver = null; 122 } 123 onConnected(Car car)124 private void onConnected(Car car) { 125 CarUserManager carUserManager = car.getCarManager(CarUserManager.class); 126 if (carUserManager != null) { 127 carUserManager.addListener(mUserStoppedEventReceiver, mFilter, 128 mUserStoppedEventListener); 129 } 130 } 131 132 /** 133 * This method is to prevent repeated clicks on the same user icon on different displays. 134 * It is called before starting user, and check the user id is in mUsersLoginStarted or not. 135 * If the user id exists on there, it returns false, and worker thread which is responsible for 136 * user start can not start the user. Otherwise, the user id is mapped with the display id, it 137 * returns true, and worker can start the user. 138 * 139 * @param displayId user want to login 140 * @param userId to login to the display 141 * @return true if user can log in to the display, otherwise false. 142 */ setUserLoginStarted(int displayId, @UserIdInt int userId)143 boolean setUserLoginStarted(int displayId, @UserIdInt int userId) { 144 if (DEBUG) { 145 Slog.d(TAG, "setUserLoginStarted: userId=" + userId + " displayId=" + displayId); 146 } 147 synchronized (mLock) { 148 for (int i = 0; i < mUsersLoginStarted.size(); i++) { 149 int startedUser = mUsersLoginStarted.valueAt(i); 150 if (startedUser == userId) { 151 Slog.w(TAG, "setUserLoginStarted: already started on display " 152 + mUsersLoginStarted.keyAt(i)); 153 return false; 154 } 155 } 156 mUsersLoginStarted.put(displayId, userId); 157 return true; 158 } 159 } 160 161 /** 162 * This method is to release user id from mUsersLoginStarted. When the started user is unlocked 163 * state, it is called, or it can be called if it succeeded in preoccupying the display by 164 * adding the user id to the map, but failed to start the user in the subsequent process. 165 */ resetUserLoginStarted(int displayId)166 void resetUserLoginStarted(int displayId) { 167 if (DEBUG) { 168 Slog.d(TAG, "resetUserLoginStarted: displayId=" + displayId); 169 } 170 synchronized (mLock) { 171 mUsersLoginStarted.put(displayId, INVALID_USER_ID); 172 } 173 } 174 175 /** 176 * Gets user id to be logging into the display. 177 * It is used when the user is unlocked. 178 * 179 * @param displayId 180 * @return user id to be logging into the display 181 */ getUserLoginStarted(int displayId)182 int getUserLoginStarted(int displayId) { 183 synchronized (mLock) { 184 return mUsersLoginStarted.get(displayId, INVALID_USER_ID); 185 } 186 } 187 188 /** 189 * The user who is stopping can not start again on secondary displays now.<b/254526109> 190 * So, we manage the list of stopping users, and block starting them again until they are 191 * completely stopped. It is short term solution to solve the problem. 192 */ addStoppingUserId(@serIdInt int userId)193 void addStoppingUserId(@UserIdInt int userId) { 194 synchronized (mLock) { 195 mStoppingUsers.add(userId); 196 } 197 } 198 199 /** 200 * Removes from the blocked list when completely stopped 201 */ removeStoppingUserId(Integer userId)202 void removeStoppingUserId(Integer userId) { 203 synchronized (mLock) { 204 mStoppingUsers.remove(userId); 205 } 206 } 207 208 /** 209 * check whether the user is on stopping. 210 */ isStoppingUser(@serIdInt int userId)211 boolean isStoppingUser(@UserIdInt int userId) { 212 synchronized (mLock) { 213 return mStoppingUsers.contains(userId); 214 } 215 } 216 217 @Override dump(@onNull PrintWriter pw, @NonNull String[] args)218 public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { 219 synchronized (mLock) { 220 pw.println(TAG + " :"); 221 pw.print(" mUsersLoginStarted [userId-displayId] : "); 222 for (int i = 0; i < mUsersLoginStarted.size(); i++) { 223 int displayId = mUsersLoginStarted.keyAt(i); 224 int userId = mUsersLoginStarted.valueAt(i); 225 pw.printf("[%d-%d] ", userId, displayId); 226 } 227 pw.println(); 228 pw.print(" mStoppingUsers : "); 229 if (mStoppingUsers.isEmpty()) { 230 pw.print("None"); 231 } else { 232 ArraySet<Integer> stoppingUsers = (ArraySet<Integer>) mStoppingUsers; 233 for (int i = 0; i < stoppingUsers.size(); i++) { 234 pw.printf("%d ", stoppingUsers.valueAt(i)); 235 } 236 } 237 pw.println(); 238 } 239 } 240 } 241