GuestUserInteractor.

Business logic surrounding guest user.

Bug: 246631653
Test: included in CL
Change-Id: I7363f64f93fff803ecabd3db4050fcdc9981aaaf
This commit is contained in:
Alejandro Nijamkin
2022-09-28 09:53:10 -07:00
parent 0fd4ee1280
commit 7c822664be
3 changed files with 699 additions and 0 deletions

View File

@@ -0,0 +1,322 @@
/*
* 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.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,
) {
/** 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
/** 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. */
private 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"
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.model
import android.os.UserHandle
/** Encapsulates a request to show a dialog. */
sealed class ShowDialogRequestModel {
data class ShowAddUserDialog(
val userHandle: UserHandle,
val isKeyguardShowing: Boolean,
val showEphemeralMessage: Boolean,
) : ShowDialogRequestModel()
data class ShowUserCreationDialog(
val isGuest: Boolean,
) : ShowDialogRequestModel()
data class ShowExitGuestDialog(
val guestUserId: Int,
val targetUserId: Int,
val isGuestEphemeral: Boolean,
val isKeyguardShowing: Boolean,
val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit,
) : ShowDialogRequestModel()
}

View File

@@ -0,0 +1,336 @@
/*
* 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.app.admin.DevicePolicyManager
import android.content.pm.UserInfo
import android.os.UserHandle
import android.os.UserManager
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.domain.model.ShowDialogRequestModel
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.kotlinArgumentCaptor
import com.android.systemui.util.mockito.whenever
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineScope
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(JUnit4::class)
class GuestUserInteractorTest : SysuiTestCase() {
@Mock private lateinit var manager: UserManager
@Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
@Mock private lateinit var devicePolicyManager: DevicePolicyManager
@Mock private lateinit var uiEventLogger: UiEventLogger
@Mock private lateinit var showDialog: (ShowDialogRequestModel) -> Unit
@Mock private lateinit var dismissDialog: () -> Unit
@Mock private lateinit var selectUser: (Int) -> Unit
@Mock private lateinit var switchUser: (Int) -> Unit
private lateinit var underTest: GuestUserInteractor
private lateinit var scope: TestCoroutineScope
private lateinit var repository: FakeUserRepository
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(manager.createGuest(any())).thenReturn(GUEST_USER_INFO)
scope = TestCoroutineScope()
repository = FakeUserRepository()
repository.setUserInfos(ALL_USERS)
underTest =
GuestUserInteractor(
applicationContext = context,
applicationScope = scope,
mainDispatcher = IMMEDIATE,
backgroundDispatcher = IMMEDIATE,
manager = manager,
repository = repository,
deviceProvisionedController = deviceProvisionedController,
devicePolicyManager = devicePolicyManager,
refreshUsersScheduler =
RefreshUsersScheduler(
applicationScope = scope,
mainDispatcher = IMMEDIATE,
repository = repository,
),
uiEventLogger = uiEventLogger,
)
}
@Test
fun `onDeviceBootCompleted - allowed to add - create guest`() =
runBlocking(IMMEDIATE) {
setAllowedToAdd()
underTest.onDeviceBootCompleted()
verify(manager).createGuest(any())
verify(deviceProvisionedController, never()).addCallback(any())
}
@Test
fun `onDeviceBootCompleted - await provisioning - and create guest`() =
runBlocking(IMMEDIATE) {
setAllowedToAdd(isAllowed = false)
underTest.onDeviceBootCompleted()
val captor =
kotlinArgumentCaptor<DeviceProvisionedController.DeviceProvisionedListener>()
verify(deviceProvisionedController).addCallback(captor.capture())
setAllowedToAdd(isAllowed = true)
captor.value.onDeviceProvisionedChanged()
verify(manager).createGuest(any())
verify(deviceProvisionedController).removeCallback(captor.value)
}
@Test
fun createAndSwitchTo() =
runBlocking(IMMEDIATE) {
underTest.createAndSwitchTo(
showDialog = showDialog,
dismissDialog = dismissDialog,
selectUser = selectUser,
)
verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
verify(manager).createGuest(any())
verify(dismissDialog).invoke()
verify(selectUser).invoke(GUEST_USER_INFO.id)
}
@Test
fun `createAndSwitchTo - fails to create - does not switch to`() =
runBlocking(IMMEDIATE) {
whenever(manager.createGuest(any())).thenReturn(null)
underTest.createAndSwitchTo(
showDialog = showDialog,
dismissDialog = dismissDialog,
selectUser = selectUser,
)
verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
verify(manager).createGuest(any())
verify(dismissDialog).invoke()
verify(selectUser, never()).invoke(anyInt())
}
@Test
fun `exit - returns to target user`() =
runBlocking(IMMEDIATE) {
repository.setSelectedUserInfo(GUEST_USER_INFO)
val targetUserId = NON_GUEST_USER_INFO.id
underTest.exit(
guestUserId = GUEST_USER_INFO.id,
targetUserId = targetUserId,
forceRemoveGuestOnExit = false,
showDialog = showDialog,
dismissDialog = dismissDialog,
switchUser = switchUser,
)
verify(manager, never()).markGuestForDeletion(anyInt())
verify(manager, never()).removeUser(anyInt())
verify(switchUser).invoke(targetUserId)
}
@Test
fun `exit - returns to last non-guest`() =
runBlocking(IMMEDIATE) {
val expectedUserId = NON_GUEST_USER_INFO.id
whenever(manager.getUserInfo(expectedUserId)).thenReturn(NON_GUEST_USER_INFO)
repository.lastSelectedNonGuestUserId = expectedUserId
repository.setSelectedUserInfo(GUEST_USER_INFO)
underTest.exit(
guestUserId = GUEST_USER_INFO.id,
targetUserId = UserHandle.USER_NULL,
forceRemoveGuestOnExit = false,
showDialog = showDialog,
dismissDialog = dismissDialog,
switchUser = switchUser,
)
verify(manager, never()).markGuestForDeletion(anyInt())
verify(manager, never()).removeUser(anyInt())
verify(switchUser).invoke(expectedUserId)
}
@Test
fun `exit - last non-guest was removed - returns to system`() =
runBlocking(IMMEDIATE) {
val removedUserId = 310
repository.lastSelectedNonGuestUserId = removedUserId
repository.setSelectedUserInfo(GUEST_USER_INFO)
underTest.exit(
guestUserId = GUEST_USER_INFO.id,
targetUserId = UserHandle.USER_NULL,
forceRemoveGuestOnExit = false,
showDialog = showDialog,
dismissDialog = dismissDialog,
switchUser = switchUser,
)
verify(manager, never()).markGuestForDeletion(anyInt())
verify(manager, never()).removeUser(anyInt())
verify(switchUser).invoke(UserHandle.USER_SYSTEM)
}
@Test
fun `exit - guest was ephemeral - it is removed`() =
runBlocking(IMMEDIATE) {
whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
repository.setUserInfos(listOf(NON_GUEST_USER_INFO, EPHEMERAL_GUEST_USER_INFO))
repository.setSelectedUserInfo(EPHEMERAL_GUEST_USER_INFO)
val targetUserId = NON_GUEST_USER_INFO.id
underTest.exit(
guestUserId = GUEST_USER_INFO.id,
targetUserId = targetUserId,
forceRemoveGuestOnExit = false,
showDialog = showDialog,
dismissDialog = dismissDialog,
switchUser = switchUser,
)
verify(manager).markGuestForDeletion(EPHEMERAL_GUEST_USER_INFO.id)
verify(manager).removeUser(EPHEMERAL_GUEST_USER_INFO.id)
verify(switchUser).invoke(targetUserId)
}
@Test
fun `exit - force remove guest - it is removed`() =
runBlocking(IMMEDIATE) {
whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
repository.setSelectedUserInfo(GUEST_USER_INFO)
val targetUserId = NON_GUEST_USER_INFO.id
underTest.exit(
guestUserId = GUEST_USER_INFO.id,
targetUserId = targetUserId,
forceRemoveGuestOnExit = true,
showDialog = showDialog,
dismissDialog = dismissDialog,
switchUser = switchUser,
)
verify(manager).markGuestForDeletion(GUEST_USER_INFO.id)
verify(manager).removeUser(GUEST_USER_INFO.id)
verify(switchUser).invoke(targetUserId)
}
@Test
fun `exit - selected different from guest user - do nothing`() =
runBlocking(IMMEDIATE) {
repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
underTest.exit(
guestUserId = GUEST_USER_INFO.id,
targetUserId = 123,
forceRemoveGuestOnExit = false,
showDialog = showDialog,
dismissDialog = dismissDialog,
switchUser = switchUser,
)
verifyDidNotExit()
}
@Test
fun `exit - selected is actually not a guest user - do nothing`() =
runBlocking(IMMEDIATE) {
repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
underTest.exit(
guestUserId = NON_GUEST_USER_INFO.id,
targetUserId = 123,
forceRemoveGuestOnExit = false,
showDialog = showDialog,
dismissDialog = dismissDialog,
switchUser = switchUser,
)
verifyDidNotExit()
}
private fun setAllowedToAdd(isAllowed: Boolean = true) {
whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(isAllowed)
whenever(devicePolicyManager.isDeviceManaged).thenReturn(!isAllowed)
}
private fun verifyDidNotExit() {
verify(manager, never()).getUserInfo(anyInt())
verify(manager, never()).markGuestForDeletion(anyInt())
verify(uiEventLogger, never()).log(any())
verify(showDialog, never()).invoke(any())
verify(dismissDialog, never()).invoke()
verify(switchUser, never()).invoke(anyInt())
}
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
private val NON_GUEST_USER_INFO =
UserInfo(
/* id= */ 818,
/* name= */ "non_guest",
/* flags= */ 0,
)
private val GUEST_USER_INFO =
UserInfo(
/* id= */ 669,
/* name= */ "guest",
/* iconPath= */ "",
/* flags= */ 0,
UserManager.USER_TYPE_FULL_GUEST,
)
private val EPHEMERAL_GUEST_USER_INFO =
UserInfo(
/* id= */ 669,
/* name= */ "guest",
/* iconPath= */ "",
/* flags= */ UserInfo.FLAG_EPHEMERAL,
UserManager.USER_TYPE_FULL_GUEST,
)
private val ALL_USERS =
listOf(
NON_GUEST_USER_INFO,
GUEST_USER_INFO,
)
}
}