blob: 42cd5bca376db39ae150e0f04e416a02dfcfe36a [file] [log] [blame]
/*
* Copyright (C) 2022 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.user.domain.interactor
import android.annotation.UserIdInt
import android.app.admin.DevicePolicyManager
import android.content.Context
import android.content.pm.UserInfo
import android.os.RemoteException
import android.os.UserHandle
import android.os.UserManager
import android.util.Log
import android.view.WindowManagerGlobal
import android.widget.Toast
import com.android.internal.logging.UiEventLogger
import com.android.systemui.GuestResetOrExitSessionReceiver
import com.android.systemui.GuestResumeSessionReceiver
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.qs.QSUserSwitcherEvent
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.user.domain.model.ShowDialogRequestModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
/** Encapsulates business logic to interact with guest user data and systems. */
@SysUISingleton
class GuestUserInteractor
@Inject
constructor(
@Application private val applicationContext: Context,
@Application private val applicationScope: CoroutineScope,
@Main private val mainDispatcher: CoroutineDispatcher,
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val manager: UserManager,
private val repository: UserRepository,
private val deviceProvisionedController: DeviceProvisionedController,
private val devicePolicyManager: DevicePolicyManager,
private val refreshUsersScheduler: RefreshUsersScheduler,
private val uiEventLogger: UiEventLogger,
resumeSessionReceiver: GuestResumeSessionReceiver,
resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver,
) {
/** Whether the device is configured to always have a guest user available. */
val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated
/** Whether the guest user is currently being reset. */
val isGuestUserResetting: Boolean = repository.isGuestUserResetting
init {
resumeSessionReceiver.register()
resetOrExitSessionReceiver.register()
}
/** Notifies that the device has finished booting. */
fun onDeviceBootCompleted() {
applicationScope.launch {
if (isDeviceAllowedToAddGuest()) {
guaranteePresent()
return@launch
}
suspendCancellableCoroutine<Unit> { continuation ->
val callback =
object : DeviceProvisionedController.DeviceProvisionedListener {
override fun onDeviceProvisionedChanged() {
continuation.resumeWith(Result.success(Unit))
deviceProvisionedController.removeCallback(this)
}
}
deviceProvisionedController.addCallback(callback)
}
if (isDeviceAllowedToAddGuest()) {
guaranteePresent()
}
}
}
/** Creates a guest user and switches to it. */
fun createAndSwitchTo(
showDialog: (ShowDialogRequestModel) -> Unit,
dismissDialog: () -> Unit,
selectUser: (userId: Int) -> Unit,
) {
applicationScope.launch {
val newGuestUserId = create(showDialog, dismissDialog)
if (newGuestUserId != UserHandle.USER_NULL) {
selectUser(newGuestUserId)
}
}
}
/** Exits the guest user, switching back to the last non-guest user or to the default user. */
fun exit(
@UserIdInt guestUserId: Int,
@UserIdInt targetUserId: Int,
forceRemoveGuestOnExit: Boolean,
showDialog: (ShowDialogRequestModel) -> Unit,
dismissDialog: () -> Unit,
switchUser: (userId: Int) -> Unit,
) {
val currentUserInfo = repository.getSelectedUserInfo()
if (currentUserInfo.id != guestUserId) {
Log.w(
TAG,
"User requesting to start a new session ($guestUserId) is not current user" +
" (${currentUserInfo.id})"
)
return
}
if (!currentUserInfo.isGuest) {
Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest")
return
}
applicationScope.launch {
var newUserId = UserHandle.USER_SYSTEM
if (targetUserId == UserHandle.USER_NULL) {
// When a target user is not specified switch to last non guest user:
val lastSelectedNonGuestUserHandle = repository.lastSelectedNonGuestUserId
if (lastSelectedNonGuestUserHandle != UserHandle.USER_SYSTEM) {
val info =
withContext(backgroundDispatcher) {
manager.getUserInfo(lastSelectedNonGuestUserHandle)
}
if (info != null && info.isEnabled && info.supportsSwitchToByUser()) {
newUserId = info.id
}
}
} else {
newUserId = targetUserId
}
if (currentUserInfo.isEphemeral || forceRemoveGuestOnExit) {
uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE)
remove(currentUserInfo.id, newUserId, showDialog, dismissDialog, switchUser)
} else {
uiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH)
switchUser(newUserId)
}
}
}
/**
* Guarantees that the guest user is present on the device, creating it if needed and if allowed
* to.
*/
suspend fun guaranteePresent() {
if (!isDeviceAllowedToAddGuest()) {
return
}
val guestUser = withContext(backgroundDispatcher) { manager.findCurrentGuestUser() }
if (guestUser == null) {
scheduleCreation()
}
}
/** Removes the guest user from the device. */
suspend fun remove(
@UserIdInt guestUserId: Int,
@UserIdInt targetUserId: Int,
showDialog: (ShowDialogRequestModel) -> Unit,
dismissDialog: () -> Unit,
switchUser: (userId: Int) -> Unit,
) {
val currentUser: UserInfo = repository.getSelectedUserInfo()
if (currentUser.id != guestUserId) {
Log.w(
TAG,
"User requesting to start a new session ($guestUserId) is not current user" +
" ($currentUser.id)"
)
return
}
if (!currentUser.isGuest) {
Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest")
return
}
val marked =
withContext(backgroundDispatcher) { manager.markGuestForDeletion(currentUser.id) }
if (!marked) {
Log.w(TAG, "Couldn't mark the guest for deletion for user $guestUserId")
return
}
if (targetUserId == UserHandle.USER_NULL) {
// Create a new guest in the foreground, and then immediately switch to it
val newGuestId = create(showDialog, dismissDialog)
if (newGuestId == UserHandle.USER_NULL) {
Log.e(TAG, "Could not create new guest, switching back to system user")
switchUser(UserHandle.USER_SYSTEM)
withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) }
try {
WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null)
} catch (e: RemoteException) {
Log.e(
TAG,
"Couldn't remove guest because ActivityManager or WindowManager is dead"
)
}
return
}
switchUser(newGuestId)
withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) }
} else {
if (repository.isGuestUserAutoCreated) {
repository.isGuestUserResetting = true
}
switchUser(targetUserId)
manager.removeUser(currentUser.id)
}
}
/**
* Creates the guest user and adds it to the device.
*
* @param showDialog A function to invoke to show a dialog.
* @param dismissDialog A function to invoke to dismiss a dialog.
* @return The user ID of the newly-created guest user.
*/
private suspend fun create(
showDialog: (ShowDialogRequestModel) -> Unit,
dismissDialog: () -> Unit,
): Int {
return withContext(mainDispatcher) {
showDialog(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
val guestUserId = createInBackground()
dismissDialog()
if (guestUserId != UserHandle.USER_NULL) {
uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD)
} else {
Toast.makeText(
applicationContext,
com.android.settingslib.R.string.add_guest_failed,
Toast.LENGTH_SHORT,
)
.show()
}
guestUserId
}
}
/** Schedules the creation of the guest user. */
private suspend fun scheduleCreation() {
if (!repository.isGuestUserCreationScheduled.compareAndSet(false, true)) {
return
}
withContext(backgroundDispatcher) {
val newGuestUserId = createInBackground()
repository.isGuestUserCreationScheduled.set(false)
repository.isGuestUserResetting = false
if (newGuestUserId == UserHandle.USER_NULL) {
Log.w(TAG, "Could not create new guest while exiting existing guest")
// Refresh users so that we still display "Guest" if
// config_guestUserAutoCreated=true
refreshUsersScheduler.refreshIfNotPaused()
}
}
}
/**
* Creates a guest user and return its multi-user user ID.
*
* This method does not check if a guest already exists before it makes a call to [UserManager]
* to create a new one.
*
* @return The multi-user user ID of the newly created guest user, or [UserHandle.USER_NULL] if
* the guest couldn't be created.
*/
@UserIdInt
private suspend fun createInBackground(): Int {
return withContext(backgroundDispatcher) {
try {
val guestUser = manager.createGuest(applicationContext)
if (guestUser != null) {
guestUser.id
} else {
Log.e(
TAG,
"Couldn't create guest, most likely because there already exists one!"
)
UserHandle.USER_NULL
}
} catch (e: UserManager.UserOperationException) {
Log.e(TAG, "Couldn't create guest user!", e)
UserHandle.USER_NULL
}
}
}
private fun isDeviceAllowedToAddGuest(): Boolean {
return deviceProvisionedController.isDeviceProvisioned &&
!devicePolicyManager.isDeviceManaged
}
companion object {
private const val TAG = "GuestUserInteractor"
}
}