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