1 /*
2  * Copyright (C) 2022 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 
18 package com.android.systemui.user.domain.interactor
19 
20 import android.annotation.UserIdInt
21 import android.app.admin.DevicePolicyManager
22 import android.content.Context
23 import android.content.pm.UserInfo
24 import android.os.RemoteException
25 import android.os.UserHandle
26 import android.os.UserManager
27 import android.util.Log
28 import android.view.WindowManagerGlobal
29 import android.widget.Toast
30 import com.android.internal.logging.UiEventLogger
31 import com.android.systemui.GuestResetOrExitSessionReceiver
32 import com.android.systemui.GuestResumeSessionReceiver
33 import com.android.systemui.dagger.SysUISingleton
34 import com.android.systemui.dagger.qualifiers.Application
35 import com.android.systemui.dagger.qualifiers.Background
36 import com.android.systemui.dagger.qualifiers.Main
37 import com.android.systemui.qs.QSUserSwitcherEvent
38 import com.android.systemui.statusbar.policy.DeviceProvisionedController
39 import com.android.systemui.user.data.repository.UserRepository
40 import com.android.systemui.user.domain.model.ShowDialogRequestModel
41 import javax.inject.Inject
42 import kotlinx.coroutines.CoroutineDispatcher
43 import kotlinx.coroutines.CoroutineScope
44 import kotlinx.coroutines.launch
45 import kotlinx.coroutines.suspendCancellableCoroutine
46 import kotlinx.coroutines.withContext
47 
48 /** Encapsulates business logic to interact with guest user data and systems. */
49 @SysUISingleton
50 class GuestUserInteractor
51 @Inject
52 constructor(
53     @Application private val applicationContext: Context,
54     @Application private val applicationScope: CoroutineScope,
55     @Main private val mainDispatcher: CoroutineDispatcher,
56     @Background private val backgroundDispatcher: CoroutineDispatcher,
57     private val manager: UserManager,
58     private val repository: UserRepository,
59     private val deviceProvisionedController: DeviceProvisionedController,
60     private val devicePolicyManager: DevicePolicyManager,
61     private val refreshUsersScheduler: RefreshUsersScheduler,
62     private val uiEventLogger: UiEventLogger,
63     resumeSessionReceiver: GuestResumeSessionReceiver,
64     resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver,
65 ) {
66     /** Whether the device is configured to always have a guest user available. */
67     val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated
68 
69     /** Whether the guest user is currently being reset. */
70     val isGuestUserResetting: Boolean = repository.isGuestUserResetting
71 
72     init {
73         resumeSessionReceiver.register()
74         resetOrExitSessionReceiver.register()
75     }
76 
77     /** Notifies that the device has finished booting. */
78     fun onDeviceBootCompleted() {
79         applicationScope.launch {
80             if (isDeviceAllowedToAddGuest()) {
81                 guaranteePresent()
82                 return@launch
83             }
84 
85             suspendCancellableCoroutine<Unit> { continuation ->
86                 val callback =
87                     object : DeviceProvisionedController.DeviceProvisionedListener {
88                         override fun onDeviceProvisionedChanged() {
89                             continuation.resumeWith(Result.success(Unit))
90                             deviceProvisionedController.removeCallback(this)
91                         }
92                     }
93 
94                 deviceProvisionedController.addCallback(callback)
95             }
96 
97             if (isDeviceAllowedToAddGuest()) {
98                 guaranteePresent()
99             }
100         }
101     }
102 
103     /** Creates a guest user and switches to it. */
104     fun createAndSwitchTo(
105         showDialog: (ShowDialogRequestModel) -> Unit,
106         dismissDialog: () -> Unit,
107         selectUser: (userId: Int) -> Unit,
108     ) {
109         applicationScope.launch {
110             val newGuestUserId = create(showDialog, dismissDialog)
111             if (newGuestUserId != UserHandle.USER_NULL) {
112                 selectUser(newGuestUserId)
113             }
114         }
115     }
116 
117     /** Exits the guest user, switching back to the last non-guest user or to the default user. */
118     fun exit(
119         @UserIdInt guestUserId: Int,
120         @UserIdInt targetUserId: Int,
121         forceRemoveGuestOnExit: Boolean,
122         showDialog: (ShowDialogRequestModel) -> Unit,
123         dismissDialog: () -> Unit,
124         switchUser: (userId: Int) -> Unit,
125     ) {
126         val currentUserInfo = repository.getSelectedUserInfo()
127         if (currentUserInfo.id != guestUserId) {
128             Log.w(
129                 TAG,
130                 "User requesting to start a new session ($guestUserId) is not current user" +
131                     " (${currentUserInfo.id})"
132             )
133             return
134         }
135 
136         if (!currentUserInfo.isGuest) {
137             Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest")
138             return
139         }
140 
141         applicationScope.launch {
142             var newUserId = repository.mainUserId
143             if (targetUserId == UserHandle.USER_NULL) {
144                 // When a target user is not specified switch to last non guest user:
145                 val lastSelectedNonGuestUserHandle = repository.lastSelectedNonGuestUserId
146                 if (lastSelectedNonGuestUserHandle != repository.mainUserId) {
147                     val info =
148                         withContext(backgroundDispatcher) {
149                             manager.getUserInfo(lastSelectedNonGuestUserHandle)
150                         }
151                     if (info != null && info.isEnabled && info.supportsSwitchTo()) {
152                         newUserId = info.id
153                     }
154                 }
155             } else {
156                 newUserId = targetUserId
157             }
158 
159             if (currentUserInfo.isEphemeral || forceRemoveGuestOnExit) {
160                 uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE)
161                 remove(currentUserInfo.id, newUserId, showDialog, dismissDialog, switchUser)
162             } else {
163                 uiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH)
164                 switchUser(newUserId)
165             }
166         }
167     }
168 
169     /**
170      * Guarantees that the guest user is present on the device, creating it if needed and if allowed
171      * to.
172      */
173     suspend fun guaranteePresent() {
174         if (!isDeviceAllowedToAddGuest()) {
175             return
176         }
177 
178         val guestUser = withContext(backgroundDispatcher) { manager.findCurrentGuestUser() }
179         if (guestUser == null) {
180             scheduleCreation()
181         }
182     }
183 
184     /** Removes the guest user from the device. */
185     suspend fun remove(
186         @UserIdInt guestUserId: Int,
187         @UserIdInt targetUserId: Int,
188         showDialog: (ShowDialogRequestModel) -> Unit,
189         dismissDialog: () -> Unit,
190         switchUser: (userId: Int) -> Unit,
191     ) {
192         val currentUser: UserInfo = repository.getSelectedUserInfo()
193         if (currentUser.id != guestUserId) {
194             Log.w(
195                 TAG,
196                 "User requesting to start a new session ($guestUserId) is not current user" +
197                     " ($currentUser.id)"
198             )
199             return
200         }
201 
202         if (!currentUser.isGuest) {
203             Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest")
204             return
205         }
206 
207         val marked =
208             withContext(backgroundDispatcher) { manager.markGuestForDeletion(currentUser.id) }
209         if (!marked) {
210             Log.w(TAG, "Couldn't mark the guest for deletion for user $guestUserId")
211             return
212         }
213 
214         if (targetUserId == UserHandle.USER_NULL) {
215             // Create a new guest in the foreground, and then immediately switch to it
216             val newGuestId = create(showDialog, dismissDialog)
217             if (newGuestId == UserHandle.USER_NULL) {
218                 Log.e(TAG, "Could not create new guest, switching back to main user")
219                 val mainUser = withContext(backgroundDispatcher) { manager.mainUser?.identifier }
220 
221                 mainUser?.let { switchUser(it) }
222 
223                 withContext(backgroundDispatcher) {
224                     manager.removeUserWhenPossible(
225                         UserHandle.of(currentUser.id),
226                         /* overrideDevicePolicy= */ false
227                     )
228                 }
229                 try {
230                     checkNotNull(WindowManagerGlobal.getWindowManagerService())
231                         .lockNow(/* options= */ null)
232                 } catch (e: RemoteException) {
233                     Log.e(
234                         TAG,
235                         "Couldn't remove guest because ActivityManager or WindowManager is dead"
236                     )
237                 }
238                 return
239             }
240 
241             switchUser(newGuestId)
242 
243             withContext(backgroundDispatcher) {
244                 manager.removeUserWhenPossible(
245                     UserHandle.of(currentUser.id),
246                     /* overrideDevicePolicy= */ false
247                 )
248             }
249         } else {
250             if (repository.isGuestUserAutoCreated) {
251                 repository.isGuestUserResetting = true
252             }
253             switchUser(targetUserId)
254             manager.removeUserWhenPossible(
255                 UserHandle.of(currentUser.id),
256                 /* overrideDevicePolicy= */ false
257             )
258         }
259     }
260 
261     /**
262      * Creates the guest user and adds it to the device.
263      *
264      * @param showDialog A function to invoke to show a dialog.
265      * @param dismissDialog A function to invoke to dismiss a dialog.
266      * @return The user ID of the newly-created guest user.
267      */
268     private suspend fun create(
269         showDialog: (ShowDialogRequestModel) -> Unit,
270         dismissDialog: () -> Unit,
271     ): Int {
272         return withContext(mainDispatcher) {
273             showDialog(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
274             val guestUserId = createInBackground()
275             dismissDialog()
276             if (guestUserId != UserHandle.USER_NULL) {
277                 uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD)
278             } else {
279                 Toast.makeText(
280                         applicationContext,
281                         com.android.settingslib.R.string.add_guest_failed,
282                         Toast.LENGTH_SHORT,
283                     )
284                     .show()
285             }
286 
287             guestUserId
288         }
289     }
290 
291     /** Schedules the creation of the guest user. */
292     private suspend fun scheduleCreation() {
293         if (!repository.isGuestUserCreationScheduled.compareAndSet(false, true)) {
294             return
295         }
296 
297         withContext(backgroundDispatcher) {
298             val newGuestUserId = createInBackground()
299             repository.isGuestUserCreationScheduled.set(false)
300             repository.isGuestUserResetting = false
301             if (newGuestUserId == UserHandle.USER_NULL) {
302                 Log.w(TAG, "Could not create new guest while exiting existing guest")
303                 // Refresh users so that we still display "Guest" if
304                 // config_guestUserAutoCreated=true
305                 refreshUsersScheduler.refreshIfNotPaused()
306             }
307         }
308     }
309 
310     /**
311      * Creates a guest user and return its multi-user user ID.
312      *
313      * This method does not check if a guest already exists before it makes a call to [UserManager]
314      * to create a new one.
315      *
316      * @return The multi-user user ID of the newly created guest user, or [UserHandle.USER_NULL] if
317      *   the guest couldn't be created.
318      */
319     @UserIdInt
320     private suspend fun createInBackground(): Int {
321         return withContext(backgroundDispatcher) {
322             try {
323                 val guestUser = manager.createGuest(applicationContext)
324                 if (guestUser != null) {
325                     guestUser.id
326                 } else {
327                     Log.e(
328                         TAG,
329                         "Couldn't create guest, most likely because there already exists one!"
330                     )
331                     UserHandle.USER_NULL
332                 }
333             } catch (e: UserManager.UserOperationException) {
334                 Log.e(TAG, "Couldn't create guest user!", e)
335                 UserHandle.USER_NULL
336             }
337         }
338     }
339 
340     private fun isDeviceAllowedToAddGuest(): Boolean {
341         return deviceProvisionedController.isDeviceProvisioned &&
342             !devicePolicyManager.isDeviceManaged
343     }
344 
345     companion object {
346         private const val TAG = "GuestUserInteractor"
347     }
348 }
349