Cleans up old user switcher impl. and flags.

Fix: 246631653,254513286,254513102
Test: tests updated, all pass
Test: manually verified full screen and popup dialog user switchers both
still work

Change-Id: I74b09ae8d64073b7b6b9019195e9bc0fcbb2ae40
This commit is contained in:
Alejandro Nijamkin
2022-11-11 15:18:45 -08:00
parent 463531eecc
commit bbc6083009
23 changed files with 1511 additions and 4676 deletions

View File

@@ -1,392 +0,0 @@
/*
* 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.ui.compose
import android.graphics.drawable.Drawable
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import com.android.systemui.common.ui.compose.load
import com.android.systemui.compose.SysUiOutlinedButton
import com.android.systemui.compose.SysUiTextButton
import com.android.systemui.compose.features.R
import com.android.systemui.compose.theme.LocalAndroidColorScheme
import com.android.systemui.user.ui.viewmodel.UserActionViewModel
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
import com.android.systemui.user.ui.viewmodel.UserViewModel
import java.lang.Integer.min
import kotlin.math.ceil
@Composable
fun UserSwitcherScreen(
viewModel: UserSwitcherViewModel,
onFinished: () -> Unit,
modifier: Modifier = Modifier,
) {
val isFinishRequested: Boolean by viewModel.isFinishRequested.collectAsState(false)
val users: List<UserViewModel> by viewModel.users.collectAsState(emptyList())
val maxUserColumns: Int by viewModel.maximumUserColumns.collectAsState(1)
val menuActions: List<UserActionViewModel> by viewModel.menu.collectAsState(emptyList())
val isOpenMenuButtonVisible: Boolean by viewModel.isOpenMenuButtonVisible.collectAsState(false)
val isMenuVisible: Boolean by viewModel.isMenuVisible.collectAsState(false)
UserSwitcherScreenStateless(
isFinishRequested = isFinishRequested,
users = users,
maxUserColumns = maxUserColumns,
menuActions = menuActions,
isOpenMenuButtonVisible = isOpenMenuButtonVisible,
isMenuVisible = isMenuVisible,
onMenuClosed = viewModel::onMenuClosed,
onOpenMenuButtonClicked = viewModel::onOpenMenuButtonClicked,
onCancelButtonClicked = viewModel::onCancelButtonClicked,
onFinished = {
onFinished()
viewModel.onFinished()
},
modifier = modifier,
)
}
@Composable
private fun UserSwitcherScreenStateless(
isFinishRequested: Boolean,
users: List<UserViewModel>,
maxUserColumns: Int,
menuActions: List<UserActionViewModel>,
isOpenMenuButtonVisible: Boolean,
isMenuVisible: Boolean,
onMenuClosed: () -> Unit,
onOpenMenuButtonClicked: () -> Unit,
onCancelButtonClicked: () -> Unit,
onFinished: () -> Unit,
modifier: Modifier = Modifier,
) {
LaunchedEffect(isFinishRequested) {
if (isFinishRequested) {
onFinished()
}
}
Box(
modifier =
modifier
.fillMaxSize()
.padding(
horizontal = 60.dp,
vertical = 40.dp,
),
) {
UserGrid(
users = users,
maxUserColumns = maxUserColumns,
modifier = Modifier.align(Alignment.Center),
)
Buttons(
menuActions = menuActions,
isOpenMenuButtonVisible = isOpenMenuButtonVisible,
isMenuVisible = isMenuVisible,
onMenuClosed = onMenuClosed,
onOpenMenuButtonClicked = onOpenMenuButtonClicked,
onCancelButtonClicked = onCancelButtonClicked,
modifier = Modifier.align(Alignment.BottomEnd),
)
}
}
@Composable
private fun UserGrid(
users: List<UserViewModel>,
maxUserColumns: Int,
modifier: Modifier = Modifier,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(44.dp),
modifier = modifier,
) {
val rowCount = ceil(users.size / maxUserColumns.toFloat()).toInt()
(0 until rowCount).forEach { rowIndex ->
Row(
horizontalArrangement = Arrangement.spacedBy(64.dp),
modifier = modifier,
) {
val fromIndex = rowIndex * maxUserColumns
val toIndex = min(users.size, (rowIndex + 1) * maxUserColumns)
users.subList(fromIndex, toIndex).forEach { user ->
UserItem(
viewModel = user,
)
}
}
}
}
}
@Composable
private fun UserItem(
viewModel: UserViewModel,
) {
val onClicked = viewModel.onClicked
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
if (onClicked != null) {
Modifier.clickable { onClicked() }
} else {
Modifier
}
.alpha(viewModel.alpha),
) {
Box {
UserItemBackground(modifier = Modifier.align(Alignment.Center).size(222.dp))
UserItemIcon(
image = viewModel.image,
isSelectionMarkerVisible = viewModel.isSelectionMarkerVisible,
modifier = Modifier.align(Alignment.Center).size(222.dp)
)
}
// User name
val text = viewModel.name.load()
if (text != null) {
// We use the box to center-align the text vertically as that is not possible with Text
// alone.
Box(
modifier = Modifier.size(width = 222.dp, height = 48.dp),
) {
Text(
text = text,
style = MaterialTheme.typography.titleLarge,
color = colorResource(com.android.internal.R.color.system_neutral1_50),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.align(Alignment.Center),
)
}
}
}
}
@Composable
private fun UserItemBackground(
modifier: Modifier = Modifier,
) {
Image(
painter = ColorPainter(LocalAndroidColorScheme.current.colorBackground),
contentDescription = null,
modifier = modifier.clip(CircleShape),
)
}
@Composable
private fun UserItemIcon(
image: Drawable,
isSelectionMarkerVisible: Boolean,
modifier: Modifier = Modifier,
) {
Image(
bitmap = image.toBitmap().asImageBitmap(),
contentDescription = null,
modifier =
if (isSelectionMarkerVisible) {
// Draws a ring
modifier.border(
width = 8.dp,
color = LocalAndroidColorScheme.current.colorAccentPrimary,
shape = CircleShape,
)
} else {
modifier
}
.padding(16.dp)
.clip(CircleShape)
)
}
@Composable
private fun Buttons(
menuActions: List<UserActionViewModel>,
isOpenMenuButtonVisible: Boolean,
isMenuVisible: Boolean,
onMenuClosed: () -> Unit,
onOpenMenuButtonClicked: () -> Unit,
onCancelButtonClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
) {
// Cancel button.
SysUiTextButton(
onClick = onCancelButtonClicked,
) {
Text(stringResource(R.string.cancel))
}
// "Open menu" button.
if (isOpenMenuButtonVisible) {
Spacer(modifier = Modifier.width(8.dp))
// To properly use a DropdownMenu in Compose, we need to wrap the button that opens it
// and the menu itself in a Box.
Box {
SysUiOutlinedButton(
onClick = onOpenMenuButtonClicked,
) {
Text(stringResource(R.string.add))
}
Menu(
viewModel = menuActions,
isMenuVisible = isMenuVisible,
onMenuClosed = onMenuClosed,
)
}
}
}
}
@Composable
private fun Menu(
viewModel: List<UserActionViewModel>,
isMenuVisible: Boolean,
onMenuClosed: () -> Unit,
modifier: Modifier = Modifier,
) {
val maxItemWidth = LocalConfiguration.current.screenWidthDp.dp / 4
DropdownMenu(
expanded = isMenuVisible,
onDismissRequest = onMenuClosed,
modifier =
modifier.background(
color = MaterialTheme.colorScheme.inverseOnSurface,
),
) {
viewModel.forEachIndexed { index, action ->
MenuItem(
viewModel = action,
onClicked = { action.onClicked() },
topPadding =
if (index == 0) {
16.dp
} else {
0.dp
},
bottomPadding =
if (index == viewModel.size - 1) {
16.dp
} else {
0.dp
},
modifier = Modifier.sizeIn(maxWidth = maxItemWidth),
)
}
}
}
@Composable
private fun MenuItem(
viewModel: UserActionViewModel,
onClicked: () -> Unit,
topPadding: Dp,
bottomPadding: Dp,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val density = LocalDensity.current
val icon =
remember(viewModel.iconResourceId) {
val drawable =
checkNotNull(AppCompatResources.getDrawable(context, viewModel.iconResourceId))
val size = with(density) { 20.dp.toPx() }.toInt()
drawable
.toBitmap(
width = size,
height = size,
)
.asImageBitmap()
}
DropdownMenuItem(
text = {
Text(
text = stringResource(viewModel.textResourceId),
style = MaterialTheme.typography.bodyMedium,
)
},
onClick = onClicked,
leadingIcon = {
Spacer(modifier = Modifier.width(10.dp))
Image(
bitmap = icon,
contentDescription = null,
)
},
modifier =
modifier
.heightIn(
min = 56.dp,
)
.padding(
start = 18.dp,
end = 65.dp,
top = topPadding,
bottom = bottomPadding,
),
)
}

View File

@@ -111,28 +111,6 @@ object Flags {
// TODO(b/254512385): Tracking Bug
@JvmField val MODERN_BOUNCER = releasedFlag(208, "modern_bouncer")
/**
* Whether the user interactor and repository should use `UserSwitcherController`.
*
* If this is `false`, the interactor and repo skip the controller and directly access the
* framework APIs.
*/
// TODO(b/254513286): Tracking Bug
val USER_INTERACTOR_AND_REPO_USE_CONTROLLER =
unreleasedFlag(210, "user_interactor_and_repo_use_controller")
/**
* Whether `UserSwitcherController` should use the user interactor.
*
* When this is `true`, the controller does not directly access framework APIs. Instead, it goes
* through the interactor.
*
* Note: do not set this to true if [.USER_INTERACTOR_AND_REPO_USE_CONTROLLER] is `true` as it
* would created a cycle between controller -> interactor -> controller.
*/
// TODO(b/254513102): Tracking Bug
val USER_CONTROLLER_USES_INTERACTOR = releasedFlag(211, "user_controller_uses_interactor")
/**
* Whether the clock on a wide lock screen should use the new "stepping" animation for moving
* the digits when the clock moves.

View File

@@ -1148,7 +1148,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
// into fragments, but the rest here, it leaves some awkward lifecycle and whatnot.
mNotificationIconAreaController.setupShelf(mNotificationShelfController);
mShadeExpansionStateManager.addExpansionListener(mWakeUpCoordinator);
mUserSwitcherController.init(mNotificationShadeWindowView);
// Allow plugins to reference DarkIconDispatcher and StatusBarStateController
mPluginDependencyProvider.allowPluginDependency(DarkIconDispatcher.class);
@@ -4285,7 +4284,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
}
// TODO: Bring these out of CentralSurfaces.
mUserInfoControllerImpl.onDensityOrFontScaleChanged();
mUserSwitcherController.onDensityOrFontScaleChanged();
mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext);
mHeadsUpManager.onDensityOrFontScaleChanged();
}

View File

@@ -21,7 +21,6 @@ import android.graphics.ColorFilter
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.drawable.Drawable
import android.os.UserHandle
import android.widget.BaseAdapter
import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower
import com.android.systemui.user.data.source.UserRecord
@@ -84,7 +83,7 @@ protected constructor(
}
fun refresh() {
controller.refreshUsers(UserHandle.USER_NULL)
controller.refreshUsers()
}
companion object {

View File

@@ -14,35 +14,74 @@
* limitations under the License.
*
*/
package com.android.systemui.statusbar.policy
import android.annotation.UserIdInt
import android.content.Context
import android.content.Intent
import android.view.View
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower
import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.domain.interactor.GuestUserInteractor
import com.android.systemui.user.domain.interactor.UserInteractor
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import dagger.Lazy
import java.io.PrintWriter
import java.lang.ref.WeakReference
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
/** Defines interface for a class that provides user switching functionality and state. */
interface UserSwitcherController : Dumpable {
/** Access point into multi-user switching logic. */
@Deprecated("Use UserInteractor or GuestUserInteractor instead.")
@SysUISingleton
class UserSwitcherController
@Inject
constructor(
@Application private val applicationContext: Context,
private val userInteractorLazy: Lazy<UserInteractor>,
private val guestUserInteractorLazy: Lazy<GuestUserInteractor>,
private val keyguardInteractorLazy: Lazy<KeyguardInteractor>,
private val activityStarter: ActivityStarter,
) {
/** Defines interface for classes that can be called back when the user is switched. */
fun interface UserSwitchCallback {
/** Notifies that the user has switched. */
fun onUserSwitched()
}
private val userInteractor: UserInteractor by lazy { userInteractorLazy.get() }
private val guestUserInteractor: GuestUserInteractor by lazy { guestUserInteractorLazy.get() }
private val keyguardInteractor: KeyguardInteractor by lazy { keyguardInteractorLazy.get() }
private val callbackCompatMap = mutableMapOf<UserSwitchCallback, UserInteractor.UserCallback>()
/** The current list of [UserRecord]. */
val users: ArrayList<UserRecord>
get() = userInteractor.userRecords.value
/** Whether the user switcher experience should use the simple experience. */
val isSimpleUserSwitcher: Boolean
/** Require a view for jank detection */
fun init(view: View)
get() = userInteractor.isSimpleUserSwitcher
/** The [UserRecord] of the current user or `null` when none. */
val currentUserRecord: UserRecord?
get() = userInteractor.selectedUserRecord.value
/** The name of the current user of the device or `null`, when none is selected. */
val currentUserName: String?
get() =
currentUserRecord?.let {
LegacyUserUiHelper.getUserRecordName(
context = applicationContext,
record = it,
isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
isGuestUserResetting = userInteractor.isGuestUserResetting,
)
}
/**
* Notifies that a user has been selected.
@@ -55,34 +94,40 @@ interface UserSwitcherController : Dumpable {
* @param userId The ID of the user to switch to.
* @param dialogShower An optional [DialogShower] in case we need to show dialogs.
*/
fun onUserSelected(userId: Int, dialogShower: DialogShower?)
/** Whether it is allowed to add users while the device is locked. */
val isAddUsersFromLockScreenEnabled: Flow<Boolean>
fun onUserSelected(userId: Int, dialogShower: DialogShower?) {
userInteractor.selectUser(userId, dialogShower)
}
/** Whether the guest user is configured to always be present on the device. */
val isGuestUserAutoCreated: Boolean
get() = userInteractor.isGuestUserAutoCreated
/** Whether the guest user is currently being reset. */
val isGuestUserResetting: Boolean
/** Creates and switches to the guest user. */
fun createAndSwitchToGuestUser(dialogShower: DialogShower?)
/** Shows the add user dialog. */
fun showAddUserDialog(dialogShower: DialogShower?)
/** Starts an activity to add a supervised user to the device. */
fun startSupervisedUserActivity()
/** Notifies when the display density or font scale has changed. */
fun onDensityOrFontScaleChanged()
get() = userInteractor.isGuestUserResetting
/** Registers an adapter to notify when the users change. */
fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>)
fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) {
userInteractor.addCallback(
object : UserInteractor.UserCallback {
override fun isEvictable(): Boolean {
return adapter.get() == null
}
override fun onUserStateChanged() {
adapter.get()?.notifyDataSetChanged()
}
}
)
}
/** Notifies the item for a user has been clicked. */
fun onUserListItemClicked(record: UserRecord, dialogShower: DialogShower?)
fun onUserListItemClicked(
record: UserRecord,
dialogShower: DialogShower?,
) {
userInteractor.onRecordSelected(record, dialogShower)
}
/**
* Removes guest user and switches to target user. The guest must be the current user and its id
@@ -103,7 +148,12 @@ interface UserSwitcherController : Dumpable {
* @param targetUserId id of the user to switch to after guest is removed. If
* `UserHandle.USER_NULL`, then switch immediately to the newly created guest user.
*/
fun removeGuestUser(@UserIdInt guestUserId: Int, @UserIdInt targetUserId: Int)
fun removeGuestUser(guestUserId: Int, targetUserId: Int) {
userInteractor.removeGuestUser(
guestUserId = guestUserId,
targetUserId = targetUserId,
)
}
/**
* Exits guest user and switches to previous non-guest user. The guest must be the current user.
@@ -114,43 +164,58 @@ interface UserSwitcherController : Dumpable {
* @param forceRemoveGuestOnExit true: remove guest before switching user, false: remove guest
* only if its ephemeral, else keep guest
*/
fun exitGuestUser(
@UserIdInt guestUserId: Int,
@UserIdInt targetUserId: Int,
forceRemoveGuestOnExit: Boolean
)
fun exitGuestUser(guestUserId: Int, targetUserId: Int, forceRemoveGuestOnExit: Boolean) {
userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
}
/**
* Guarantee guest is present only if the device is provisioned. Otherwise, create a content
* observer to wait until the device is provisioned, then schedule the guest creation.
*/
fun schedulePostBootGuestCreation()
fun schedulePostBootGuestCreation() {
guestUserInteractor.onDeviceBootCompleted()
}
/** Whether keyguard is showing. */
val isKeyguardShowing: Boolean
get() = keyguardInteractor.isKeyguardShowing()
/** Starts an activity with the given [Intent]. */
fun startActivity(intent: Intent)
fun startActivity(intent: Intent) {
activityStarter.startActivity(intent, /* dismissShade= */ true)
}
/**
* Refreshes users from UserManager.
*
* The pictures are only loaded if they have not been loaded yet.
*
* @param forcePictureLoadForId forces the picture of the given user to be reloaded.
*/
fun refreshUsers(forcePictureLoadForId: Int)
fun refreshUsers() {
userInteractor.refreshUsers()
}
/** Adds a subscriber to when user switches. */
fun addUserSwitchCallback(callback: UserSwitchCallback)
fun addUserSwitchCallback(callback: UserSwitchCallback) {
val interactorCallback =
object : UserInteractor.UserCallback {
override fun onUserStateChanged() {
callback.onUserSwitched()
}
}
callbackCompatMap[callback] = interactorCallback
userInteractor.addCallback(interactorCallback)
}
/** Removes a previously-added subscriber. */
fun removeUserSwitchCallback(callback: UserSwitchCallback)
fun removeUserSwitchCallback(callback: UserSwitchCallback) {
val interactorCallback = callbackCompatMap.remove(callback)
if (interactorCallback != null) {
userInteractor.removeCallback(interactorCallback)
}
}
/** Defines interface for classes that can be called back when the user is switched. */
fun interface UserSwitchCallback {
/** Notifies that the user has switched. */
fun onUserSwitched()
fun dump(pw: PrintWriter, args: Array<out String>) {
userInteractor.dump(pw)
}
companion object {

View File

@@ -1,299 +0,0 @@
/*
* 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.statusbar.policy
import android.content.Context
import android.content.Intent
import android.view.View
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.domain.interactor.GuestUserInteractor
import com.android.systemui.user.domain.interactor.UserInteractor
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import dagger.Lazy
import java.io.PrintWriter
import java.lang.ref.WeakReference
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
/** Implementation of [UserSwitcherController]. */
@SysUISingleton
class UserSwitcherControllerImpl
@Inject
constructor(
@Application private val applicationContext: Context,
flags: FeatureFlags,
@Suppress("DEPRECATION") private val oldImpl: Lazy<UserSwitcherControllerOldImpl>,
private val userInteractorLazy: Lazy<UserInteractor>,
private val guestUserInteractorLazy: Lazy<GuestUserInteractor>,
private val keyguardInteractorLazy: Lazy<KeyguardInteractor>,
private val activityStarter: ActivityStarter,
) : UserSwitcherController {
private val useInteractor: Boolean =
flags.isEnabled(Flags.USER_CONTROLLER_USES_INTERACTOR) &&
!flags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
private val _oldImpl: UserSwitcherControllerOldImpl
get() = oldImpl.get()
private val userInteractor: UserInteractor by lazy { userInteractorLazy.get() }
private val guestUserInteractor: GuestUserInteractor by lazy { guestUserInteractorLazy.get() }
private val keyguardInteractor: KeyguardInteractor by lazy { keyguardInteractorLazy.get() }
private val callbackCompatMap =
mutableMapOf<UserSwitcherController.UserSwitchCallback, UserInteractor.UserCallback>()
private fun notSupported(): Nothing {
error("Not supported in the new implementation!")
}
override val users: ArrayList<UserRecord>
get() =
if (useInteractor) {
userInteractor.userRecords.value
} else {
_oldImpl.users
}
override val isSimpleUserSwitcher: Boolean
get() =
if (useInteractor) {
userInteractor.isSimpleUserSwitcher
} else {
_oldImpl.isSimpleUserSwitcher
}
override fun init(view: View) {
if (!useInteractor) {
_oldImpl.init(view)
}
}
override val currentUserRecord: UserRecord?
get() =
if (useInteractor) {
userInteractor.selectedUserRecord.value
} else {
_oldImpl.currentUserRecord
}
override val currentUserName: String?
get() =
if (useInteractor) {
currentUserRecord?.let {
LegacyUserUiHelper.getUserRecordName(
context = applicationContext,
record = it,
isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
isGuestUserResetting = userInteractor.isGuestUserResetting,
)
}
} else {
_oldImpl.currentUserName
}
override fun onUserSelected(
userId: Int,
dialogShower: UserSwitchDialogController.DialogShower?
) {
if (useInteractor) {
userInteractor.selectUser(userId, dialogShower)
} else {
_oldImpl.onUserSelected(userId, dialogShower)
}
}
override val isAddUsersFromLockScreenEnabled: Flow<Boolean>
get() =
if (useInteractor) {
notSupported()
} else {
_oldImpl.isAddUsersFromLockScreenEnabled
}
override val isGuestUserAutoCreated: Boolean
get() =
if (useInteractor) {
userInteractor.isGuestUserAutoCreated
} else {
_oldImpl.isGuestUserAutoCreated
}
override val isGuestUserResetting: Boolean
get() =
if (useInteractor) {
userInteractor.isGuestUserResetting
} else {
_oldImpl.isGuestUserResetting
}
override fun createAndSwitchToGuestUser(
dialogShower: UserSwitchDialogController.DialogShower?,
) {
if (useInteractor) {
notSupported()
} else {
_oldImpl.createAndSwitchToGuestUser(dialogShower)
}
}
override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) {
if (useInteractor) {
notSupported()
} else {
_oldImpl.showAddUserDialog(dialogShower)
}
}
override fun startSupervisedUserActivity() {
if (useInteractor) {
notSupported()
} else {
_oldImpl.startSupervisedUserActivity()
}
}
override fun onDensityOrFontScaleChanged() {
if (!useInteractor) {
_oldImpl.onDensityOrFontScaleChanged()
}
}
override fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) {
if (useInteractor) {
userInteractor.addCallback(
object : UserInteractor.UserCallback {
override fun isEvictable(): Boolean {
return adapter.get() == null
}
override fun onUserStateChanged() {
adapter.get()?.notifyDataSetChanged()
}
}
)
} else {
_oldImpl.addAdapter(adapter)
}
}
override fun onUserListItemClicked(
record: UserRecord,
dialogShower: UserSwitchDialogController.DialogShower?,
) {
if (useInteractor) {
userInteractor.onRecordSelected(record, dialogShower)
} else {
_oldImpl.onUserListItemClicked(record, dialogShower)
}
}
override fun removeGuestUser(guestUserId: Int, targetUserId: Int) {
if (useInteractor) {
userInteractor.removeGuestUser(
guestUserId = guestUserId,
targetUserId = targetUserId,
)
} else {
_oldImpl.removeGuestUser(guestUserId, targetUserId)
}
}
override fun exitGuestUser(
guestUserId: Int,
targetUserId: Int,
forceRemoveGuestOnExit: Boolean
) {
if (useInteractor) {
userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
} else {
_oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
}
}
override fun schedulePostBootGuestCreation() {
if (useInteractor) {
guestUserInteractor.onDeviceBootCompleted()
} else {
_oldImpl.schedulePostBootGuestCreation()
}
}
override val isKeyguardShowing: Boolean
get() =
if (useInteractor) {
keyguardInteractor.isKeyguardShowing()
} else {
_oldImpl.isKeyguardShowing
}
override fun startActivity(intent: Intent) {
if (useInteractor) {
activityStarter.startActivity(intent, /* dismissShade= */ true)
} else {
_oldImpl.startActivity(intent)
}
}
override fun refreshUsers(forcePictureLoadForId: Int) {
if (useInteractor) {
userInteractor.refreshUsers()
} else {
_oldImpl.refreshUsers(forcePictureLoadForId)
}
}
override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
if (useInteractor) {
val interactorCallback =
object : UserInteractor.UserCallback {
override fun onUserStateChanged() {
callback.onUserSwitched()
}
}
callbackCompatMap[callback] = interactorCallback
userInteractor.addCallback(interactorCallback)
} else {
_oldImpl.addUserSwitchCallback(callback)
}
}
override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
if (useInteractor) {
val interactorCallback = callbackCompatMap.remove(callback)
if (interactorCallback != null) {
userInteractor.removeCallback(interactorCallback)
}
} else {
_oldImpl.removeUserSwitchCallback(callback)
}
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
if (useInteractor) {
userInteractor.dump(pw)
} else {
_oldImpl.dump(pw, args)
}
}
}

View File

@@ -58,8 +58,6 @@ import com.android.systemui.statusbar.policy.SecurityController;
import com.android.systemui.statusbar.policy.SecurityControllerImpl;
import com.android.systemui.statusbar.policy.UserInfoController;
import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.statusbar.policy.UserSwitcherControllerImpl;
import com.android.systemui.statusbar.policy.WalletController;
import com.android.systemui.statusbar.policy.WalletControllerImpl;
import com.android.systemui.statusbar.policy.ZenModeController;
@@ -198,8 +196,4 @@ public interface StatusBarPolicyModule {
static DataSaverController provideDataSaverController(NetworkController networkController) {
return networkController.getDataSaverController();
}
/** Binds {@link UserSwitcherController} to its implementation. */
@Binds
UserSwitcherController bindUserSwitcherController(UserSwitcherControllerImpl impl);
}

View File

@@ -19,31 +19,18 @@ package com.android.systemui.user.data.repository
import android.content.Context
import android.content.pm.UserInfo
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import com.android.internal.util.UserIcons
import com.android.systemui.R
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.common.shared.model.Text
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.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.UserSwitcherController
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
import java.util.concurrent.atomic.AtomicBoolean
@@ -55,7 +42,6 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@@ -72,15 +58,6 @@ import kotlinx.coroutines.withContext
* upstream changes.
*/
interface UserRepository {
/** List of all users on the device. */
val users: Flow<List<UserModel>>
/** The currently-selected user. */
val selectedUser: Flow<UserModel>
/** List of available user-related actions. */
val actions: Flow<List<UserActionModel>>
/** User switcher related settings. */
val userSwitcherSettings: Flow<UserSwitcherSettingsModel>
@@ -93,9 +70,6 @@ interface UserRepository {
/** User ID of the last non-guest selected user. */
val lastSelectedNonGuestUserId: Int
/** Whether actions are available even when locked. */
val isActionableWhenLocked: Flow<Boolean>
/** Whether the device is configured to always have a guest user available. */
val isGuestUserAutoCreated: Boolean
@@ -125,18 +99,13 @@ class UserRepositoryImpl
constructor(
@Application private val appContext: Context,
private val manager: UserManager,
private val controller: UserSwitcherController,
@Application private val applicationScope: CoroutineScope,
@Main private val mainDispatcher: CoroutineDispatcher,
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val globalSettings: GlobalSettings,
private val tracker: UserTracker,
private val featureFlags: FeatureFlags,
) : UserRepository {
private val isNewImpl: Boolean
get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
private val _userSwitcherSettings = MutableStateFlow(runBlocking { getSettings() })
override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
_userSwitcherSettings.asStateFlow().filterNotNull()
@@ -150,58 +119,11 @@ constructor(
override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
private set
private val userRecords: Flow<List<UserRecord>> = conflatedCallbackFlow {
fun send() {
trySendWithFailureLogging(
controller.users,
TAG,
)
}
val callback = UserSwitcherController.UserSwitchCallback { send() }
controller.addUserSwitchCallback(callback)
send()
awaitClose { controller.removeUserSwitchCallback(callback) }
}
override val users: Flow<List<UserModel>> =
userRecords.map { records -> records.filter { it.isUser() }.map { it.toUserModel() } }
override val selectedUser: Flow<UserModel> =
users.map { users -> users.first { user -> user.isSelected } }
override val actions: Flow<List<UserActionModel>> =
userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } }
override val isActionableWhenLocked: Flow<Boolean> =
if (isNewImpl) {
emptyFlow()
} else {
controller.isAddUsersFromLockScreenEnabled
}
override val isGuestUserAutoCreated: Boolean =
if (isNewImpl) {
appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated)
} else {
controller.isGuestUserAutoCreated
}
appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated)
private var _isGuestUserResetting: Boolean = false
override var isGuestUserResetting: Boolean =
if (isNewImpl) {
_isGuestUserResetting
} else {
controller.isGuestUserResetting
}
set(value) =
if (isNewImpl) {
_isGuestUserResetting = value
} else {
error("Not supported in the old implementation!")
}
override var isGuestUserResetting: Boolean = _isGuestUserResetting
override val isGuestUserCreationScheduled = AtomicBoolean()
@@ -210,10 +132,8 @@ constructor(
override var isRefreshUsersPaused: Boolean = false
init {
if (isNewImpl) {
observeSelectedUser()
observeUserSettings()
}
observeSelectedUser()
observeUserSettings()
}
override fun refreshUsers() {
@@ -327,64 +247,6 @@ constructor(
}
}
private fun UserRecord.isUser(): Boolean {
return when {
isAddUser -> false
isAddSupervisedUser -> false
isManageUsers -> false
isGuest -> info != null
else -> true
}
}
private fun UserRecord.isNotUser(): Boolean {
return !isUser()
}
private fun UserRecord.toUserModel(): UserModel {
return UserModel(
id = resolveId(),
name = getUserName(this),
image = getUserImage(this),
isSelected = isCurrent,
isSelectable = isSwitchToEnabled || isGuest,
isGuest = isGuest,
)
}
private fun UserRecord.toActionModel(): UserActionModel {
return when {
isAddUser -> UserActionModel.ADD_USER
isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER
isGuest -> UserActionModel.ENTER_GUEST_MODE
isManageUsers -> UserActionModel.NAVIGATE_TO_USER_MANAGEMENT
else -> error("Don't know how to convert to UserActionModel: $this")
}
}
private fun getUserName(record: UserRecord): Text {
val resourceId: Int? = LegacyUserUiHelper.getGuestUserRecordNameResourceId(record)
return if (resourceId != null) {
Text.Resource(resourceId)
} else {
Text.Loaded(checkNotNull(record.info).name)
}
}
private fun getUserImage(record: UserRecord): Drawable {
if (record.isGuest) {
return checkNotNull(
AppCompatResources.getDrawable(appContext, R.drawable.ic_account_circle)
)
}
val userId = checkNotNull(record.info?.id)
return manager.getUserIcon(userId)?.let { userSelectedIcon ->
BitmapDrawable(userSelectedIcon)
}
?: UserIcons.getDefaultUserIcon(appContext.resources, userId, /* light= */ false)
}
companion object {
private const val TAG = "UserRepository"
@VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher"

View File

@@ -39,12 +39,9 @@ import com.android.systemui.common.shared.model.Text
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.statusbar.policy.UserSwitcherController
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.user.data.source.UserRecord
@@ -64,8 +61,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@@ -82,10 +77,8 @@ class UserInteractor
constructor(
@Application private val applicationContext: Context,
private val repository: UserRepository,
private val controller: UserSwitcherController,
private val activityStarter: ActivityStarter,
private val keyguardInteractor: KeyguardInteractor,
private val featureFlags: FeatureFlags,
private val manager: UserManager,
@Application private val applicationScope: CoroutineScope,
telephonyInteractor: TelephonyInteractor,
@@ -107,9 +100,6 @@ constructor(
fun onUserStateChanged()
}
private val isNewImpl: Boolean
get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
private val supervisedUserPackageName: String?
get() =
applicationContext.getString(
@@ -122,181 +112,146 @@ constructor(
/** List of current on-device users to select from. */
val users: Flow<List<UserModel>>
get() =
if (isNewImpl) {
combine(
repository.userInfos,
repository.selectedUserInfo,
repository.userSwitcherSettings,
) { userInfos, selectedUserInfo, settings ->
toUserModels(
userInfos = userInfos,
selectedUserId = selectedUserInfo.id,
isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
)
}
} else {
repository.users
combine(
repository.userInfos,
repository.selectedUserInfo,
repository.userSwitcherSettings,
) { userInfos, selectedUserInfo, settings ->
toUserModels(
userInfos = userInfos,
selectedUserId = selectedUserInfo.id,
isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
)
}
/** The currently-selected user. */
val selectedUser: Flow<UserModel>
get() =
if (isNewImpl) {
combine(
repository.selectedUserInfo,
repository.userSwitcherSettings,
) { selectedUserInfo, settings ->
val selectedUserId = selectedUserInfo.id
checkNotNull(
toUserModel(
userInfo = selectedUserInfo,
selectedUserId = selectedUserId,
canSwitchUsers = canSwitchUsers(selectedUserId),
isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
)
combine(
repository.selectedUserInfo,
repository.userSwitcherSettings,
) { selectedUserInfo, settings ->
val selectedUserId = selectedUserInfo.id
checkNotNull(
toUserModel(
userInfo = selectedUserInfo,
selectedUserId = selectedUserId,
canSwitchUsers = canSwitchUsers(selectedUserId),
isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
)
}
} else {
repository.selectedUser
)
}
/** List of user-switcher related actions that are available. */
val actions: Flow<List<UserActionModel>>
get() =
if (isNewImpl) {
combine(
repository.selectedUserInfo,
repository.userInfos,
repository.userSwitcherSettings,
keyguardInteractor.isKeyguardShowing,
) { _, userInfos, settings, isDeviceLocked ->
buildList {
val hasGuestUser = userInfos.any { it.isGuest }
if (
!hasGuestUser &&
(guestUserInteractor.isGuestUserAutoCreated ||
UserActionsUtil.canCreateGuest(
manager,
repository,
settings.isUserSwitcherEnabled,
settings.isAddUsersFromLockscreen,
))
) {
add(UserActionModel.ENTER_GUEST_MODE)
}
if (!isDeviceLocked || settings.isAddUsersFromLockscreen) {
// The device is locked and our setting to allow actions that add users
// from the lock-screen is not enabled. The guest action from above is
// always allowed, even when the device is locked, but the various "add
// user" actions below are not. We can finish building the list here.
val canCreateUsers =
UserActionsUtil.canCreateUser(
combine(
repository.selectedUserInfo,
repository.userInfos,
repository.userSwitcherSettings,
keyguardInteractor.isKeyguardShowing,
) { _, userInfos, settings, isDeviceLocked ->
buildList {
val hasGuestUser = userInfos.any { it.isGuest }
if (
!hasGuestUser &&
(guestUserInteractor.isGuestUserAutoCreated ||
UserActionsUtil.canCreateGuest(
manager,
repository,
settings.isUserSwitcherEnabled,
settings.isAddUsersFromLockscreen,
)
))
) {
add(UserActionModel.ENTER_GUEST_MODE)
}
if (canCreateUsers) {
add(UserActionModel.ADD_USER)
}
if (!isDeviceLocked || settings.isAddUsersFromLockscreen) {
// The device is locked and our setting to allow actions that add users
// from the lock-screen is not enabled. The guest action from above is
// always allowed, even when the device is locked, but the various "add
// user" actions below are not. We can finish building the list here.
if (
UserActionsUtil.canCreateSupervisedUser(
manager,
repository,
settings.isUserSwitcherEnabled,
settings.isAddUsersFromLockscreen,
supervisedUserPackageName,
)
) {
add(UserActionModel.ADD_SUPERVISED_USER)
}
}
if (
UserActionsUtil.canManageUsers(
val canCreateUsers =
UserActionsUtil.canCreateUser(
manager,
repository,
settings.isUserSwitcherEnabled,
settings.isAddUsersFromLockscreen,
)
) {
add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
if (canCreateUsers) {
add(UserActionModel.ADD_USER)
}
if (
UserActionsUtil.canCreateSupervisedUser(
manager,
repository,
settings.isUserSwitcherEnabled,
settings.isAddUsersFromLockscreen,
supervisedUserPackageName,
)
) {
add(UserActionModel.ADD_SUPERVISED_USER)
}
}
if (
UserActionsUtil.canManageUsers(
repository,
settings.isUserSwitcherEnabled,
settings.isAddUsersFromLockscreen,
)
) {
add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
}
}
} else {
combine(
repository.isActionableWhenLocked,
keyguardInteractor.isKeyguardShowing,
) { isActionableWhenLocked, isLocked ->
isActionableWhenLocked || !isLocked
}
.flatMapLatest { isActionable ->
if (isActionable) {
repository.actions
} else {
// If not actionable it means that we're not allowed to show actions
// when
// locked and we are locked. Therefore, we should show no actions.
flowOf(emptyList())
}
}
}
val userRecords: StateFlow<ArrayList<UserRecord>> =
if (isNewImpl) {
combine(
repository.userInfos,
repository.selectedUserInfo,
actions,
repository.userSwitcherSettings,
) { userInfos, selectedUserInfo, actionModels, settings ->
ArrayList(
userInfos.map {
combine(
repository.userInfos,
repository.selectedUserInfo,
actions,
repository.userSwitcherSettings,
) { userInfos, selectedUserInfo, actionModels, settings ->
ArrayList(
userInfos.map {
toRecord(
userInfo = it,
selectedUserId = selectedUserInfo.id,
)
} +
actionModels.map {
toRecord(
userInfo = it,
action = it,
selectedUserId = selectedUserInfo.id,
isRestricted =
it != UserActionModel.ENTER_GUEST_MODE &&
it != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT &&
!settings.isAddUsersFromLockscreen,
)
} +
actionModels.map {
toRecord(
action = it,
selectedUserId = selectedUserInfo.id,
isRestricted =
it != UserActionModel.ENTER_GUEST_MODE &&
it != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT &&
!settings.isAddUsersFromLockscreen,
)
}
)
}
.onEach { notifyCallbacks() }
.stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
initialValue = ArrayList(),
}
)
} else {
MutableStateFlow(ArrayList())
}
}
.onEach { notifyCallbacks() }
.stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
initialValue = ArrayList(),
)
val selectedUserRecord: StateFlow<UserRecord?> =
if (isNewImpl) {
repository.selectedUserInfo
.map { selectedUserInfo ->
toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id)
}
.stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
initialValue = null,
)
} else {
MutableStateFlow(null)
}
repository.selectedUserInfo
.map { selectedUserInfo ->
toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id)
}
.stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
initialValue = null,
)
/** Whether the device is configured to always have a guest user available. */
val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated
@@ -311,44 +266,37 @@ constructor(
val dialogDismissRequests: Flow<Unit?> = _dialogDismissRequests.asStateFlow()
val isSimpleUserSwitcher: Boolean
get() =
if (isNewImpl) {
repository.isSimpleUserSwitcher()
} else {
error("Not supported in the old implementation!")
}
get() = repository.isSimpleUserSwitcher()
init {
if (isNewImpl) {
refreshUsersScheduler.refreshIfNotPaused()
telephonyInteractor.callState
.distinctUntilChanged()
.onEach { refreshUsersScheduler.refreshIfNotPaused() }
.launchIn(applicationScope)
refreshUsersScheduler.refreshIfNotPaused()
telephonyInteractor.callState
.distinctUntilChanged()
.onEach { refreshUsersScheduler.refreshIfNotPaused() }
.launchIn(applicationScope)
combine(
broadcastDispatcher.broadcastFlow(
filter =
IntentFilter().apply {
addAction(Intent.ACTION_USER_ADDED)
addAction(Intent.ACTION_USER_REMOVED)
addAction(Intent.ACTION_USER_INFO_CHANGED)
addAction(Intent.ACTION_USER_SWITCHED)
addAction(Intent.ACTION_USER_STOPPED)
addAction(Intent.ACTION_USER_UNLOCKED)
},
user = UserHandle.SYSTEM,
map = { intent, _ -> intent },
),
repository.selectedUserInfo.pairwise(null),
) { intent, selectedUserChange ->
Pair(intent, selectedUserChange.previousValue)
}
.onEach { (intent, previousSelectedUser) ->
onBroadcastReceived(intent, previousSelectedUser)
}
.launchIn(applicationScope)
}
combine(
broadcastDispatcher.broadcastFlow(
filter =
IntentFilter().apply {
addAction(Intent.ACTION_USER_ADDED)
addAction(Intent.ACTION_USER_REMOVED)
addAction(Intent.ACTION_USER_INFO_CHANGED)
addAction(Intent.ACTION_USER_SWITCHED)
addAction(Intent.ACTION_USER_STOPPED)
addAction(Intent.ACTION_USER_UNLOCKED)
},
user = UserHandle.SYSTEM,
map = { intent, _ -> intent },
),
repository.selectedUserInfo.pairwise(null),
) { intent, selectedUserChange ->
Pair(intent, selectedUserChange.previousValue)
}
.onEach { (intent, previousSelectedUser) ->
onBroadcastReceived(intent, previousSelectedUser)
}
.launchIn(applicationScope)
}
fun addCallback(callback: UserCallback) {
@@ -414,48 +362,43 @@ constructor(
newlySelectedUserId: Int,
dialogShower: UserSwitchDialogController.DialogShower? = null,
) {
if (isNewImpl) {
val currentlySelectedUserInfo = repository.getSelectedUserInfo()
if (
newlySelectedUserId == currentlySelectedUserInfo.id &&
currentlySelectedUserInfo.isGuest
) {
// Here when clicking on the currently-selected guest user to leave guest mode
// and return to the previously-selected non-guest user.
showDialog(
ShowDialogRequestModel.ShowExitGuestDialog(
guestUserId = currentlySelectedUserInfo.id,
targetUserId = repository.lastSelectedNonGuestUserId,
isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
onExitGuestUser = this::exitGuestUser,
dialogShower = dialogShower,
)
val currentlySelectedUserInfo = repository.getSelectedUserInfo()
if (
newlySelectedUserId == currentlySelectedUserInfo.id && currentlySelectedUserInfo.isGuest
) {
// Here when clicking on the currently-selected guest user to leave guest mode
// and return to the previously-selected non-guest user.
showDialog(
ShowDialogRequestModel.ShowExitGuestDialog(
guestUserId = currentlySelectedUserInfo.id,
targetUserId = repository.lastSelectedNonGuestUserId,
isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
onExitGuestUser = this::exitGuestUser,
dialogShower = dialogShower,
)
return
}
if (currentlySelectedUserInfo.isGuest) {
// Here when switching from guest to a non-guest user.
showDialog(
ShowDialogRequestModel.ShowExitGuestDialog(
guestUserId = currentlySelectedUserInfo.id,
targetUserId = newlySelectedUserId,
isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
onExitGuestUser = this::exitGuestUser,
dialogShower = dialogShower,
)
)
return
}
dialogShower?.dismiss()
switchUser(newlySelectedUserId)
} else {
controller.onUserSelected(newlySelectedUserId, dialogShower)
)
return
}
if (currentlySelectedUserInfo.isGuest) {
// Here when switching from guest to a non-guest user.
showDialog(
ShowDialogRequestModel.ShowExitGuestDialog(
guestUserId = currentlySelectedUserInfo.id,
targetUserId = newlySelectedUserId,
isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
onExitGuestUser = this::exitGuestUser,
dialogShower = dialogShower,
)
)
return
}
dialogShower?.dismiss()
switchUser(newlySelectedUserId)
}
/** Executes the given action. */
@@ -463,51 +406,38 @@ constructor(
action: UserActionModel,
dialogShower: UserSwitchDialogController.DialogShower? = null,
) {
if (isNewImpl) {
when (action) {
UserActionModel.ENTER_GUEST_MODE ->
guestUserInteractor.createAndSwitchTo(
this::showDialog,
this::dismissDialog,
) { userId ->
selectUser(userId, dialogShower)
}
UserActionModel.ADD_USER -> {
val currentUser = repository.getSelectedUserInfo()
showDialog(
ShowDialogRequestModel.ShowAddUserDialog(
userHandle = currentUser.userHandle,
isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral,
dialogShower = dialogShower,
)
)
when (action) {
UserActionModel.ENTER_GUEST_MODE ->
guestUserInteractor.createAndSwitchTo(
this::showDialog,
this::dismissDialog,
) { userId ->
selectUser(userId, dialogShower)
}
UserActionModel.ADD_SUPERVISED_USER ->
activityStarter.startActivity(
Intent()
.setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
.setPackage(supervisedUserPackageName)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
/* dismissShade= */ true,
)
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
activityStarter.startActivity(
Intent(Settings.ACTION_USER_SETTINGS),
/* dismissShade= */ true,
)
}
} else {
when (action) {
UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null)
UserActionModel.ADD_USER -> controller.showAddUserDialog(null)
UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity()
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
activityStarter.startActivity(
Intent(Settings.ACTION_USER_SETTINGS),
/* dismissShade= */ false,
UserActionModel.ADD_USER -> {
val currentUser = repository.getSelectedUserInfo()
showDialog(
ShowDialogRequestModel.ShowAddUserDialog(
userHandle = currentUser.userHandle,
isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral,
dialogShower = dialogShower,
)
)
}
UserActionModel.ADD_SUPERVISED_USER ->
activityStarter.startActivity(
Intent()
.setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
.setPackage(supervisedUserPackageName)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
/* dismissShade= */ true,
)
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
activityStarter.startActivity(
Intent(Settings.ACTION_USER_SETTINGS),
/* dismissShade= */ true,
)
}
}

View File

@@ -27,15 +27,12 @@ import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.user.domain.interactor.UserInteractor
import com.android.systemui.user.domain.model.ShowDialogRequestModel
import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
@@ -50,16 +47,11 @@ constructor(
private val broadcastSender: Lazy<BroadcastSender>,
private val dialogLaunchAnimator: Lazy<DialogLaunchAnimator>,
private val interactor: Lazy<UserInteractor>,
private val featureFlags: Lazy<FeatureFlags>,
) : CoreStartable {
private var currentDialog: Dialog? = null
override fun start() {
if (featureFlags.get().isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) {
return
}
startHandlingDialogShowRequests()
startHandlingDialogDismissRequests()
}

View File

@@ -20,8 +20,6 @@ package com.android.systemui.user.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.android.systemui.common.ui.drawable.CircularDrawable
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.user.domain.interactor.GuestUserInteractor
import com.android.systemui.user.domain.interactor.UserInteractor
@@ -41,12 +39,8 @@ private constructor(
private val userInteractor: UserInteractor,
private val guestUserInteractor: GuestUserInteractor,
private val powerInteractor: PowerInteractor,
private val featureFlags: FeatureFlags,
) : ViewModel() {
private val isNewImpl: Boolean
get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
/** On-device users. */
val users: Flow<List<UserViewModel>> =
userInteractor.users.map { models -> models.map { user -> toViewModel(user) } }
@@ -216,7 +210,6 @@ private constructor(
private val userInteractor: UserInteractor,
private val guestUserInteractor: GuestUserInteractor,
private val powerInteractor: PowerInteractor,
private val featureFlags: FeatureFlags,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
@@ -224,7 +217,6 @@ private constructor(
userInteractor = userInteractor,
guestUserInteractor = guestUserInteractor,
powerInteractor = powerInteractor,
featureFlags = featureFlags,
)
as T
}

View File

@@ -42,27 +42,21 @@ import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@SmallTest
class UserDetailViewAdapterTest : SysuiTestCase() {
@Mock
private lateinit var mUserSwitcherController: UserSwitcherController
@Mock
private lateinit var mParent: ViewGroup
@Mock
private lateinit var mUserDetailItemView: UserDetailItemView
@Mock
private lateinit var mOtherView: View
@Mock
private lateinit var mInflatedUserDetailItemView: UserDetailItemView
@Mock
private lateinit var mLayoutInflater: LayoutInflater
@Mock private lateinit var mUserSwitcherController: UserSwitcherController
@Mock private lateinit var mParent: ViewGroup
@Mock private lateinit var mUserDetailItemView: UserDetailItemView
@Mock private lateinit var mOtherView: View
@Mock private lateinit var mInflatedUserDetailItemView: UserDetailItemView
@Mock private lateinit var mLayoutInflater: LayoutInflater
private var falsingManagerFake: FalsingManagerFake = FalsingManagerFake()
private lateinit var adapter: UserDetailView.Adapter
private lateinit var uiEventLogger: UiEventLoggerFake
@@ -77,10 +71,13 @@ class UserDetailViewAdapterTest : SysuiTestCase() {
`when`(mLayoutInflater.inflate(anyInt(), any(ViewGroup::class.java), anyBoolean()))
.thenReturn(mInflatedUserDetailItemView)
`when`(mParent.context).thenReturn(mContext)
adapter = UserDetailView.Adapter(
mContext, mUserSwitcherController, uiEventLogger,
falsingManagerFake
)
adapter =
UserDetailView.Adapter(
mContext,
mUserSwitcherController,
uiEventLogger,
falsingManagerFake
)
mPicture = UserIcons.convertToBitmap(mContext.getDrawable(R.drawable.ic_avatar_user))
}

View File

@@ -237,7 +237,7 @@ class BaseUserSwitcherAdapterTest : SysuiTestCase() {
fun refresh() {
underTest.refresh()
verify(controller).refreshUsers(UserHandle.USER_NULL)
verify(controller).refreshUsers()
}
private fun createUserRecord(

View File

@@ -1,727 +0,0 @@
/*
* Copyright (C) 2021 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.statusbar.policy
import android.app.IActivityManager
import android.app.NotificationManager
import android.app.admin.DevicePolicyManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.UserInfo
import android.graphics.Bitmap
import android.hardware.face.FaceManager
import android.hardware.fingerprint.FingerprintManager
import android.os.Handler
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.ThreadedRenderer
import androidx.test.filters.SmallTest
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.util.LatencyTracker
import com.android.internal.util.UserIcons
import com.android.systemui.GuestResetOrExitSessionReceiver
import com.android.systemui.GuestResumeSessionReceiver
import com.android.systemui.GuestSessionNotification
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.QSUserSwitcherEvent
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.settings.UserTracker
import com.android.systemui.shade.NotificationShadeWindowView
import com.android.systemui.telephony.TelephonyListenerManager
import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.kotlinArgumentCaptor
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.eq
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
class UserSwitcherControllerOldImplTest : SysuiTestCase() {
@Mock private lateinit var keyguardStateController: KeyguardStateController
@Mock private lateinit var activityManager: IActivityManager
@Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
@Mock private lateinit var devicePolicyManager: DevicePolicyManager
@Mock private lateinit var handler: Handler
@Mock private lateinit var userTracker: UserTracker
@Mock private lateinit var userManager: UserManager
@Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
@Mock private lateinit var broadcastSender: BroadcastSender
@Mock private lateinit var telephonyListenerManager: TelephonyListenerManager
@Mock private lateinit var secureSettings: SecureSettings
@Mock private lateinit var falsingManager: FalsingManager
@Mock private lateinit var dumpManager: DumpManager
@Mock private lateinit var interactionJankMonitor: InteractionJankMonitor
@Mock private lateinit var latencyTracker: LatencyTracker
@Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower
@Mock private lateinit var notificationShadeWindowView: NotificationShadeWindowView
@Mock private lateinit var threadedRenderer: ThreadedRenderer
@Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
@Mock private lateinit var globalSettings: GlobalSettings
@Mock private lateinit var guestSessionNotification: GuestSessionNotification
@Mock private lateinit var guestResetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
private lateinit var resetSessionDialogFactory:
GuestResumeSessionReceiver.ResetSessionDialog.Factory
private lateinit var guestResumeSessionReceiver: GuestResumeSessionReceiver
private lateinit var testableLooper: TestableLooper
private lateinit var bgExecutor: FakeExecutor
private lateinit var longRunningExecutor: FakeExecutor
private lateinit var uiExecutor: FakeExecutor
private lateinit var uiEventLogger: UiEventLoggerFake
private lateinit var userSwitcherController: UserSwitcherControllerOldImpl
private lateinit var picture: Bitmap
private val ownerId = UserHandle.USER_SYSTEM
private val ownerInfo = UserInfo(ownerId, "Owner", null,
UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL or UserInfo.FLAG_INITIALIZED or
UserInfo.FLAG_PRIMARY or UserInfo.FLAG_SYSTEM or UserInfo.FLAG_ADMIN,
UserManager.USER_TYPE_FULL_SYSTEM)
private val guestId = 1234
private val guestInfo = UserInfo(guestId, "Guest", null,
UserInfo.FLAG_FULL or UserInfo.FLAG_GUEST, UserManager.USER_TYPE_FULL_GUEST)
private val secondaryUser =
UserInfo(10, "Secondary", null, 0, UserManager.USER_TYPE_FULL_SECONDARY)
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
testableLooper = TestableLooper.get(this)
bgExecutor = FakeExecutor(FakeSystemClock())
longRunningExecutor = FakeExecutor(FakeSystemClock())
uiExecutor = FakeExecutor(FakeSystemClock())
uiEventLogger = UiEventLoggerFake()
mContext.orCreateTestableResources.addOverride(
com.android.internal.R.bool.config_guestUserAutoCreated, false)
mContext.addMockSystemService(Context.FACE_SERVICE, mock(FaceManager::class.java))
mContext.addMockSystemService(Context.NOTIFICATION_SERVICE,
mock(NotificationManager::class.java))
mContext.addMockSystemService(Context.FINGERPRINT_SERVICE,
mock(FingerprintManager::class.java))
resetSessionDialogFactory = object : GuestResumeSessionReceiver.ResetSessionDialog.Factory {
override fun create(userId: Int): GuestResumeSessionReceiver.ResetSessionDialog {
return GuestResumeSessionReceiver.ResetSessionDialog(
mContext,
mock(UserSwitcherController::class.java),
uiEventLogger,
userId
)
}
}
guestResumeSessionReceiver = GuestResumeSessionReceiver(userTracker,
secureSettings,
broadcastDispatcher,
guestSessionNotification,
resetSessionDialogFactory)
`when`(userManager.canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_SECONDARY)))
.thenReturn(true)
`when`(notificationShadeWindowView.context).thenReturn(context)
// Since userSwitcherController involves InteractionJankMonitor.
// Let's fulfill the dependencies.
val mockedContext = mock(Context::class.java)
doReturn(mockedContext).`when`(notificationShadeWindowView).context
doReturn(true).`when`(notificationShadeWindowView).isAttachedToWindow
doNothing().`when`(threadedRenderer).addObserver(any())
doNothing().`when`(threadedRenderer).removeObserver(any())
doReturn(threadedRenderer).`when`(notificationShadeWindowView).threadedRenderer
picture = UserIcons.convertToBitmap(context.getDrawable(R.drawable.ic_avatar_user))
// Create defaults for the current user
`when`(userTracker.userId).thenReturn(ownerId)
`when`(userTracker.userInfo).thenReturn(ownerInfo)
`when`(
globalSettings.getIntForUser(
eq(Settings.Global.ADD_USERS_WHEN_LOCKED),
anyInt(),
eq(UserHandle.USER_SYSTEM)
)
).thenReturn(0)
`when`(
globalSettings.getIntForUser(
eq(Settings.Global.USER_SWITCHER_ENABLED),
anyInt(),
eq(UserHandle.USER_SYSTEM)
)
).thenReturn(1)
setupController()
}
private fun setupController() {
userSwitcherController =
UserSwitcherControllerOldImpl(
mContext,
activityManager,
userManager,
userTracker,
keyguardStateController,
deviceProvisionedController,
devicePolicyManager,
handler,
activityStarter,
broadcastDispatcher,
broadcastSender,
uiEventLogger,
falsingManager,
telephonyListenerManager,
secureSettings,
globalSettings,
bgExecutor,
longRunningExecutor,
uiExecutor,
interactionJankMonitor,
latencyTracker,
dumpManager,
dialogLaunchAnimator,
guestResumeSessionReceiver,
guestResetOrExitSessionReceiver
)
userSwitcherController.init(notificationShadeWindowView)
}
@Test
fun testSwitchUser_parentDialogDismissed() {
val otherUserRecord = UserRecord(
secondaryUser,
picture,
false /* guest */,
false /* current */,
false /* isAddUser */,
false /* isRestricted */,
true /* isSwitchToEnabled */,
false /* isAddSupervisedUser */
)
`when`(userTracker.userId).thenReturn(ownerId)
`when`(userTracker.userInfo).thenReturn(ownerInfo)
userSwitcherController.onUserListItemClicked(otherUserRecord, dialogShower)
testableLooper.processAllMessages()
verify(dialogShower).dismiss()
}
@Test
fun testAddGuest_okButtonPressed() {
val emptyGuestUserRecord =
UserRecord(
null,
null,
true /* guest */,
false /* current */,
false /* isAddUser */,
false /* isRestricted */,
true /* isSwitchToEnabled */,
false /* isAddSupervisedUser */
)
`when`(userTracker.userId).thenReturn(ownerId)
`when`(userTracker.userInfo).thenReturn(ownerInfo)
`when`(userManager.createGuest(any())).thenReturn(guestInfo)
userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, null)
bgExecutor.runAllReady()
uiExecutor.runAllReady()
testableLooper.processAllMessages()
verify(interactionJankMonitor).begin(any())
verify(latencyTracker).onActionStart(LatencyTracker.ACTION_USER_SWITCH)
verify(activityManager).switchUser(guestInfo.id)
assertEquals(1, uiEventLogger.numLogs())
assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_ADD.id, uiEventLogger.eventId(0))
}
@Test
fun testAddGuest_parentDialogDismissed() {
val emptyGuestUserRecord =
UserRecord(
null,
null,
true /* guest */,
false /* current */,
false /* isAddUser */,
false /* isRestricted */,
true /* isSwitchToEnabled */,
false /* isAddSupervisedUser */
)
`when`(userTracker.userId).thenReturn(ownerId)
`when`(userTracker.userInfo).thenReturn(ownerInfo)
`when`(userManager.createGuest(any())).thenReturn(guestInfo)
userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, dialogShower)
bgExecutor.runAllReady()
uiExecutor.runAllReady()
testableLooper.processAllMessages()
verify(dialogShower).dismiss()
}
@Test
fun testRemoveGuest_removeButtonPressed_isLogged() {
val currentGuestUserRecord =
UserRecord(
guestInfo,
picture,
true /* guest */,
true /* current */,
false /* isAddUser */,
false /* isRestricted */,
true /* isSwitchToEnabled */,
false /* isAddSupervisedUser */
)
`when`(userTracker.userId).thenReturn(guestInfo.id)
`when`(userTracker.userInfo).thenReturn(guestInfo)
userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
assertNotNull(userSwitcherController.mExitGuestDialog)
userSwitcherController.mExitGuestDialog
.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
testableLooper.processAllMessages()
assertEquals(1, uiEventLogger.numLogs())
assertTrue(
QSUserSwitcherEvent.QS_USER_GUEST_REMOVE.id == uiEventLogger.eventId(0) ||
QSUserSwitcherEvent.QS_USER_SWITCH.id == uiEventLogger.eventId(0)
)
}
@Test
fun testRemoveGuest_removeButtonPressed_dialogDismissed() {
val currentGuestUserRecord =
UserRecord(
guestInfo,
picture,
true /* guest */,
true /* current */,
false /* isAddUser */,
false /* isRestricted */,
true /* isSwitchToEnabled */,
false /* isAddSupervisedUser */
)
`when`(userTracker.userId).thenReturn(guestInfo.id)
`when`(userTracker.userInfo).thenReturn(guestInfo)
userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
assertNotNull(userSwitcherController.mExitGuestDialog)
userSwitcherController.mExitGuestDialog
.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
testableLooper.processAllMessages()
assertFalse(userSwitcherController.mExitGuestDialog.isShowing)
}
@Test
fun testRemoveGuest_dialogShowerUsed() {
val currentGuestUserRecord =
UserRecord(
guestInfo,
picture,
true /* guest */,
true /* current */,
false /* isAddUser */,
false /* isRestricted */,
true /* isSwitchToEnabled */,
false /* isAddSupervisedUser */
)
`when`(userTracker.userId).thenReturn(guestInfo.id)
`when`(userTracker.userInfo).thenReturn(guestInfo)
userSwitcherController.onUserListItemClicked(currentGuestUserRecord, dialogShower)
assertNotNull(userSwitcherController.mExitGuestDialog)
testableLooper.processAllMessages()
verify(dialogShower)
.showDialog(
userSwitcherController.mExitGuestDialog,
DialogCuj(InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, "exit_guest_mode"))
}
@Test
fun testRemoveGuest_cancelButtonPressed_isNotLogged() {
val currentGuestUserRecord =
UserRecord(
guestInfo,
picture,
true /* guest */,
true /* current */,
false /* isAddUser */,
false /* isRestricted */,
true /* isSwitchToEnabled */,
false /* isAddSupervisedUser */
)
`when`(userTracker.userId).thenReturn(guestId)
`when`(userTracker.userInfo).thenReturn(guestInfo)
userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
assertNotNull(userSwitcherController.mExitGuestDialog)
userSwitcherController.mExitGuestDialog
.getButton(DialogInterface.BUTTON_NEUTRAL).performClick()
testableLooper.processAllMessages()
assertEquals(0, uiEventLogger.numLogs())
}
@Test
fun testWipeGuest_startOverButtonPressed_isLogged() {
val currentGuestUserRecord =
UserRecord(
guestInfo,
picture,
true /* guest */,
false /* current */,
false /* isAddUser */,
false /* isRestricted */,
true /* isSwitchToEnabled */,
false /* isAddSupervisedUser */
)
`when`(userTracker.userId).thenReturn(guestId)
`when`(userTracker.userInfo).thenReturn(guestInfo)
// Simulate that guest user has already logged in
`when`(secureSettings.getIntForUser(
eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt()))
.thenReturn(1)
userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
// Simulate a user switch event
val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId)
assertNotNull(userSwitcherController.mGuestResumeSessionReceiver)
userSwitcherController.mGuestResumeSessionReceiver.onReceive(context, intent)
assertNotNull(userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog)
userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog
.getButton(GuestResumeSessionReceiver.ResetSessionDialog.BUTTON_WIPE).performClick()
testableLooper.processAllMessages()
assertEquals(1, uiEventLogger.numLogs())
assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_WIPE.id, uiEventLogger.eventId(0))
}
@Test
fun testWipeGuest_continueButtonPressed_isLogged() {
val currentGuestUserRecord =
UserRecord(
guestInfo,
picture,
true /* guest */,
false /* current */,
false /* isAddUser */,
false /* isRestricted */,
true /* isSwitchToEnabled */,
false /* isAddSupervisedUser */
)
`when`(userTracker.userId).thenReturn(guestId)
`when`(userTracker.userInfo).thenReturn(guestInfo)
// Simulate that guest user has already logged in
`when`(secureSettings.getIntForUser(
eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt()))
.thenReturn(1)
userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
// Simulate a user switch event
val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId)
assertNotNull(userSwitcherController.mGuestResumeSessionReceiver)
userSwitcherController.mGuestResumeSessionReceiver.onReceive(context, intent)
assertNotNull(userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog)
userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog
.getButton(GuestResumeSessionReceiver.ResetSessionDialog.BUTTON_DONTWIPE)
.performClick()
testableLooper.processAllMessages()
assertEquals(1, uiEventLogger.numLogs())
assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_CONTINUE.id, uiEventLogger.eventId(0))
}
@Test
fun test_getCurrentUserName_shouldReturnNameOfTheCurrentUser() {
fun addUser(id: Int, name: String, isCurrent: Boolean) {
userSwitcherController.users.add(
UserRecord(
UserInfo(id, name, 0),
null, false, isCurrent, false,
false, false, false
)
)
}
val bgUserName = "background_user"
val fgUserName = "foreground_user"
addUser(1, bgUserName, false)
addUser(2, fgUserName, true)
assertEquals(fgUserName, userSwitcherController.currentUserName)
}
@Test
fun isSystemUser_currentUserIsSystemUser_shouldReturnTrue() {
`when`(userTracker.userId).thenReturn(UserHandle.USER_SYSTEM)
assertEquals(true, userSwitcherController.isSystemUser)
}
@Test
fun isSystemUser_currentUserIsNotSystemUser_shouldReturnFalse() {
`when`(userTracker.userId).thenReturn(1)
assertEquals(false, userSwitcherController.isSystemUser)
}
@Test
fun testCanCreateSupervisedUserWithConfiguredPackage() {
// GIVEN the supervised user creation package is configured
`when`(context.getString(
com.android.internal.R.string.config_supervisedUserCreationPackage))
.thenReturn("some_pkg")
// AND the current user is allowed to create new users
`when`(userTracker.userId).thenReturn(ownerId)
`when`(userTracker.userInfo).thenReturn(ownerInfo)
// WHEN the controller is started with the above config
setupController()
testableLooper.processAllMessages()
// THEN a supervised user can be constructed
assertTrue(userSwitcherController.canCreateSupervisedUser())
}
@Test
fun testCannotCreateSupervisedUserWithConfiguredPackage() {
// GIVEN the supervised user creation package is NOT configured
`when`(context.getString(
com.android.internal.R.string.config_supervisedUserCreationPackage))
.thenReturn(null)
// AND the current user is allowed to create new users
`when`(userTracker.userId).thenReturn(ownerId)
`when`(userTracker.userInfo).thenReturn(ownerInfo)
// WHEN the controller is started with the above config
setupController()
testableLooper.processAllMessages()
// THEN a supervised user can NOT be constructed
assertFalse(userSwitcherController.canCreateSupervisedUser())
}
@Test
fun testCannotCreateUserWhenUserSwitcherDisabled() {
`when`(
globalSettings.getIntForUser(
eq(Settings.Global.USER_SWITCHER_ENABLED),
anyInt(),
eq(UserHandle.USER_SYSTEM)
)
).thenReturn(0)
setupController()
assertFalse(userSwitcherController.canCreateUser())
}
@Test
fun testCannotCreateGuestUserWhenUserSwitcherDisabled() {
`when`(
globalSettings.getIntForUser(
eq(Settings.Global.USER_SWITCHER_ENABLED),
anyInt(),
eq(UserHandle.USER_SYSTEM)
)
).thenReturn(0)
setupController()
assertFalse(userSwitcherController.canCreateGuest(false))
}
@Test
fun testCannotCreateSupervisedUserWhenUserSwitcherDisabled() {
`when`(
globalSettings.getIntForUser(
eq(Settings.Global.USER_SWITCHER_ENABLED),
anyInt(),
eq(UserHandle.USER_SYSTEM)
)
).thenReturn(0)
setupController()
assertFalse(userSwitcherController.canCreateSupervisedUser())
}
@Test
fun testCanManageUser_userSwitcherEnabled_addUserWhenLocked() {
`when`(
globalSettings.getIntForUser(
eq(Settings.Global.USER_SWITCHER_ENABLED),
anyInt(),
eq(UserHandle.USER_SYSTEM)
)
).thenReturn(1)
`when`(
globalSettings.getIntForUser(
eq(Settings.Global.ADD_USERS_WHEN_LOCKED),
anyInt(),
eq(UserHandle.USER_SYSTEM)
)
).thenReturn(1)
setupController()
assertTrue(userSwitcherController.canManageUsers())
}
@Test
fun testCanManageUser_userSwitcherDisabled_addUserWhenLocked() {
`when`(
globalSettings.getIntForUser(
eq(Settings.Global.USER_SWITCHER_ENABLED),
anyInt(),
eq(UserHandle.USER_SYSTEM)
)
).thenReturn(0)
`when`(
globalSettings.getIntForUser(
eq(Settings.Global.ADD_USERS_WHEN_LOCKED),
anyInt(),
eq(UserHandle.USER_SYSTEM)
)
).thenReturn(1)
setupController()
assertFalse(userSwitcherController.canManageUsers())
}
@Test
fun testCanManageUser_userSwitcherEnabled_isAdmin() {
`when`(
globalSettings.getIntForUser(
eq(Settings.Global.USER_SWITCHER_ENABLED),
anyInt(),
eq(UserHandle.USER_SYSTEM)
)
).thenReturn(1)
setupController()
assertTrue(userSwitcherController.canManageUsers())
}
@Test
fun testCanManageUser_userSwitcherDisabled_isAdmin() {
`when`(
globalSettings.getIntForUser(
eq(Settings.Global.USER_SWITCHER_ENABLED),
anyInt(),
eq(UserHandle.USER_SYSTEM)
)
).thenReturn(0)
setupController()
assertFalse(userSwitcherController.canManageUsers())
}
@Test
fun addUserSwitchCallback() {
val broadcastReceiverCaptor = argumentCaptor<BroadcastReceiver>()
verify(broadcastDispatcher).registerReceiver(
capture(broadcastReceiverCaptor),
any(),
nullable(), nullable(), anyInt(), nullable())
val cb = mock(UserSwitcherController.UserSwitchCallback::class.java)
userSwitcherController.addUserSwitchCallback(cb)
val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId)
broadcastReceiverCaptor.value.onReceive(context, intent)
verify(cb).onUserSwitched()
}
@Test
fun onUserItemClicked_guest_runsOnBgThread() {
val dialogShower = mock(UserSwitchDialogController.DialogShower::class.java)
val guestUserRecord = UserRecord(
null,
picture,
true /* guest */,
false /* current */,
false /* isAddUser */,
false /* isRestricted */,
true /* isSwitchToEnabled */,
false /* isAddSupervisedUser */
)
userSwitcherController.onUserListItemClicked(guestUserRecord, dialogShower)
assertTrue(bgExecutor.numPending() > 0)
verify(userManager, never()).createGuest(context)
bgExecutor.runAllReady()
verify(userManager).createGuest(context)
}
@Test
fun onUserItemClicked_manageUsers() {
val manageUserRecord = LegacyUserDataHelper.createRecord(
mContext,
ownerId,
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
isRestricted = false,
isSwitchToEnabled = true
)
userSwitcherController.onUserListItemClicked(manageUserRecord, null)
val intentCaptor = kotlinArgumentCaptor<Intent>()
verify(activityStarter).startActivity(intentCaptor.capture(),
eq(true)
)
Truth.assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS)
}
}

View File

@@ -1,248 +0,0 @@
/*
* 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.data.repository
import android.content.pm.UserInfo
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
import androidx.test.filters.SmallTest
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(JUnit4::class)
class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() {
@Before
fun setUp() {
super.setUp(isRefactored = true)
}
@Test
fun userSwitcherSettings() = runSelfCancelingTest {
setUpGlobalSettings(
isSimpleUserSwitcher = true,
isAddUsersFromLockscreen = true,
isUserSwitcherEnabled = true,
)
underTest = create(this)
var value: UserSwitcherSettingsModel? = null
underTest.userSwitcherSettings.onEach { value = it }.launchIn(this)
assertUserSwitcherSettings(
model = value,
expectedSimpleUserSwitcher = true,
expectedAddUsersFromLockscreen = true,
expectedUserSwitcherEnabled = true,
)
setUpGlobalSettings(
isSimpleUserSwitcher = false,
isAddUsersFromLockscreen = true,
isUserSwitcherEnabled = true,
)
assertUserSwitcherSettings(
model = value,
expectedSimpleUserSwitcher = false,
expectedAddUsersFromLockscreen = true,
expectedUserSwitcherEnabled = true,
)
}
@Test
fun refreshUsers() = runSelfCancelingTest {
underTest = create(this)
val initialExpectedValue =
setUpUsers(
count = 3,
selectedIndex = 0,
)
var userInfos: List<UserInfo>? = null
var selectedUserInfo: UserInfo? = null
underTest.userInfos.onEach { userInfos = it }.launchIn(this)
underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)
underTest.refreshUsers()
assertThat(userInfos).isEqualTo(initialExpectedValue)
assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0])
assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
val secondExpectedValue =
setUpUsers(
count = 4,
selectedIndex = 1,
)
underTest.refreshUsers()
assertThat(userInfos).isEqualTo(secondExpectedValue)
assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1])
assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
val selectedNonGuestUserId = selectedUserInfo?.id
val thirdExpectedValue =
setUpUsers(
count = 2,
isLastGuestUser = true,
selectedIndex = 1,
)
underTest.refreshUsers()
assertThat(userInfos).isEqualTo(thirdExpectedValue)
assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1])
assertThat(selectedUserInfo?.isGuest).isTrue()
assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId)
}
@Test
fun `refreshUsers - sorts by creation time - guest user last`() = runSelfCancelingTest {
underTest = create(this)
val unsortedUsers =
setUpUsers(
count = 3,
selectedIndex = 0,
isLastGuestUser = true,
)
unsortedUsers[0].creationTime = 999
unsortedUsers[1].creationTime = 900
unsortedUsers[2].creationTime = 950
val expectedUsers =
listOf(
unsortedUsers[1],
unsortedUsers[0],
unsortedUsers[2], // last because this is the guest
)
var userInfos: List<UserInfo>? = null
underTest.userInfos.onEach { userInfos = it }.launchIn(this)
underTest.refreshUsers()
assertThat(userInfos).isEqualTo(expectedUsers)
}
@Test
fun `userTrackerCallback - updates selectedUserInfo`() = runSelfCancelingTest {
underTest = create(this)
var selectedUserInfo: UserInfo? = null
underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)
setUpUsers(
count = 2,
selectedIndex = 0,
)
tracker.onProfileChanged()
assertThat(selectedUserInfo?.id == 0)
setUpUsers(
count = 2,
selectedIndex = 1,
)
tracker.onProfileChanged()
assertThat(selectedUserInfo?.id == 1)
}
private fun setUpUsers(
count: Int,
isLastGuestUser: Boolean = false,
selectedIndex: Int = 0,
): List<UserInfo> {
val userInfos =
(0 until count).map { index ->
createUserInfo(
index,
isGuest = isLastGuestUser && index == count - 1,
)
}
whenever(manager.aliveUsers).thenReturn(userInfos)
tracker.set(userInfos, selectedIndex)
return userInfos
}
private fun createUserInfo(
id: Int,
isGuest: Boolean,
): UserInfo {
val flags = 0
return UserInfo(
id,
"user_$id",
/* iconPath= */ "",
flags,
if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags),
)
}
private fun setUpGlobalSettings(
isSimpleUserSwitcher: Boolean = false,
isAddUsersFromLockscreen: Boolean = false,
isUserSwitcherEnabled: Boolean = true,
) {
context.orCreateTestableResources.addOverride(
com.android.internal.R.bool.config_expandLockScreenUserSwitcher,
true,
)
globalSettings.putIntForUser(
UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER,
if (isSimpleUserSwitcher) 1 else 0,
UserHandle.USER_SYSTEM,
)
globalSettings.putIntForUser(
Settings.Global.ADD_USERS_WHEN_LOCKED,
if (isAddUsersFromLockscreen) 1 else 0,
UserHandle.USER_SYSTEM,
)
globalSettings.putIntForUser(
Settings.Global.USER_SWITCHER_ENABLED,
if (isUserSwitcherEnabled) 1 else 0,
UserHandle.USER_SYSTEM,
)
}
private fun assertUserSwitcherSettings(
model: UserSwitcherSettingsModel?,
expectedSimpleUserSwitcher: Boolean,
expectedAddUsersFromLockscreen: Boolean,
expectedUserSwitcherEnabled: Boolean,
) {
checkNotNull(model)
assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher)
assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen)
assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled)
}
/**
* Executes the given block of execution within the scope of a dedicated [CoroutineScope] which
* is then automatically canceled and cleaned-up.
*/
private fun runSelfCancelingTest(
block: suspend CoroutineScope.() -> Unit,
) =
runBlocking(Dispatchers.Main.immediate) {
val scope = CoroutineScope(coroutineContext + Job())
block(scope)
scope.cancel()
}
}

View File

@@ -17,54 +17,263 @@
package com.android.systemui.user.data.repository
import android.content.pm.UserInfo
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.statusbar.policy.UserSwitcherController
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
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.Mock
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
abstract class UserRepositoryImplTest : SysuiTestCase() {
@SmallTest
@RunWith(JUnit4::class)
class UserRepositoryImplTest : SysuiTestCase() {
@Mock protected lateinit var manager: UserManager
@Mock protected lateinit var controller: UserSwitcherController
@Mock private lateinit var manager: UserManager
protected lateinit var underTest: UserRepositoryImpl
private lateinit var underTest: UserRepositoryImpl
protected lateinit var globalSettings: FakeSettings
protected lateinit var tracker: FakeUserTracker
protected lateinit var featureFlags: FakeFeatureFlags
private lateinit var globalSettings: FakeSettings
private lateinit var tracker: FakeUserTracker
protected fun setUp(isRefactored: Boolean) {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
globalSettings = FakeSettings()
tracker = FakeUserTracker()
featureFlags = FakeFeatureFlags()
featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored)
}
protected fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl {
@Test
fun userSwitcherSettings() = runSelfCancelingTest {
setUpGlobalSettings(
isSimpleUserSwitcher = true,
isAddUsersFromLockscreen = true,
isUserSwitcherEnabled = true,
)
underTest = create(this)
var value: UserSwitcherSettingsModel? = null
underTest.userSwitcherSettings.onEach { value = it }.launchIn(this)
assertUserSwitcherSettings(
model = value,
expectedSimpleUserSwitcher = true,
expectedAddUsersFromLockscreen = true,
expectedUserSwitcherEnabled = true,
)
setUpGlobalSettings(
isSimpleUserSwitcher = false,
isAddUsersFromLockscreen = true,
isUserSwitcherEnabled = true,
)
assertUserSwitcherSettings(
model = value,
expectedSimpleUserSwitcher = false,
expectedAddUsersFromLockscreen = true,
expectedUserSwitcherEnabled = true,
)
}
@Test
fun refreshUsers() = runSelfCancelingTest {
underTest = create(this)
val initialExpectedValue =
setUpUsers(
count = 3,
selectedIndex = 0,
)
var userInfos: List<UserInfo>? = null
var selectedUserInfo: UserInfo? = null
underTest.userInfos.onEach { userInfos = it }.launchIn(this)
underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)
underTest.refreshUsers()
assertThat(userInfos).isEqualTo(initialExpectedValue)
assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0])
assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
val secondExpectedValue =
setUpUsers(
count = 4,
selectedIndex = 1,
)
underTest.refreshUsers()
assertThat(userInfos).isEqualTo(secondExpectedValue)
assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1])
assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
val selectedNonGuestUserId = selectedUserInfo?.id
val thirdExpectedValue =
setUpUsers(
count = 2,
isLastGuestUser = true,
selectedIndex = 1,
)
underTest.refreshUsers()
assertThat(userInfos).isEqualTo(thirdExpectedValue)
assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1])
assertThat(selectedUserInfo?.isGuest).isTrue()
assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId)
}
@Test
fun `refreshUsers - sorts by creation time - guest user last`() = runSelfCancelingTest {
underTest = create(this)
val unsortedUsers =
setUpUsers(
count = 3,
selectedIndex = 0,
isLastGuestUser = true,
)
unsortedUsers[0].creationTime = 999
unsortedUsers[1].creationTime = 900
unsortedUsers[2].creationTime = 950
val expectedUsers =
listOf(
unsortedUsers[1],
unsortedUsers[0],
unsortedUsers[2], // last because this is the guest
)
var userInfos: List<UserInfo>? = null
underTest.userInfos.onEach { userInfos = it }.launchIn(this)
underTest.refreshUsers()
assertThat(userInfos).isEqualTo(expectedUsers)
}
private fun setUpUsers(
count: Int,
isLastGuestUser: Boolean = false,
selectedIndex: Int = 0,
): List<UserInfo> {
val userInfos =
(0 until count).map { index ->
createUserInfo(
index,
isGuest = isLastGuestUser && index == count - 1,
)
}
whenever(manager.aliveUsers).thenReturn(userInfos)
tracker.set(userInfos, selectedIndex)
return userInfos
}
@Test
fun `userTrackerCallback - updates selectedUserInfo`() = runSelfCancelingTest {
underTest = create(this)
var selectedUserInfo: UserInfo? = null
underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)
setUpUsers(
count = 2,
selectedIndex = 0,
)
tracker.onProfileChanged()
assertThat(selectedUserInfo?.id).isEqualTo(0)
setUpUsers(
count = 2,
selectedIndex = 1,
)
tracker.onProfileChanged()
assertThat(selectedUserInfo?.id).isEqualTo(1)
}
private fun createUserInfo(
id: Int,
isGuest: Boolean,
): UserInfo {
val flags = 0
return UserInfo(
id,
"user_$id",
/* iconPath= */ "",
flags,
if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags),
)
}
private fun setUpGlobalSettings(
isSimpleUserSwitcher: Boolean = false,
isAddUsersFromLockscreen: Boolean = false,
isUserSwitcherEnabled: Boolean = true,
) {
context.orCreateTestableResources.addOverride(
com.android.internal.R.bool.config_expandLockScreenUserSwitcher,
true,
)
globalSettings.putIntForUser(
UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER,
if (isSimpleUserSwitcher) 1 else 0,
UserHandle.USER_SYSTEM,
)
globalSettings.putIntForUser(
Settings.Global.ADD_USERS_WHEN_LOCKED,
if (isAddUsersFromLockscreen) 1 else 0,
UserHandle.USER_SYSTEM,
)
globalSettings.putIntForUser(
Settings.Global.USER_SWITCHER_ENABLED,
if (isUserSwitcherEnabled) 1 else 0,
UserHandle.USER_SYSTEM,
)
}
private fun assertUserSwitcherSettings(
model: UserSwitcherSettingsModel?,
expectedSimpleUserSwitcher: Boolean,
expectedAddUsersFromLockscreen: Boolean,
expectedUserSwitcherEnabled: Boolean,
) {
checkNotNull(model)
assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher)
assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen)
assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled)
}
/**
* Executes the given block of execution within the scope of a dedicated [CoroutineScope] which
* is then automatically canceled and cleaned-up.
*/
private fun runSelfCancelingTest(
block: suspend CoroutineScope.() -> Unit,
) =
runBlocking(Dispatchers.Main.immediate) {
val scope = CoroutineScope(coroutineContext + Job())
block(scope)
scope.cancel()
}
private fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl {
return UserRepositoryImpl(
appContext = context,
manager = manager,
controller = controller,
applicationScope = scope,
mainDispatcher = IMMEDIATE,
backgroundDispatcher = IMMEDIATE,
globalSettings = globalSettings,
tracker = tracker,
featureFlags = featureFlags,
)
}
companion object {
@JvmStatic protected val IMMEDIATE = Dispatchers.Main.immediate
@JvmStatic private val IMMEDIATE = Dispatchers.Main.immediate
}
}

View File

@@ -1,209 +0,0 @@
/*
* 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.data.repository
import android.content.pm.UserInfo
import androidx.test.filters.SmallTest
import com.android.systemui.statusbar.policy.UserSwitcherController
import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(JUnit4::class)
class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() {
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
}
@Captor
private lateinit var userSwitchCallbackCaptor:
ArgumentCaptor<UserSwitcherController.UserSwitchCallback>
@Before
fun setUp() {
super.setUp(isRefactored = false)
whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false))
whenever(controller.isGuestUserAutoCreated).thenReturn(false)
whenever(controller.isGuestUserResetting).thenReturn(false)
underTest = create()
}
@Test
fun `users - registers for updates`() =
runBlocking(IMMEDIATE) {
val job = underTest.users.onEach {}.launchIn(this)
verify(controller).addUserSwitchCallback(any())
job.cancel()
}
@Test
fun `users - unregisters from updates`() =
runBlocking(IMMEDIATE) {
val job = underTest.users.onEach {}.launchIn(this)
verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
job.cancel()
verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
}
@Test
fun `users - does not include actions`() =
runBlocking(IMMEDIATE) {
whenever(controller.users)
.thenReturn(
arrayListOf(
createUserRecord(0, isSelected = true),
createActionRecord(UserActionModel.ADD_USER),
createUserRecord(1),
createUserRecord(2),
createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
createActionRecord(UserActionModel.ENTER_GUEST_MODE),
createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT),
)
)
var models: List<UserModel>? = null
val job = underTest.users.onEach { models = it }.launchIn(this)
assertThat(models).hasSize(3)
assertThat(models?.get(0)?.id).isEqualTo(0)
assertThat(models?.get(0)?.isSelected).isTrue()
assertThat(models?.get(1)?.id).isEqualTo(1)
assertThat(models?.get(1)?.isSelected).isFalse()
assertThat(models?.get(2)?.id).isEqualTo(2)
assertThat(models?.get(2)?.isSelected).isFalse()
job.cancel()
}
@Test
fun selectedUser() =
runBlocking(IMMEDIATE) {
whenever(controller.users)
.thenReturn(
arrayListOf(
createUserRecord(0, isSelected = true),
createUserRecord(1),
createUserRecord(2),
)
)
var id: Int? = null
val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this)
assertThat(id).isEqualTo(0)
whenever(controller.users)
.thenReturn(
arrayListOf(
createUserRecord(0),
createUserRecord(1),
createUserRecord(2, isSelected = true),
)
)
verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
userSwitchCallbackCaptor.value.onUserSwitched()
assertThat(id).isEqualTo(2)
job.cancel()
}
@Test
fun `actions - unregisters from updates`() =
runBlocking(IMMEDIATE) {
val job = underTest.actions.onEach {}.launchIn(this)
verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
job.cancel()
verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
}
@Test
fun `actions - registers for updates`() =
runBlocking(IMMEDIATE) {
val job = underTest.actions.onEach {}.launchIn(this)
verify(controller).addUserSwitchCallback(any())
job.cancel()
}
@Test
fun `actions - does not include users`() =
runBlocking(IMMEDIATE) {
whenever(controller.users)
.thenReturn(
arrayListOf(
createUserRecord(0, isSelected = true),
createActionRecord(UserActionModel.ADD_USER),
createUserRecord(1),
createUserRecord(2),
createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
createActionRecord(UserActionModel.ENTER_GUEST_MODE),
createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT),
)
)
var models: List<UserActionModel>? = null
val job = underTest.actions.onEach { models = it }.launchIn(this)
assertThat(models).hasSize(4)
assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER)
assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER)
assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE)
assertThat(models?.get(3)).isEqualTo(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
job.cancel()
}
private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord {
return UserRecord(
info = UserInfo(id, "name$id", 0),
isCurrent = isSelected,
)
}
private fun createActionRecord(action: UserActionModel): UserRecord {
return UserRecord(
isAddUser = action == UserActionModel.ADD_USER,
isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER,
isGuest = action == UserActionModel.ENTER_GUEST_MODE,
isManageUsers = action == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
}
}

View File

@@ -1,740 +0,0 @@
/*
* 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.content.Intent
import android.content.pm.UserInfo
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
import androidx.test.filters.SmallTest
import com.android.internal.R.drawable.ic_account_circle
import com.android.systemui.R
import com.android.systemui.common.shared.model.Text
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.domain.model.ShowDialogRequestModel
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.kotlinArgumentCaptor
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.advanceUntilIdle
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
@SmallTest
@RunWith(JUnit4::class)
class UserInteractorRefactoredTest : UserInteractorTest() {
override fun isRefactored(): Boolean {
return true
}
@Before
override fun setUp() {
super.setUp()
overrideResource(R.drawable.ic_account_circle, GUEST_ICON)
overrideResource(R.dimen.max_avatar_size, 10)
overrideResource(
com.android.internal.R.string.config_supervisedUserCreationPackage,
SUPERVISED_USER_CREATION_APP_PACKAGE,
)
whenever(manager.getUserIcon(anyInt())).thenReturn(ICON)
whenever(manager.canAddMoreUsers(any())).thenReturn(true)
}
@Test
fun `onRecordSelected - user`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 3, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
underTest.onRecordSelected(UserRecord(info = userInfos[1]), dialogShower)
verify(dialogShower).dismiss()
verify(activityManager).switchUser(userInfos[1].id)
Unit
}
@Test
fun `onRecordSelected - switch to guest user`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 3, includeGuest = true)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
underTest.onRecordSelected(UserRecord(info = userInfos.last()))
verify(activityManager).switchUser(userInfos.last().id)
Unit
}
@Test
fun `onRecordSelected - enter guest mode`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 3, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true)
whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower)
verify(dialogShower).dismiss()
verify(manager).createGuest(any())
Unit
}
@Test
fun `onRecordSelected - action`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 3, includeGuest = true)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
underTest.onRecordSelected(UserRecord(isAddSupervisedUser = true), dialogShower)
verify(dialogShower, never()).dismiss()
verify(activityStarter).startActivity(any(), anyBoolean())
}
@Test
fun `users - switcher enabled`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 3, includeGuest = true)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
var value: List<UserModel>? = null
val job = underTest.users.onEach { value = it }.launchIn(this)
assertUsers(models = value, count = 3, includeGuest = true)
job.cancel()
}
@Test
fun `users - switches to second user`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
var value: List<UserModel>? = null
val job = underTest.users.onEach { value = it }.launchIn(this)
userRepository.setSelectedUserInfo(userInfos[1])
assertUsers(models = value, count = 2, selectedIndex = 1)
job.cancel()
}
@Test
fun `users - switcher not enabled`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
var value: List<UserModel>? = null
val job = underTest.users.onEach { value = it }.launchIn(this)
assertUsers(models = value, count = 1)
job.cancel()
}
@Test
fun selectedUser() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
var value: UserModel? = null
val job = underTest.selectedUser.onEach { value = it }.launchIn(this)
assertUser(value, id = 0, isSelected = true)
userRepository.setSelectedUserInfo(userInfos[1])
assertUser(value, id = 1, isSelected = true)
job.cancel()
}
@Test
fun `actions - device unlocked`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
keyguardRepository.setKeyguardShowing(false)
var value: List<UserActionModel>? = null
val job = underTest.actions.onEach { value = it }.launchIn(this)
assertThat(value)
.isEqualTo(
listOf(
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.ADD_USER,
UserActionModel.ADD_SUPERVISED_USER,
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
)
job.cancel()
}
@Test
fun `actions - device unlocked user not primary - empty list`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[1])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
keyguardRepository.setKeyguardShowing(false)
var value: List<UserActionModel>? = null
val job = underTest.actions.onEach { value = it }.launchIn(this)
assertThat(value).isEqualTo(emptyList<UserActionModel>())
job.cancel()
}
@Test
fun `actions - device unlocked user is guest - empty list`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = true)
assertThat(userInfos[1].isGuest).isTrue()
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[1])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
keyguardRepository.setKeyguardShowing(false)
var value: List<UserActionModel>? = null
val job = underTest.actions.onEach { value = it }.launchIn(this)
assertThat(value).isEqualTo(emptyList<UserActionModel>())
job.cancel()
}
@Test
fun `actions - device locked add from lockscreen set - full list`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(
UserSwitcherSettingsModel(
isUserSwitcherEnabled = true,
isAddUsersFromLockscreen = true,
)
)
keyguardRepository.setKeyguardShowing(false)
var value: List<UserActionModel>? = null
val job = underTest.actions.onEach { value = it }.launchIn(this)
assertThat(value)
.isEqualTo(
listOf(
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.ADD_USER,
UserActionModel.ADD_SUPERVISED_USER,
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
)
job.cancel()
}
@Test
fun `actions - device locked - only guest action and manage user is shown`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
keyguardRepository.setKeyguardShowing(true)
var value: List<UserActionModel>? = null
val job = underTest.actions.onEach { value = it }.launchIn(this)
assertThat(value)
.isEqualTo(
listOf(
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT
)
)
job.cancel()
}
@Test
fun `executeAction - add user - dialog shown`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
keyguardRepository.setKeyguardShowing(false)
var dialogRequest: ShowDialogRequestModel? = null
val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
val dialogShower: UserSwitchDialogController.DialogShower = mock()
underTest.executeAction(UserActionModel.ADD_USER, dialogShower)
assertThat(dialogRequest)
.isEqualTo(
ShowDialogRequestModel.ShowAddUserDialog(
userHandle = userInfos[0].userHandle,
isKeyguardShowing = false,
showEphemeralMessage = false,
dialogShower = dialogShower,
)
)
underTest.onDialogShown()
assertThat(dialogRequest).isNull()
job.cancel()
}
@Test
fun `executeAction - add supervised user - starts activity`() =
runBlocking(IMMEDIATE) {
underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
val intentCaptor = kotlinArgumentCaptor<Intent>()
verify(activityStarter).startActivity(intentCaptor.capture(), eq(true))
assertThat(intentCaptor.value.action)
.isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER)
assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE)
}
@Test
fun `executeAction - navigate to manage users`() =
runBlocking(IMMEDIATE) {
underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
val intentCaptor = kotlinArgumentCaptor<Intent>()
verify(activityStarter).startActivity(intentCaptor.capture(), eq(true))
assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS)
}
@Test
fun `executeAction - guest mode`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true)
whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
val dialogRequests = mutableListOf<ShowDialogRequestModel?>()
val showDialogsJob =
underTest.dialogShowRequests
.onEach {
dialogRequests.add(it)
if (it != null) {
underTest.onDialogShown()
}
}
.launchIn(this)
val dismissDialogsJob =
underTest.dialogDismissRequests
.onEach {
if (it != null) {
underTest.onDialogDismissed()
}
}
.launchIn(this)
underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
assertThat(dialogRequests)
.contains(
ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true),
)
verify(activityManager).switchUser(guestUserInfo.id)
showDialogsJob.cancel()
dismissDialogsJob.cancel()
}
@Test
fun `selectUser - already selected guest re-selected - exit guest dialog`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = true)
val guestUserInfo = userInfos[1]
assertThat(guestUserInfo.isGuest).isTrue()
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(guestUserInfo)
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
var dialogRequest: ShowDialogRequestModel? = null
val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
underTest.selectUser(
newlySelectedUserId = guestUserInfo.id,
dialogShower = dialogShower,
)
assertThat(dialogRequest)
.isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
verify(dialogShower, never()).dismiss()
job.cancel()
}
@Test
fun `selectUser - currently guest non-guest selected - exit guest dialog`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = true)
val guestUserInfo = userInfos[1]
assertThat(guestUserInfo.isGuest).isTrue()
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(guestUserInfo)
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
var dialogRequest: ShowDialogRequestModel? = null
val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
underTest.selectUser(newlySelectedUserId = userInfos[0].id, dialogShower = dialogShower)
assertThat(dialogRequest)
.isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
verify(dialogShower, never()).dismiss()
job.cancel()
}
@Test
fun `selectUser - not currently guest - switches users`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
var dialogRequest: ShowDialogRequestModel? = null
val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
underTest.selectUser(newlySelectedUserId = userInfos[1].id, dialogShower = dialogShower)
assertThat(dialogRequest).isNull()
verify(activityManager).switchUser(userInfos[1].id)
verify(dialogShower).dismiss()
job.cancel()
}
@Test
fun `Telephony call state changes - refreshes users`() =
runBlocking(IMMEDIATE) {
val refreshUsersCallCount = userRepository.refreshUsersCallCount
telephonyRepository.setCallState(1)
assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
}
@Test
fun `User switched broadcast`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
val callback1: UserInteractor.UserCallback = mock()
val callback2: UserInteractor.UserCallback = mock()
underTest.addCallback(callback1)
underTest.addCallback(callback2)
val refreshUsersCallCount = userRepository.refreshUsersCallCount
userRepository.setSelectedUserInfo(userInfos[1])
fakeBroadcastDispatcher.registeredReceivers.forEach {
it.onReceive(
context,
Intent(Intent.ACTION_USER_SWITCHED)
.putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id),
)
}
verify(callback1).onUserStateChanged()
verify(callback2).onUserStateChanged()
assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id)
assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
}
@Test
fun `User info changed broadcast`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
val refreshUsersCallCount = userRepository.refreshUsersCallCount
fakeBroadcastDispatcher.registeredReceivers.forEach {
it.onReceive(
context,
Intent(Intent.ACTION_USER_INFO_CHANGED),
)
}
assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
}
@Test
fun `System user unlocked broadcast - refresh users`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
val refreshUsersCallCount = userRepository.refreshUsersCallCount
fakeBroadcastDispatcher.registeredReceivers.forEach {
it.onReceive(
context,
Intent(Intent.ACTION_USER_UNLOCKED)
.putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM),
)
}
assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
}
@Test
fun `Non-system user unlocked broadcast - do not refresh users`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
val refreshUsersCallCount = userRepository.refreshUsersCallCount
fakeBroadcastDispatcher.registeredReceivers.forEach {
it.onReceive(
context,
Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337),
)
}
assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount)
}
@Test
fun userRecords() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 3, includeGuest = false)
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
keyguardRepository.setKeyguardShowing(false)
testCoroutineScope.advanceUntilIdle()
assertRecords(
records = underTest.userRecords.value,
userIds = listOf(0, 1, 2),
selectedUserIndex = 0,
includeGuest = false,
expectedActions =
listOf(
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.ADD_USER,
UserActionModel.ADD_SUPERVISED_USER,
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
),
)
}
@Test
fun selectedUserRecord() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 3, includeGuest = true)
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
keyguardRepository.setKeyguardShowing(false)
assertRecordForUser(
record = underTest.selectedUserRecord.value,
id = 0,
hasPicture = true,
isCurrent = true,
isSwitchToEnabled = true,
)
}
private fun assertUsers(
models: List<UserModel>?,
count: Int,
selectedIndex: Int = 0,
includeGuest: Boolean = false,
) {
checkNotNull(models)
assertThat(models.size).isEqualTo(count)
models.forEachIndexed { index, model ->
assertUser(
model = model,
id = index,
isSelected = index == selectedIndex,
isGuest = includeGuest && index == count - 1
)
}
}
private fun assertUser(
model: UserModel?,
id: Int,
isSelected: Boolean = false,
isGuest: Boolean = false,
) {
checkNotNull(model)
assertThat(model.id).isEqualTo(id)
assertThat(model.name).isEqualTo(Text.Loaded(if (isGuest) "guest" else "user_$id"))
assertThat(model.isSelected).isEqualTo(isSelected)
assertThat(model.isSelectable).isTrue()
assertThat(model.isGuest).isEqualTo(isGuest)
}
private fun assertRecords(
records: List<UserRecord>,
userIds: List<Int>,
selectedUserIndex: Int = 0,
includeGuest: Boolean = false,
expectedActions: List<UserActionModel> = emptyList(),
) {
assertThat(records.size >= userIds.size).isTrue()
userIds.indices.forEach { userIndex ->
val record = records[userIndex]
assertThat(record.info).isNotNull()
val isGuest = includeGuest && userIndex == userIds.size - 1
assertRecordForUser(
record = record,
id = userIds[userIndex],
hasPicture = !isGuest,
isCurrent = userIndex == selectedUserIndex,
isGuest = isGuest,
isSwitchToEnabled = true,
)
}
assertThat(records.size - userIds.size).isEqualTo(expectedActions.size)
(userIds.size until userIds.size + expectedActions.size).forEach { actionIndex ->
val record = records[actionIndex]
assertThat(record.info).isNull()
assertRecordForAction(
record = record,
type = expectedActions[actionIndex - userIds.size],
)
}
}
private fun assertRecordForUser(
record: UserRecord?,
id: Int? = null,
hasPicture: Boolean = false,
isCurrent: Boolean = false,
isGuest: Boolean = false,
isSwitchToEnabled: Boolean = false,
) {
checkNotNull(record)
assertThat(record.info?.id).isEqualTo(id)
assertThat(record.picture != null).isEqualTo(hasPicture)
assertThat(record.isCurrent).isEqualTo(isCurrent)
assertThat(record.isGuest).isEqualTo(isGuest)
assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled)
}
private fun assertRecordForAction(
record: UserRecord,
type: UserActionModel,
) {
assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE)
assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER)
assertThat(record.isAddSupervisedUser)
.isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER)
}
private fun createUserInfos(
count: Int,
includeGuest: Boolean,
): List<UserInfo> {
return (0 until count).map { index ->
val isGuest = includeGuest && index == count - 1
createUserInfo(
id = index,
name =
if (isGuest) {
"guest"
} else {
"user_$index"
},
isPrimary = !isGuest && index == 0,
isGuest = isGuest,
)
}
}
private fun createUserInfo(
id: Int,
name: String,
isPrimary: Boolean = false,
isGuest: Boolean = false,
): UserInfo {
return UserInfo(
id,
name,
/* iconPath= */ "",
/* flags= */ if (isPrimary) {
UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN
} else {
0
},
if (isGuest) {
UserManager.USER_TYPE_FULL_GUEST
} else {
UserManager.USER_TYPE_FULL_SYSTEM
},
)
}
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
private val GUEST_ICON: Drawable = mock()
private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation"
}
}

View File

@@ -19,51 +19,90 @@ package com.android.systemui.user.domain.interactor
import android.app.ActivityManager
import android.app.admin.DevicePolicyManager
import android.content.Intent
import android.content.pm.UserInfo
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
import androidx.test.filters.SmallTest
import com.android.internal.R.drawable.ic_account_circle
import com.android.internal.logging.UiEventLogger
import com.android.systemui.GuestResetOrExitSessionReceiver
import com.android.systemui.GuestResumeSessionReceiver
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.common.shared.model.Text
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.UserSwitcherController
import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.domain.model.ShowDialogRequestModel
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.kotlinArgumentCaptor
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.advanceUntilIdle
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
abstract class UserInteractorTest : SysuiTestCase() {
@SmallTest
@RunWith(JUnit4::class)
class UserInteractorTest : SysuiTestCase() {
@Mock protected lateinit var controller: UserSwitcherController
@Mock protected lateinit var activityStarter: ActivityStarter
@Mock protected lateinit var manager: UserManager
@Mock protected lateinit var activityManager: ActivityManager
@Mock protected lateinit var deviceProvisionedController: DeviceProvisionedController
@Mock protected lateinit var devicePolicyManager: DevicePolicyManager
@Mock protected lateinit var uiEventLogger: UiEventLogger
@Mock protected lateinit var dialogShower: UserSwitchDialogController.DialogShower
@Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var manager: UserManager
@Mock private lateinit var activityManager: ActivityManager
@Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
@Mock private lateinit var devicePolicyManager: DevicePolicyManager
@Mock private lateinit var uiEventLogger: UiEventLogger
@Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower
@Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
@Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
protected lateinit var underTest: UserInteractor
private lateinit var underTest: UserInteractor
protected lateinit var testCoroutineScope: TestCoroutineScope
protected lateinit var userRepository: FakeUserRepository
protected lateinit var keyguardRepository: FakeKeyguardRepository
protected lateinit var telephonyRepository: FakeTelephonyRepository
private lateinit var testCoroutineScope: TestCoroutineScope
private lateinit var userRepository: FakeUserRepository
private lateinit var keyguardRepository: FakeKeyguardRepository
private lateinit var telephonyRepository: FakeTelephonyRepository
abstract fun isRefactored(): Boolean
open fun setUp() {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(manager.getUserIcon(anyInt())).thenReturn(ICON)
whenever(manager.canAddMoreUsers(any())).thenReturn(true)
overrideResource(R.drawable.ic_account_circle, GUEST_ICON)
overrideResource(R.dimen.max_avatar_size, 10)
overrideResource(
com.android.internal.R.string.config_supervisedUserCreationPackage,
SUPERVISED_USER_CREATION_APP_PACKAGE,
)
userRepository = FakeUserRepository()
keyguardRepository = FakeKeyguardRepository()
@@ -79,16 +118,11 @@ abstract class UserInteractorTest : SysuiTestCase() {
UserInteractor(
applicationContext = context,
repository = userRepository,
controller = controller,
activityStarter = activityStarter,
keyguardInteractor =
KeyguardInteractor(
repository = keyguardRepository,
),
featureFlags =
FakeFeatureFlags().apply {
set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored())
},
manager = manager,
applicationScope = testCoroutineScope,
telephonyInteractor =
@@ -117,7 +151,665 @@ abstract class UserInteractorTest : SysuiTestCase() {
)
}
@Test
fun `onRecordSelected - user`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 3, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
underTest.onRecordSelected(UserRecord(info = userInfos[1]), dialogShower)
verify(dialogShower).dismiss()
verify(activityManager).switchUser(userInfos[1].id)
Unit
}
@Test
fun `onRecordSelected - switch to guest user`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 3, includeGuest = true)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
underTest.onRecordSelected(UserRecord(info = userInfos.last()))
verify(activityManager).switchUser(userInfos.last().id)
Unit
}
@Test
fun `onRecordSelected - enter guest mode`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 3, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true)
whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower)
verify(dialogShower).dismiss()
verify(manager).createGuest(any())
Unit
}
@Test
fun `onRecordSelected - action`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 3, includeGuest = true)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
underTest.onRecordSelected(UserRecord(isAddSupervisedUser = true), dialogShower)
verify(dialogShower, never()).dismiss()
verify(activityStarter).startActivity(any(), anyBoolean())
}
@Test
fun `users - switcher enabled`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 3, includeGuest = true)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
var value: List<UserModel>? = null
val job = underTest.users.onEach { value = it }.launchIn(this)
assertUsers(models = value, count = 3, includeGuest = true)
job.cancel()
}
@Test
fun `users - switches to second user`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
var value: List<UserModel>? = null
val job = underTest.users.onEach { value = it }.launchIn(this)
userRepository.setSelectedUserInfo(userInfos[1])
assertUsers(models = value, count = 2, selectedIndex = 1)
job.cancel()
}
@Test
fun `users - switcher not enabled`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
var value: List<UserModel>? = null
val job = underTest.users.onEach { value = it }.launchIn(this)
assertUsers(models = value, count = 1)
job.cancel()
}
@Test
fun selectedUser() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
var value: UserModel? = null
val job = underTest.selectedUser.onEach { value = it }.launchIn(this)
assertUser(value, id = 0, isSelected = true)
userRepository.setSelectedUserInfo(userInfos[1])
assertUser(value, id = 1, isSelected = true)
job.cancel()
}
@Test
fun `actions - device unlocked`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
keyguardRepository.setKeyguardShowing(false)
var value: List<UserActionModel>? = null
val job = underTest.actions.onEach { value = it }.launchIn(this)
assertThat(value)
.isEqualTo(
listOf(
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.ADD_USER,
UserActionModel.ADD_SUPERVISED_USER,
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
)
job.cancel()
}
@Test
fun `actions - device unlocked user not primary - empty list`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[1])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
keyguardRepository.setKeyguardShowing(false)
var value: List<UserActionModel>? = null
val job = underTest.actions.onEach { value = it }.launchIn(this)
assertThat(value).isEqualTo(emptyList<UserActionModel>())
job.cancel()
}
@Test
fun `actions - device unlocked user is guest - empty list`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = true)
assertThat(userInfos[1].isGuest).isTrue()
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[1])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
keyguardRepository.setKeyguardShowing(false)
var value: List<UserActionModel>? = null
val job = underTest.actions.onEach { value = it }.launchIn(this)
assertThat(value).isEqualTo(emptyList<UserActionModel>())
job.cancel()
}
@Test
fun `actions - device locked add from lockscreen set - full list`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(
UserSwitcherSettingsModel(
isUserSwitcherEnabled = true,
isAddUsersFromLockscreen = true,
)
)
keyguardRepository.setKeyguardShowing(false)
var value: List<UserActionModel>? = null
val job = underTest.actions.onEach { value = it }.launchIn(this)
assertThat(value)
.isEqualTo(
listOf(
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.ADD_USER,
UserActionModel.ADD_SUPERVISED_USER,
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
)
job.cancel()
}
@Test
fun `actions - device locked - only guest action and manage user is shown`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
keyguardRepository.setKeyguardShowing(true)
var value: List<UserActionModel>? = null
val job = underTest.actions.onEach { value = it }.launchIn(this)
assertThat(value)
.isEqualTo(
listOf(
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT
)
)
job.cancel()
}
@Test
fun `executeAction - add user - dialog shown`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
keyguardRepository.setKeyguardShowing(false)
var dialogRequest: ShowDialogRequestModel? = null
val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
val dialogShower: UserSwitchDialogController.DialogShower = mock()
underTest.executeAction(UserActionModel.ADD_USER, dialogShower)
assertThat(dialogRequest)
.isEqualTo(
ShowDialogRequestModel.ShowAddUserDialog(
userHandle = userInfos[0].userHandle,
isKeyguardShowing = false,
showEphemeralMessage = false,
dialogShower = dialogShower,
)
)
underTest.onDialogShown()
assertThat(dialogRequest).isNull()
job.cancel()
}
@Test
fun `executeAction - add supervised user - starts activity`() =
runBlocking(IMMEDIATE) {
underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
val intentCaptor = kotlinArgumentCaptor<Intent>()
verify(activityStarter).startActivity(intentCaptor.capture(), eq(true))
assertThat(intentCaptor.value.action)
.isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER)
assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE)
}
@Test
fun `executeAction - navigate to manage users`() =
runBlocking(IMMEDIATE) {
underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
val intentCaptor = kotlinArgumentCaptor<Intent>()
verify(activityStarter).startActivity(intentCaptor.capture(), eq(true))
assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS)
}
@Test
fun `executeAction - guest mode`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true)
whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
val dialogRequests = mutableListOf<ShowDialogRequestModel?>()
val showDialogsJob =
underTest.dialogShowRequests
.onEach {
dialogRequests.add(it)
if (it != null) {
underTest.onDialogShown()
}
}
.launchIn(this)
val dismissDialogsJob =
underTest.dialogDismissRequests
.onEach {
if (it != null) {
underTest.onDialogDismissed()
}
}
.launchIn(this)
underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
assertThat(dialogRequests)
.contains(
ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true),
)
verify(activityManager).switchUser(guestUserInfo.id)
showDialogsJob.cancel()
dismissDialogsJob.cancel()
}
@Test
fun `selectUser - already selected guest re-selected - exit guest dialog`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = true)
val guestUserInfo = userInfos[1]
assertThat(guestUserInfo.isGuest).isTrue()
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(guestUserInfo)
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
var dialogRequest: ShowDialogRequestModel? = null
val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
underTest.selectUser(
newlySelectedUserId = guestUserInfo.id,
dialogShower = dialogShower,
)
assertThat(dialogRequest)
.isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
verify(dialogShower, never()).dismiss()
job.cancel()
}
@Test
fun `selectUser - currently guest non-guest selected - exit guest dialog`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = true)
val guestUserInfo = userInfos[1]
assertThat(guestUserInfo.isGuest).isTrue()
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(guestUserInfo)
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
var dialogRequest: ShowDialogRequestModel? = null
val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
underTest.selectUser(newlySelectedUserId = userInfos[0].id, dialogShower = dialogShower)
assertThat(dialogRequest)
.isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
verify(dialogShower, never()).dismiss()
job.cancel()
}
@Test
fun `selectUser - not currently guest - switches users`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
var dialogRequest: ShowDialogRequestModel? = null
val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
underTest.selectUser(newlySelectedUserId = userInfos[1].id, dialogShower = dialogShower)
assertThat(dialogRequest).isNull()
verify(activityManager).switchUser(userInfos[1].id)
verify(dialogShower).dismiss()
job.cancel()
}
@Test
fun `Telephony call state changes - refreshes users`() =
runBlocking(IMMEDIATE) {
val refreshUsersCallCount = userRepository.refreshUsersCallCount
telephonyRepository.setCallState(1)
assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
}
@Test
fun `User switched broadcast`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
val callback1: UserInteractor.UserCallback = mock()
val callback2: UserInteractor.UserCallback = mock()
underTest.addCallback(callback1)
underTest.addCallback(callback2)
val refreshUsersCallCount = userRepository.refreshUsersCallCount
userRepository.setSelectedUserInfo(userInfos[1])
fakeBroadcastDispatcher.registeredReceivers.forEach {
it.onReceive(
context,
Intent(Intent.ACTION_USER_SWITCHED)
.putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id),
)
}
verify(callback1).onUserStateChanged()
verify(callback2).onUserStateChanged()
assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id)
assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
}
@Test
fun `User info changed broadcast`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
val refreshUsersCallCount = userRepository.refreshUsersCallCount
fakeBroadcastDispatcher.registeredReceivers.forEach {
it.onReceive(
context,
Intent(Intent.ACTION_USER_INFO_CHANGED),
)
}
assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
}
@Test
fun `System user unlocked broadcast - refresh users`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
val refreshUsersCallCount = userRepository.refreshUsersCallCount
fakeBroadcastDispatcher.registeredReceivers.forEach {
it.onReceive(
context,
Intent(Intent.ACTION_USER_UNLOCKED)
.putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM),
)
}
assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
}
@Test
fun `Non-system user unlocked broadcast - do not refresh users`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
val refreshUsersCallCount = userRepository.refreshUsersCallCount
fakeBroadcastDispatcher.registeredReceivers.forEach {
it.onReceive(
context,
Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337),
)
}
assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount)
}
@Test
fun userRecords() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 3, includeGuest = false)
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
keyguardRepository.setKeyguardShowing(false)
testCoroutineScope.advanceUntilIdle()
assertRecords(
records = underTest.userRecords.value,
userIds = listOf(0, 1, 2),
selectedUserIndex = 0,
includeGuest = false,
expectedActions =
listOf(
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.ADD_USER,
UserActionModel.ADD_SUPERVISED_USER,
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
),
)
}
@Test
fun selectedUserRecord() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 3, includeGuest = true)
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
keyguardRepository.setKeyguardShowing(false)
assertRecordForUser(
record = underTest.selectedUserRecord.value,
id = 0,
hasPicture = true,
isCurrent = true,
isSwitchToEnabled = true,
)
}
private fun assertUsers(
models: List<UserModel>?,
count: Int,
selectedIndex: Int = 0,
includeGuest: Boolean = false,
) {
checkNotNull(models)
assertThat(models.size).isEqualTo(count)
models.forEachIndexed { index, model ->
assertUser(
model = model,
id = index,
isSelected = index == selectedIndex,
isGuest = includeGuest && index == count - 1
)
}
}
private fun assertUser(
model: UserModel?,
id: Int,
isSelected: Boolean = false,
isGuest: Boolean = false,
) {
checkNotNull(model)
assertThat(model.id).isEqualTo(id)
assertThat(model.name).isEqualTo(Text.Loaded(if (isGuest) "guest" else "user_$id"))
assertThat(model.isSelected).isEqualTo(isSelected)
assertThat(model.isSelectable).isTrue()
assertThat(model.isGuest).isEqualTo(isGuest)
}
private fun assertRecords(
records: List<UserRecord>,
userIds: List<Int>,
selectedUserIndex: Int = 0,
includeGuest: Boolean = false,
expectedActions: List<UserActionModel> = emptyList(),
) {
assertThat(records.size >= userIds.size).isTrue()
userIds.indices.forEach { userIndex ->
val record = records[userIndex]
assertThat(record.info).isNotNull()
val isGuest = includeGuest && userIndex == userIds.size - 1
assertRecordForUser(
record = record,
id = userIds[userIndex],
hasPicture = !isGuest,
isCurrent = userIndex == selectedUserIndex,
isGuest = isGuest,
isSwitchToEnabled = true,
)
}
assertThat(records.size - userIds.size).isEqualTo(expectedActions.size)
(userIds.size until userIds.size + expectedActions.size).forEach { actionIndex ->
val record = records[actionIndex]
assertThat(record.info).isNull()
assertRecordForAction(
record = record,
type = expectedActions[actionIndex - userIds.size],
)
}
}
private fun assertRecordForUser(
record: UserRecord?,
id: Int? = null,
hasPicture: Boolean = false,
isCurrent: Boolean = false,
isGuest: Boolean = false,
isSwitchToEnabled: Boolean = false,
) {
checkNotNull(record)
assertThat(record.info?.id).isEqualTo(id)
assertThat(record.picture != null).isEqualTo(hasPicture)
assertThat(record.isCurrent).isEqualTo(isCurrent)
assertThat(record.isGuest).isEqualTo(isGuest)
assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled)
}
private fun assertRecordForAction(
record: UserRecord,
type: UserActionModel,
) {
assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE)
assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER)
assertThat(record.isAddSupervisedUser)
.isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER)
}
private fun createUserInfos(
count: Int,
includeGuest: Boolean,
): List<UserInfo> {
return (0 until count).map { index ->
val isGuest = includeGuest && index == count - 1
createUserInfo(
id = index,
name =
if (isGuest) {
"guest"
} else {
"user_$index"
},
isPrimary = !isGuest && index == 0,
isGuest = isGuest,
)
}
}
private fun createUserInfo(
id: Int,
name: String,
isPrimary: Boolean = false,
isGuest: Boolean = false,
): UserInfo {
return UserInfo(
id,
name,
/* iconPath= */ "",
/* flags= */ if (isPrimary) {
UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN
} else {
0
},
if (isGuest) {
UserManager.USER_TYPE_FULL_GUEST
} else {
UserManager.USER_TYPE_FULL_SYSTEM
},
)
}
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
private val GUEST_ICON: Drawable = mock()
private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation"
}
}

View File

@@ -1,174 +0,0 @@
/*
* 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 androidx.test.filters.SmallTest
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.nullable
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.verify
@SmallTest
@RunWith(JUnit4::class)
open class UserInteractorUnrefactoredTest : UserInteractorTest() {
override fun isRefactored(): Boolean {
return false
}
@Before
override fun setUp() {
super.setUp()
}
@Test
fun `actions - not actionable when locked and locked - no actions`() =
runBlocking(IMMEDIATE) {
userRepository.setActions(UserActionModel.values().toList())
userRepository.setActionableWhenLocked(false)
keyguardRepository.setKeyguardShowing(true)
var actions: List<UserActionModel>? = null
val job = underTest.actions.onEach { actions = it }.launchIn(this)
assertThat(actions).isEmpty()
job.cancel()
}
@Test
fun `actions - not actionable when locked and not locked`() =
runBlocking(IMMEDIATE) {
setActions()
userRepository.setActionableWhenLocked(false)
keyguardRepository.setKeyguardShowing(false)
var actions: List<UserActionModel>? = null
val job = underTest.actions.onEach { actions = it }.launchIn(this)
assertThat(actions)
.isEqualTo(
listOf(
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.ADD_USER,
UserActionModel.ADD_SUPERVISED_USER,
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
)
job.cancel()
}
@Test
fun `actions - actionable when locked and not locked`() =
runBlocking(IMMEDIATE) {
setActions()
userRepository.setActionableWhenLocked(true)
keyguardRepository.setKeyguardShowing(false)
var actions: List<UserActionModel>? = null
val job = underTest.actions.onEach { actions = it }.launchIn(this)
assertThat(actions)
.isEqualTo(
listOf(
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.ADD_USER,
UserActionModel.ADD_SUPERVISED_USER,
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
)
job.cancel()
}
@Test
fun `actions - actionable when locked and locked`() =
runBlocking(IMMEDIATE) {
setActions()
userRepository.setActionableWhenLocked(true)
keyguardRepository.setKeyguardShowing(true)
var actions: List<UserActionModel>? = null
val job = underTest.actions.onEach { actions = it }.launchIn(this)
assertThat(actions)
.isEqualTo(
listOf(
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.ADD_USER,
UserActionModel.ADD_SUPERVISED_USER,
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
)
job.cancel()
}
@Test
fun selectUser() {
val userId = 3
underTest.selectUser(userId)
verify(controller).onUserSelected(eq(userId), nullable())
}
@Test
fun `executeAction - guest`() {
underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
verify(controller).createAndSwitchToGuestUser(nullable())
}
@Test
fun `executeAction - add user`() {
underTest.executeAction(UserActionModel.ADD_USER)
verify(controller).showAddUserDialog(nullable())
}
@Test
fun `executeAction - add supervised user`() {
underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
verify(controller).startSupervisedUserActivity()
}
@Test
fun `executeAction - manage users`() {
underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
verify(activityStarter).startActivity(any(), anyBoolean())
}
private fun setActions() {
userRepository.setActions(UserActionModel.values().toList())
}
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
}
}

View File

@@ -19,7 +19,7 @@ package com.android.systemui.user.ui.viewmodel
import android.app.ActivityManager
import android.app.admin.DevicePolicyManager
import android.graphics.drawable.Drawable
import android.content.pm.UserInfo
import android.os.UserManager
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
@@ -27,32 +27,37 @@ import com.android.systemui.GuestResetOrExitSessionReceiver
import com.android.systemui.GuestResumeSessionReceiver
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Text
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.power.data.repository.FakePowerRepository
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.UserSwitcherController
import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.domain.interactor.GuestUserInteractor
import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
import com.android.systemui.user.domain.interactor.UserInteractor
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.yield
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestResult
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -60,11 +65,11 @@ import org.junit.runners.JUnit4
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class UserSwitcherViewModelTest : SysuiTestCase() {
@Mock private lateinit var controller: UserSwitcherController
@Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var activityManager: ActivityManager
@Mock private lateinit var manager: UserManager
@@ -80,28 +85,47 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
private lateinit var keyguardRepository: FakeKeyguardRepository
private lateinit var powerRepository: FakePowerRepository
private lateinit var testDispatcher: TestDispatcher
private lateinit var testScope: TestScope
private lateinit var injectedScope: CoroutineScope
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(manager.canAddMoreUsers(any())).thenReturn(true)
whenever(manager.getUserSwitchability(any()))
.thenReturn(UserManager.SWITCHABILITY_STATUS_OK)
overrideResource(
com.android.internal.R.string.config_supervisedUserCreationPackage,
SUPERVISED_USER_CREATION_PACKAGE,
)
testDispatcher = UnconfinedTestDispatcher()
testScope = TestScope(testDispatcher)
injectedScope = CoroutineScope(testScope.coroutineContext + SupervisorJob())
userRepository = FakeUserRepository()
runBlocking {
userRepository.setSettings(
UserSwitcherSettingsModel(
isUserSwitcherEnabled = true,
)
)
}
keyguardRepository = FakeKeyguardRepository()
powerRepository = FakePowerRepository()
val featureFlags = FakeFeatureFlags()
featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, true)
val scope = TestCoroutineScope()
val refreshUsersScheduler =
RefreshUsersScheduler(
applicationScope = scope,
mainDispatcher = IMMEDIATE,
applicationScope = injectedScope,
mainDispatcher = testDispatcher,
repository = userRepository,
)
val guestUserInteractor =
GuestUserInteractor(
applicationContext = context,
applicationScope = scope,
mainDispatcher = IMMEDIATE,
backgroundDispatcher = IMMEDIATE,
applicationScope = injectedScope,
mainDispatcher = testDispatcher,
backgroundDispatcher = testDispatcher,
manager = manager,
repository = userRepository,
deviceProvisionedController = deviceProvisionedController,
@@ -118,21 +142,19 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
UserInteractor(
applicationContext = context,
repository = userRepository,
controller = controller,
activityStarter = activityStarter,
keyguardInteractor =
KeyguardInteractor(
repository = keyguardRepository,
),
featureFlags = featureFlags,
manager = manager,
applicationScope = scope,
applicationScope = injectedScope,
telephonyInteractor =
TelephonyInteractor(
repository = FakeTelephonyRepository(),
),
broadcastDispatcher = fakeBroadcastDispatcher,
backgroundDispatcher = IMMEDIATE,
backgroundDispatcher = testDispatcher,
activityManager = activityManager,
refreshUsersScheduler = refreshUsersScheduler,
guestUserInteractor = guestUserInteractor,
@@ -141,222 +163,216 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
PowerInteractor(
repository = powerRepository,
),
featureFlags = featureFlags,
guestUserInteractor = guestUserInteractor,
)
.create(UserSwitcherViewModel::class.java)
}
@Test
fun users() =
runBlocking(IMMEDIATE) {
userRepository.setUsers(
fun users() = selfCancelingTest {
val userInfos =
listOf(
UserInfo(
/* id= */ 0,
/* name= */ "zero",
/* iconPath= */ "",
/* flags= */ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN,
UserManager.USER_TYPE_FULL_SYSTEM,
),
UserInfo(
/* id= */ 1,
/* name= */ "one",
/* iconPath= */ "",
/* flags= */ 0,
UserManager.USER_TYPE_FULL_SYSTEM,
),
UserInfo(
/* id= */ 2,
/* name= */ "two",
/* iconPath= */ "",
/* flags= */ 0,
UserManager.USER_TYPE_FULL_SYSTEM,
),
)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
val userViewModels = mutableListOf<List<UserViewModel>>()
val job = launch(testDispatcher) { underTest.users.toList(userViewModels) }
assertThat(userViewModels.last()).hasSize(3)
assertUserViewModel(
viewModel = userViewModels.last()[0],
viewKey = 0,
name = "zero",
isSelectionMarkerVisible = true,
)
assertUserViewModel(
viewModel = userViewModels.last()[1],
viewKey = 1,
name = "one",
isSelectionMarkerVisible = false,
)
assertUserViewModel(
viewModel = userViewModels.last()[2],
viewKey = 2,
name = "two",
isSelectionMarkerVisible = false,
)
job.cancel()
}
@Test
fun `maximumUserColumns - few users`() = selfCancelingTest {
setUsers(count = 2)
val values = mutableListOf<Int>()
val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) }
assertThat(values.last()).isEqualTo(4)
job.cancel()
}
@Test
fun `maximumUserColumns - many users`() = selfCancelingTest {
setUsers(count = 5)
val values = mutableListOf<Int>()
val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) }
assertThat(values.last()).isEqualTo(3)
job.cancel()
}
@Test
fun `isOpenMenuButtonVisible - has actions - true`() = selfCancelingTest {
setUsers(2)
val isVisible = mutableListOf<Boolean>()
val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) }
assertThat(isVisible.last()).isTrue()
job.cancel()
}
@Test
fun `isOpenMenuButtonVisible - no actions - false`() = selfCancelingTest {
val userInfos = setUsers(2)
userRepository.setSelectedUserInfo(userInfos[1])
keyguardRepository.setKeyguardShowing(true)
whenever(manager.canAddMoreUsers(any())).thenReturn(false)
val isVisible = mutableListOf<Boolean>()
val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) }
assertThat(isVisible.last()).isFalse()
job.cancel()
}
@Test
fun menu() = selfCancelingTest {
val isMenuVisible = mutableListOf<Boolean>()
val job = launch(testDispatcher) { underTest.isMenuVisible.toList(isMenuVisible) }
assertThat(isMenuVisible.last()).isFalse()
underTest.onOpenMenuButtonClicked()
assertThat(isMenuVisible.last()).isTrue()
underTest.onMenuClosed()
assertThat(isMenuVisible.last()).isFalse()
job.cancel()
}
@Test
fun `menu actions`() = selfCancelingTest {
setUsers(2)
val actions = mutableListOf<List<UserActionViewModel>>()
val job = launch(testDispatcher) { underTest.menu.toList(actions) }
assertThat(actions.last().map { it.viewKey })
.isEqualTo(
listOf(
UserModel(
id = 0,
name = Text.Loaded("zero"),
image = USER_IMAGE,
isSelected = true,
isSelectable = true,
isGuest = false,
),
UserModel(
id = 1,
name = Text.Loaded("one"),
image = USER_IMAGE,
isSelected = false,
isSelectable = true,
isGuest = false,
),
UserModel(
id = 2,
name = Text.Loaded("two"),
image = USER_IMAGE,
isSelected = false,
isSelectable = false,
isGuest = false,
),
UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(),
UserActionModel.ADD_USER.ordinal.toLong(),
UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(),
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(),
)
)
var userViewModels: List<UserViewModel>? = null
val job = underTest.users.onEach { userViewModels = it }.launchIn(this)
assertThat(userViewModels).hasSize(3)
assertUserViewModel(
viewModel = userViewModels?.get(0),
viewKey = 0,
name = "zero",
isSelectionMarkerVisible = true,
alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA,
isClickable = true,
)
assertUserViewModel(
viewModel = userViewModels?.get(1),
viewKey = 1,
name = "one",
isSelectionMarkerVisible = false,
alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA,
isClickable = true,
)
assertUserViewModel(
viewModel = userViewModels?.get(2),
viewKey = 2,
name = "two",
isSelectionMarkerVisible = false,
alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_NOT_SELECTABLE_ALPHA,
isClickable = false,
)
job.cancel()
}
job.cancel()
}
@Test
fun `maximumUserColumns - few users`() =
runBlocking(IMMEDIATE) {
setUsers(count = 2)
var value: Int? = null
val job = underTest.maximumUserColumns.onEach { value = it }.launchIn(this)
fun `isFinishRequested - finishes when user is switched`() = selfCancelingTest {
val userInfos = setUsers(count = 2)
val isFinishRequested = mutableListOf<Boolean>()
val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
assertThat(isFinishRequested.last()).isFalse()
assertThat(value).isEqualTo(4)
job.cancel()
}
userRepository.setSelectedUserInfo(userInfos[1])
assertThat(isFinishRequested.last()).isTrue()
job.cancel()
}
@Test
fun `maximumUserColumns - many users`() =
runBlocking(IMMEDIATE) {
setUsers(count = 5)
var value: Int? = null
val job = underTest.maximumUserColumns.onEach { value = it }.launchIn(this)
fun `isFinishRequested - finishes when the screen turns off`() = selfCancelingTest {
setUsers(count = 2)
powerRepository.setInteractive(true)
val isFinishRequested = mutableListOf<Boolean>()
val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
assertThat(isFinishRequested.last()).isFalse()
assertThat(value).isEqualTo(3)
job.cancel()
}
powerRepository.setInteractive(false)
assertThat(isFinishRequested.last()).isTrue()
job.cancel()
}
@Test
fun `isOpenMenuButtonVisible - has actions - true`() =
runBlocking(IMMEDIATE) {
userRepository.setActions(UserActionModel.values().toList())
fun `isFinishRequested - finishes when cancel button is clicked`() = selfCancelingTest {
setUsers(count = 2)
powerRepository.setInteractive(true)
val isFinishRequested = mutableListOf<Boolean>()
val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
assertThat(isFinishRequested.last()).isFalse()
var isVisible: Boolean? = null
val job = underTest.isOpenMenuButtonVisible.onEach { isVisible = it }.launchIn(this)
underTest.onCancelButtonClicked()
assertThat(isVisible).isTrue()
job.cancel()
}
assertThat(isFinishRequested.last()).isTrue()
@Test
fun `isOpenMenuButtonVisible - no actions - false`() =
runBlocking(IMMEDIATE) {
userRepository.setActions(emptyList())
underTest.onFinished()
var isVisible: Boolean? = null
val job = underTest.isOpenMenuButtonVisible.onEach { isVisible = it }.launchIn(this)
assertThat(isFinishRequested.last()).isFalse()
assertThat(isVisible).isFalse()
job.cancel()
}
job.cancel()
}
@Test
fun menu() =
runBlocking(IMMEDIATE) {
userRepository.setActions(UserActionModel.values().toList())
var isMenuVisible: Boolean? = null
val job = underTest.isMenuVisible.onEach { isMenuVisible = it }.launchIn(this)
assertThat(isMenuVisible).isFalse()
underTest.onOpenMenuButtonClicked()
assertThat(isMenuVisible).isTrue()
underTest.onMenuClosed()
assertThat(isMenuVisible).isFalse()
job.cancel()
}
@Test
fun `menu actions`() =
runBlocking(IMMEDIATE) {
userRepository.setActions(UserActionModel.values().toList())
var actions: List<UserActionViewModel>? = null
val job = underTest.menu.onEach { actions = it }.launchIn(this)
assertThat(actions?.map { it.viewKey })
.isEqualTo(
listOf(
UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(),
UserActionModel.ADD_USER.ordinal.toLong(),
UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(),
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(),
)
)
job.cancel()
}
@Test
fun `isFinishRequested - finishes when user is switched`() =
runBlocking(IMMEDIATE) {
setUsers(count = 2)
var isFinishRequested: Boolean? = null
val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this)
assertThat(isFinishRequested).isFalse()
userRepository.setSelectedUser(1)
yield()
assertThat(isFinishRequested).isTrue()
job.cancel()
}
@Test
fun `isFinishRequested - finishes when the screen turns off`() =
runBlocking(IMMEDIATE) {
setUsers(count = 2)
powerRepository.setInteractive(true)
var isFinishRequested: Boolean? = null
val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this)
assertThat(isFinishRequested).isFalse()
powerRepository.setInteractive(false)
yield()
assertThat(isFinishRequested).isTrue()
job.cancel()
}
@Test
fun `isFinishRequested - finishes when cancel button is clicked`() =
runBlocking(IMMEDIATE) {
setUsers(count = 2)
powerRepository.setInteractive(true)
var isFinishRequested: Boolean? = null
val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this)
assertThat(isFinishRequested).isFalse()
underTest.onCancelButtonClicked()
yield()
assertThat(isFinishRequested).isTrue()
underTest.onFinished()
yield()
assertThat(isFinishRequested).isFalse()
job.cancel()
}
private suspend fun setUsers(count: Int) {
userRepository.setUsers(
private suspend fun setUsers(count: Int): List<UserInfo> {
val userInfos =
(0 until count).map { index ->
UserModel(
id = index,
name = Text.Loaded("$index"),
image = USER_IMAGE,
isSelected = index == 0,
isSelectable = true,
isGuest = false,
UserInfo(
/* id= */ index,
/* name= */ "$index",
/* iconPath= */ "",
/* flags= */ if (index == 0) {
// This is the primary user.
UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN
} else {
// This isn't the primary user.
0
},
UserManager.USER_TYPE_FULL_SYSTEM,
)
}
)
userRepository.setUserInfos(userInfos)
if (userInfos.isNotEmpty()) {
userRepository.setSelectedUserInfo(userInfos[0])
}
return userInfos
}
private fun assertUserViewModel(
@@ -364,19 +380,25 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
viewKey: Int,
name: String,
isSelectionMarkerVisible: Boolean,
alpha: Float,
isClickable: Boolean,
) {
checkNotNull(viewModel)
assertThat(viewModel.viewKey).isEqualTo(viewKey)
assertThat(viewModel.name).isEqualTo(Text.Loaded(name))
assertThat(viewModel.isSelectionMarkerVisible).isEqualTo(isSelectionMarkerVisible)
assertThat(viewModel.alpha).isEqualTo(alpha)
assertThat(viewModel.onClicked != null).isEqualTo(isClickable)
assertThat(viewModel.alpha)
.isEqualTo(LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA)
assertThat(viewModel.onClicked).isNotNull()
}
private fun selfCancelingTest(
block: suspend TestScope.() -> Unit,
): TestResult =
testScope.runTest {
block()
injectedScope.coroutineContext[Job.Key]?.cancelAndJoin()
}
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
private val USER_IMAGE = mock<Drawable>()
private const val SUPERVISED_USER_CREATION_PACKAGE = "com.some.package"
}
}

View File

@@ -20,26 +20,15 @@ package com.android.systemui.user.data.repository
import android.content.pm.UserInfo
import android.os.UserHandle
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.yield
class FakeUserRepository : UserRepository {
private val _users = MutableStateFlow<List<UserModel>>(emptyList())
override val users: Flow<List<UserModel>> = _users.asStateFlow()
override val selectedUser: Flow<UserModel> =
users.map { models -> models.first { model -> model.isSelected } }
private val _actions = MutableStateFlow<List<UserActionModel>>(emptyList())
override val actions: Flow<List<UserActionModel>> = _actions.asStateFlow()
private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel())
override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
_userSwitcherSettings.asStateFlow()
@@ -52,9 +41,6 @@ class FakeUserRepository : UserRepository {
override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
private val _isActionableWhenLocked = MutableStateFlow(false)
override val isActionableWhenLocked: Flow<Boolean> = _isActionableWhenLocked.asStateFlow()
private var _isGuestUserAutoCreated: Boolean = false
override val isGuestUserAutoCreated: Boolean
get() = _isGuestUserAutoCreated
@@ -100,35 +86,6 @@ class FakeUserRepository : UserRepository {
yield()
}
fun setUsers(models: List<UserModel>) {
_users.value = models
}
suspend fun setSelectedUser(userId: Int) {
check(_users.value.find { it.id == userId } != null) {
"Cannot select a user with ID $userId - no user with that ID found!"
}
setUsers(
_users.value.map { model ->
when {
model.isSelected && model.id != userId -> model.copy(isSelected = false)
!model.isSelected && model.id == userId -> model.copy(isSelected = true)
else -> model
}
}
)
yield()
}
fun setActions(models: List<UserActionModel>) {
_actions.value = models
}
fun setActionableWhenLocked(value: Boolean) {
_isActionableWhenLocked.value = value
}
fun setGuestUserAutoCreated(value: Boolean) {
_isGuestUserAutoCreated = value
}