UserSwitcherController impl uses UserInteractor

Based on a flag, UserSwitcherControllerImpl will use UserInteractor,
GuestUserInteractor, and KeyguardInteractor to implement its interface.

Once this is all cleaned up, we can remove the old implementation and
even update users of UserSwitcherControllerImpl to directly use the
interactors instead.

Bug: 246631653
Test: Unit tests added for newly exposed functionality. Existing tests
still pass. Manually verified the full-screen user switcher and the
dropdown in the bouncer.

Change-Id: Iff7c06f8c0e2fdd8adaab436ff895e9d892d2f20
This commit is contained in:
Alejandro Nijamkin
2022-09-30 16:10:19 -07:00
parent 2caeebc5df
commit 8f8b74fb73
5 changed files with 262 additions and 77 deletions

View File

@@ -17,12 +17,21 @@
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.data.LegacyUserDataHelper
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import dagger.Lazy
import java.io.PrintWriter
import java.lang.ref.WeakReference
@@ -30,11 +39,17 @@ import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
/** Implementation of [UserSwitcherController]. */
@SysUISingleton
class UserSwitcherControllerImpl
@Inject
constructor(
private val flags: FeatureFlags,
@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 =
@@ -42,15 +57,21 @@ constructor(
!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 fun notYetImplemented(): Nothing {
error("Not yet implemented!")
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) {
notYetImplemented()
userInteractor.userRecords.value
} else {
_oldImpl.users
}
@@ -58,15 +79,13 @@ constructor(
override val isSimpleUserSwitcher: Boolean
get() =
if (useInteractor) {
notYetImplemented()
userInteractor.isSimpleUserSwitcher
} else {
_oldImpl.isSimpleUserSwitcher
}
override fun init(view: View) {
if (useInteractor) {
notYetImplemented()
} else {
if (!useInteractor) {
_oldImpl.init(view)
}
}
@@ -74,7 +93,7 @@ constructor(
override val currentUserRecord: UserRecord?
get() =
if (useInteractor) {
notYetImplemented()
userInteractor.selectedUserRecord.value
} else {
_oldImpl.currentUserRecord
}
@@ -82,7 +101,14 @@ constructor(
override val currentUserName: String?
get() =
if (useInteractor) {
notYetImplemented()
currentUserRecord?.let {
LegacyUserUiHelper.getUserRecordName(
context = applicationContext,
record = it,
isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
isGuestUserResetting = userInteractor.isGuestUserResetting,
)
}
} else {
_oldImpl.currentUserName
}
@@ -92,7 +118,7 @@ constructor(
dialogShower: UserSwitchDialogController.DialogShower?
) {
if (useInteractor) {
notYetImplemented()
userInteractor.selectUser(userId)
} else {
_oldImpl.onUserSelected(userId, dialogShower)
}
@@ -101,7 +127,7 @@ constructor(
override val isAddUsersFromLockScreenEnabled: Flow<Boolean>
get() =
if (useInteractor) {
notYetImplemented()
notSupported()
} else {
_oldImpl.isAddUsersFromLockScreenEnabled
}
@@ -109,7 +135,7 @@ constructor(
override val isGuestUserAutoCreated: Boolean
get() =
if (useInteractor) {
notYetImplemented()
userInteractor.isGuestUserAutoCreated
} else {
_oldImpl.isGuestUserAutoCreated
}
@@ -117,7 +143,7 @@ constructor(
override val isGuestUserResetting: Boolean
get() =
if (useInteractor) {
notYetImplemented()
userInteractor.isGuestUserResetting
} else {
_oldImpl.isGuestUserResetting
}
@@ -126,7 +152,7 @@ constructor(
dialogShower: UserSwitchDialogController.DialogShower?,
) {
if (useInteractor) {
notYetImplemented()
notSupported()
} else {
_oldImpl.createAndSwitchToGuestUser(dialogShower)
}
@@ -134,7 +160,7 @@ constructor(
override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) {
if (useInteractor) {
notYetImplemented()
notSupported()
} else {
_oldImpl.showAddUserDialog(dialogShower)
}
@@ -142,23 +168,31 @@ constructor(
override fun startSupervisedUserActivity() {
if (useInteractor) {
notYetImplemented()
notSupported()
} else {
_oldImpl.startSupervisedUserActivity()
}
}
override fun onDensityOrFontScaleChanged() {
if (useInteractor) {
notYetImplemented()
} else {
if (!useInteractor) {
_oldImpl.onDensityOrFontScaleChanged()
}
}
override fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) {
if (useInteractor) {
notYetImplemented()
userInteractor.addCallback(
object : UserInteractor.UserCallback {
override fun isEvictable(): Boolean {
return adapter.get() == null
}
override fun onUserStateChanged() {
adapter.get()?.notifyDataSetChanged()
}
}
)
} else {
_oldImpl.addAdapter(adapter)
}
@@ -169,7 +203,11 @@ constructor(
dialogShower: UserSwitchDialogController.DialogShower?,
) {
if (useInteractor) {
notYetImplemented()
if (LegacyUserDataHelper.isUser(record)) {
userInteractor.selectUser(record.resolveId())
} else {
userInteractor.executeAction(LegacyUserDataHelper.toUserActionModel(record))
}
} else {
_oldImpl.onUserListItemClicked(record, dialogShower)
}
@@ -177,7 +215,10 @@ constructor(
override fun removeGuestUser(guestUserId: Int, targetUserId: Int) {
if (useInteractor) {
notYetImplemented()
userInteractor.removeGuestUser(
guestUserId = guestUserId,
targetUserId = targetUserId,
)
} else {
_oldImpl.removeGuestUser(guestUserId, targetUserId)
}
@@ -189,7 +230,7 @@ constructor(
forceRemoveGuestOnExit: Boolean
) {
if (useInteractor) {
notYetImplemented()
userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
} else {
_oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
}
@@ -197,7 +238,7 @@ constructor(
override fun schedulePostBootGuestCreation() {
if (useInteractor) {
notYetImplemented()
guestUserInteractor.onDeviceBootCompleted()
} else {
_oldImpl.schedulePostBootGuestCreation()
}
@@ -206,14 +247,14 @@ constructor(
override val isKeyguardShowing: Boolean
get() =
if (useInteractor) {
notYetImplemented()
keyguardInteractor.isKeyguardShowing()
} else {
_oldImpl.isKeyguardShowing
}
override fun startActivity(intent: Intent) {
if (useInteractor) {
notYetImplemented()
activityStarter.startActivity(intent, /* dismissShade= */ false)
} else {
_oldImpl.startActivity(intent)
}
@@ -221,7 +262,7 @@ constructor(
override fun refreshUsers(forcePictureLoadForId: Int) {
if (useInteractor) {
notYetImplemented()
userInteractor.refreshUsers()
} else {
_oldImpl.refreshUsers(forcePictureLoadForId)
}
@@ -229,7 +270,14 @@ constructor(
override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
if (useInteractor) {
notYetImplemented()
val interactorCallback =
object : UserInteractor.UserCallback {
override fun onUserStateChanged() {
callback.onUserSwitched()
}
}
callbackCompatMap[callback] = interactorCallback
userInteractor.addCallback(interactorCallback)
} else {
_oldImpl.addUserSwitchCallback(callback)
}
@@ -237,7 +285,10 @@ constructor(
override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
if (useInteractor) {
notYetImplemented()
val interactorCallback = callbackCompatMap.remove(callback)
if (interactorCallback != null) {
userInteractor.removeCallback(interactorCallback)
}
} else {
_oldImpl.removeUserSwitchCallback(callback)
}
@@ -245,7 +296,7 @@ constructor(
override fun dump(pw: PrintWriter, args: Array<out String>) {
if (useInteractor) {
notYetImplemented()
userInteractor.dump(pw)
} else {
_oldImpl.dump(pw, args)
}

View File

@@ -173,7 +173,7 @@ constructor(
}
/** Removes the guest user from the device. */
private suspend fun remove(
suspend fun remove(
@UserIdInt guestUserId: Int,
@UserIdInt targetUserId: Int,
showDialog: (ShowDialogRequestModel) -> Unit,

View File

@@ -52,8 +52,7 @@ import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
import com.android.systemui.util.kotlin.pairwise
import java.util.Collections
import java.util.WeakHashMap
import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -70,6 +69,9 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
/** Encapsulates business logic to interact with user data and systems. */
@@ -96,7 +98,9 @@ constructor(
* Defines interface for classes that can be notified when the state of users on the device is
* changed.
*/
fun interface UserCallback {
interface UserCallback {
/** Returns `true` if this callback can be cleaned-up. */
fun isEvictable(): Boolean = false
/** Notifies that the state of users on the device has changed. */
fun onUserStateChanged()
}
@@ -110,7 +114,8 @@ constructor(
com.android.internal.R.string.config_supervisedUserCreationPackage
)
private val callbacks = Collections.newSetFromMap(WeakHashMap<UserCallback, Boolean>())
private val callbackMutex = Mutex()
private val callbacks = mutableSetOf<UserCallback>()
/** List of current on-device users to select from. */
val users: Flow<List<UserModel>>
@@ -263,6 +268,7 @@ constructor(
}
)
}
.onEach { notifyCallbacks() }
.stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
@@ -307,30 +313,6 @@ constructor(
error("Not supported in the old implementation!")
}
fun addCallback(callback: UserCallback) {
callbacks.add(callback)
}
fun removeCallback(callback: UserCallback) {
callbacks.remove(callback)
}
fun onDialogShown() {
_dialogShowRequests.value = null
}
fun onDialogDismissed() {
_dialogDismissRequests.value = null
}
private fun showDialog(request: ShowDialogRequestModel) {
_dialogShowRequests.value = request
}
private fun dismissDialog() {
_dialogDismissRequests.value = Unit
}
init {
if (isNewImpl) {
refreshUsersScheduler.refreshIfNotPaused()
@@ -364,6 +346,46 @@ constructor(
}
}
fun addCallback(callback: UserCallback) {
applicationScope.launch { callbackMutex.withLock { callbacks.add(callback) } }
}
fun removeCallback(callback: UserCallback) {
applicationScope.launch { callbackMutex.withLock { callbacks.remove(callback) } }
}
fun refreshUsers() {
refreshUsersScheduler.refreshIfNotPaused()
}
fun onDialogShown() {
_dialogShowRequests.value = null
}
fun onDialogDismissed() {
_dialogDismissRequests.value = null
}
fun dump(pw: PrintWriter) {
pw.println("UserInteractor state:")
pw.println(" lastSelectedNonGuestUserId=${repository.lastSelectedNonGuestUserId}")
val users = userRecords.value.filter { it.info != null }
pw.println(" userCount=${userRecords.value.count { LegacyUserDataHelper.isUser(it) }}")
for (i in users.indices) {
pw.println(" ${users[i]}")
}
val actions = userRecords.value.filter { it.info == null }
pw.println(" actionCount=${userRecords.value.count { !LegacyUserDataHelper.isUser(it) }}")
for (i in actions.indices) {
pw.println(" ${actions[i]}")
}
pw.println("isSimpleUserSwitcher=$isSimpleUserSwitcher")
pw.println("isGuestUserAutoCreated=$isGuestUserAutoCreated")
}
fun onDeviceBootCompleted() {
guestUserInteractor.onDeviceBootCompleted()
}
@@ -460,6 +482,60 @@ constructor(
}
}
fun exitGuestUser(
@UserIdInt guestUserId: Int,
@UserIdInt targetUserId: Int,
forceRemoveGuestOnExit: Boolean,
) {
guestUserInteractor.exit(
guestUserId = guestUserId,
targetUserId = targetUserId,
forceRemoveGuestOnExit = forceRemoveGuestOnExit,
showDialog = this::showDialog,
dismissDialog = this::dismissDialog,
switchUser = this::switchUser,
)
}
fun removeGuestUser(
@UserIdInt guestUserId: Int,
@UserIdInt targetUserId: Int,
) {
applicationScope.launch {
guestUserInteractor.remove(
guestUserId = guestUserId,
targetUserId = targetUserId,
::showDialog,
::dismissDialog,
::selectUser,
)
}
}
private fun showDialog(request: ShowDialogRequestModel) {
_dialogShowRequests.value = request
}
private fun dismissDialog() {
_dialogDismissRequests.value = Unit
}
private fun notifyCallbacks() {
applicationScope.launch {
callbackMutex.withLock {
val iterator = callbacks.iterator()
while (iterator.hasNext()) {
val callback = iterator.next()
if (!callback.isEvictable()) {
callback.onUserStateChanged()
} else {
iterator.remove()
}
}
}
}
}
private suspend fun toRecord(
userInfo: UserInfo,
selectedUserId: Int,
@@ -498,21 +574,6 @@ constructor(
)
}
private fun exitGuestUser(
@UserIdInt guestUserId: Int,
@UserIdInt targetUserId: Int,
forceRemoveGuestOnExit: Boolean,
) {
guestUserInteractor.exit(
guestUserId = guestUserId,
targetUserId = targetUserId,
forceRemoveGuestOnExit = forceRemoveGuestOnExit,
showDialog = this::showDialog,
dismissDialog = this::dismissDialog,
switchUser = this::switchUser,
)
}
private fun switchUser(userId: Int) {
// TODO(b/246631653): track jank and lantecy like in the old impl.
refreshUsersScheduler.pause()
@@ -533,7 +594,7 @@ constructor(
dismissDialog()
val selectedUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
if (previousUserInfo?.id != selectedUserId) {
callbacks.forEach { it.onUserStateChanged() }
notifyCallbacks()
restartSecondaryService(selectedUserId)
}
if (guestUserInteractor.isGuestUserAutoCreated) {

View File

@@ -83,6 +83,21 @@ object LegacyUserDataHelper {
)
}
fun toUserActionModel(record: UserRecord): UserActionModel {
check(!isUser(record))
return when {
record.isAddUser -> UserActionModel.ADD_USER
record.isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER
record.isGuest -> UserActionModel.ENTER_GUEST_MODE
else -> error("Not a known action: $record")
}
}
fun isUser(record: UserRecord): Boolean {
return record.info != null
}
private fun getEnforcedAdmin(
context: Context,
selectedUserId: Int,

View File

@@ -289,15 +289,73 @@ class GuestUserInteractorTest : SysuiTestCase() {
verifyDidNotExit()
}
@Test
fun `remove - returns to target user`() =
runBlocking(IMMEDIATE) {
whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
repository.setSelectedUserInfo(GUEST_USER_INFO)
val targetUserId = NON_GUEST_USER_INFO.id
underTest.remove(
guestUserId = GUEST_USER_INFO.id,
targetUserId = targetUserId,
showDialog = showDialog,
dismissDialog = dismissDialog,
switchUser = switchUser,
)
verify(manager).markGuestForDeletion(GUEST_USER_INFO.id)
verify(manager).removeUser(GUEST_USER_INFO.id)
verify(switchUser).invoke(targetUserId)
}
@Test
fun `remove - selected different from guest user - do nothing`() =
runBlocking(IMMEDIATE) {
whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
underTest.remove(
guestUserId = GUEST_USER_INFO.id,
targetUserId = 123,
showDialog = showDialog,
dismissDialog = dismissDialog,
switchUser = switchUser,
)
verifyDidNotRemove()
}
@Test
fun `remove - selected is actually not a guest user - do nothing`() =
runBlocking(IMMEDIATE) {
whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
underTest.remove(
guestUserId = NON_GUEST_USER_INFO.id,
targetUserId = 123,
showDialog = showDialog,
dismissDialog = dismissDialog,
switchUser = switchUser,
)
verifyDidNotRemove()
}
private fun setAllowedToAdd(isAllowed: Boolean = true) {
whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(isAllowed)
whenever(devicePolicyManager.isDeviceManaged).thenReturn(!isAllowed)
}
private fun verifyDidNotExit() {
verifyDidNotRemove()
verify(manager, never()).getUserInfo(anyInt())
verify(manager, never()).markGuestForDeletion(anyInt())
verify(uiEventLogger, never()).log(any())
}
private fun verifyDidNotRemove() {
verify(manager, never()).markGuestForDeletion(anyInt())
verify(showDialog, never()).invoke(any())
verify(dismissDialog, never()).invoke()
verify(switchUser, never()).invoke(anyInt())