user UI layer changes to support USC dep removal

Minor changes to the view-model class and the addition of a
UserSwitcherDialogCoordinator to be able to show and dismiss dialogs
based on state exposed by the interactor.

Bug: 246631653
Test: unit tests for new coordinator class included, manually verified
the operations of the full-screen user switcher, the smaller dialog, the
footer in quick settings, and the dropdown switcher on the lock-screen
bouncer.

Change-Id: I058541bb5c31023735121bbad54b93afba73a89a
This commit is contained in:
Alejandro Nijamkin
2022-09-27 18:04:04 -07:00
parent fbbb1a8bd5
commit ae0fcebd92
6 changed files with 227 additions and 25 deletions

View File

@@ -20,6 +20,7 @@ import android.app.Activity;
import com.android.settingslib.users.EditUserInfoController;
import com.android.systemui.user.data.repository.UserRepositoryModule;
import com.android.systemui.user.ui.dialog.UserDialogModule;
import dagger.Binds;
import dagger.Module;
@@ -32,6 +33,7 @@ import dagger.multibindings.IntoMap;
*/
@Module(
includes = {
UserDialogModule.class,
UserRepositoryModule.class,
}
)

View File

@@ -33,6 +33,5 @@ data class UserModel(
/** Whether this use is selectable. A non-selectable user cannot be switched to. */
val isSelectable: Boolean,
/** Whether this model represents the guest user. */
// TODO(b/246631653): remove this default value it was only here to be able to split up CLs
val isGuest: Boolean = false,
val isGuest: Boolean,
)

View File

@@ -0,0 +1,33 @@
/*
* 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.dialog
import com.android.systemui.CoreStartable
import dagger.Binds
import dagger.Module
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
interface UserDialogModule {
@Binds
@IntoMap
@ClassKey(UserSwitcherDialogCoordinator::class)
fun bindFeature(impl: UserSwitcherDialogCoordinator): CoreStartable
}

View File

@@ -0,0 +1,122 @@
/*
* 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.dialog
import android.app.Dialog
import android.content.Context
import com.android.settingslib.users.UserCreatingDialog
import com.android.systemui.CoreStartable
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 javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
/** Coordinates dialogs for user switcher logic. */
@SysUISingleton
class UserSwitcherDialogCoordinator
@Inject
constructor(
@Application private val context: Context,
@Application private val applicationScope: CoroutineScope,
private val falsingManager: FalsingManager,
private val broadcastSender: BroadcastSender,
private val dialogLaunchAnimator: DialogLaunchAnimator,
private val interactor: UserInteractor,
private val featureFlags: FeatureFlags,
) : CoreStartable(context) {
private var currentDialog: Dialog? = null
override fun start() {
if (!featureFlags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER)) {
return
}
startHandlingDialogShowRequests()
startHandlingDialogDismissRequests()
}
private fun startHandlingDialogShowRequests() {
applicationScope.launch {
interactor.dialogShowRequests.filterNotNull().collect { request ->
currentDialog?.let {
if (it.isShowing) {
it.cancel()
}
}
currentDialog =
when (request) {
is ShowDialogRequestModel.ShowAddUserDialog ->
AddUserDialog(
context = context,
userHandle = request.userHandle,
isKeyguardShowing = request.isKeyguardShowing,
showEphemeralMessage = request.showEphemeralMessage,
falsingManager = falsingManager,
broadcastSender = broadcastSender,
dialogLaunchAnimator = dialogLaunchAnimator,
)
is ShowDialogRequestModel.ShowUserCreationDialog ->
UserCreatingDialog(
context,
request.isGuest,
)
is ShowDialogRequestModel.ShowExitGuestDialog ->
ExitGuestDialog(
context = context,
guestUserId = request.guestUserId,
isGuestEphemeral = request.isGuestEphemeral,
targetUserId = request.targetUserId,
isKeyguardShowing = request.isKeyguardShowing,
falsingManager = falsingManager,
dialogLaunchAnimator = dialogLaunchAnimator,
onExitGuestUserListener = request.onExitGuestUser,
)
}
currentDialog?.show()
interactor.onDialogShown()
}
}
}
private fun startHandlingDialogDismissRequests() {
applicationScope.launch {
interactor.dialogDismissRequests.filterNotNull().collect {
currentDialog?.let {
if (it.isShowing) {
it.cancel()
}
}
interactor.onDialogDismissed()
}
}
}
}

View File

@@ -21,7 +21,10 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.android.systemui.R
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
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel
@@ -36,9 +39,14 @@ import kotlinx.coroutines.flow.map
class UserSwitcherViewModel
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.REFACTORED_USER_SWITCHER_CONTROLLER)
/** On-device users. */
val users: Flow<List<UserViewModel>> =
userInteractor.users.map { models -> models.map { user -> toViewModel(user) } }
@@ -47,9 +55,6 @@ private constructor(
val maximumUserColumns: Flow<Int> =
users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) }
/** Whether the button to open the user action menu is visible. */
val isOpenMenuButtonVisible: Flow<Boolean> = userInteractor.actions.map { it.isNotEmpty() }
private val _isMenuVisible = MutableStateFlow(false)
/**
* Whether the user action menu should be shown. Once the action menu is dismissed/closed, the
@@ -58,9 +63,23 @@ private constructor(
val isMenuVisible: Flow<Boolean> = _isMenuVisible
/** The user action menu. */
val menu: Flow<List<UserActionViewModel>> =
userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } }
userInteractor.actions.map { actions ->
if (isNewImpl && actions.isNotEmpty()) {
// If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because that's a user
// switcher specific action that is not known to the our data source or other
// features.
actions + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
} else {
actions
}
.map { action -> toViewModel(action) }
}
/** Whether the button to open the user action menu is visible. */
val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() }
private val hasCancelButtonBeenClicked = MutableStateFlow(false)
private val isFinishRequiredDueToExecutedAction = MutableStateFlow(false)
/**
* Whether the observer should finish the experience. Once consumed, [onFinished] must be called
@@ -81,6 +100,7 @@ private constructor(
*/
fun onFinished() {
hasCancelButtonBeenClicked.value = false
isFinishRequiredDueToExecutedAction.value = false
}
/** Notifies that the user has clicked the "open menu" button. */
@@ -120,8 +140,10 @@ private constructor(
},
// When the cancel button is clicked, we should finish.
hasCancelButtonBeenClicked,
) { selectedUserChanged, screenTurnedOff, cancelButtonClicked ->
selectedUserChanged || screenTurnedOff || cancelButtonClicked
// If an executed action told us to finish, we should finish,
isFinishRequiredDueToExecutedAction,
) { selectedUserChanged, screenTurnedOff, cancelButtonClicked, executedActionFinish ->
selectedUserChanged || screenTurnedOff || cancelButtonClicked || executedActionFinish
}
}
@@ -164,13 +186,25 @@ private constructor(
} else {
LegacyUserUiHelper.getUserSwitcherActionTextResourceId(
isGuest = model == UserActionModel.ENTER_GUEST_MODE,
isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
isGuestUserResetting = userInteractor.isGuestUserResetting,
isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated,
isGuestUserResetting = guestUserInteractor.isGuestUserResetting,
isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
isAddUser = model == UserActionModel.ADD_USER,
)
},
onClicked = { userInteractor.executeAction(action = model) },
onClicked = {
userInteractor.executeAction(action = model)
// We don't finish because we want to show a dialog over the full-screen UI and
// that dialog can be dismissed in case the user changes their mind and decides not
// to add a user.
//
// We finish for all other actions because they navigate us away from the
// full-screen experience or are destructive (like changing to the guest user).
val shouldFinish = model != UserActionModel.ADD_USER
if (shouldFinish) {
isFinishRequiredDueToExecutedAction.value = true
}
},
)
}
@@ -186,13 +220,17 @@ private constructor(
@Inject
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")
return UserSwitcherViewModel(
userInteractor = userInteractor,
guestUserInteractor = guestUserInteractor,
powerInteractor = powerInteractor,
featureFlags = featureFlags,
)
as T
}

View File

@@ -92,6 +92,20 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
mainDispatcher = IMMEDIATE,
repository = userRepository,
)
val guestUserInteractor =
GuestUserInteractor(
applicationContext = context,
applicationScope = scope,
mainDispatcher = IMMEDIATE,
backgroundDispatcher = IMMEDIATE,
manager = manager,
repository = userRepository,
deviceProvisionedController = deviceProvisionedController,
devicePolicyManager = devicePolicyManager,
refreshUsersScheduler = refreshUsersScheduler,
uiEventLogger = uiEventLogger,
)
underTest =
UserSwitcherViewModel.Factory(
userInteractor =
@@ -115,24 +129,14 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
backgroundDispatcher = IMMEDIATE,
activityManager = activityManager,
refreshUsersScheduler = refreshUsersScheduler,
guestUserInteractor =
GuestUserInteractor(
applicationContext = context,
applicationScope = scope,
mainDispatcher = IMMEDIATE,
backgroundDispatcher = IMMEDIATE,
manager = manager,
repository = userRepository,
deviceProvisionedController = deviceProvisionedController,
devicePolicyManager = devicePolicyManager,
refreshUsersScheduler = refreshUsersScheduler,
uiEventLogger = uiEventLogger,
),
guestUserInteractor = guestUserInteractor,
),
powerInteractor =
PowerInteractor(
repository = powerRepository,
),
featureFlags = featureFlags,
guestUserInteractor = guestUserInteractor,
)
.create(UserSwitcherViewModel::class.java)
}
@@ -148,6 +152,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
image = USER_IMAGE,
isSelected = true,
isSelectable = true,
isGuest = false,
),
UserModel(
id = 1,
@@ -155,6 +160,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
image = USER_IMAGE,
isSelected = false,
isSelectable = true,
isGuest = false,
),
UserModel(
id = 2,
@@ -162,6 +168,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
image = USER_IMAGE,
isSelected = false,
isSelectable = false,
isGuest = false,
),
)
)
@@ -311,7 +318,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
job.cancel()
}
private fun setUsers(count: Int) {
private suspend fun setUsers(count: Int) {
userRepository.setUsers(
(0 until count).map { index ->
UserModel(
@@ -320,6 +327,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
image = USER_IMAGE,
isSelected = index == 0,
isSelectable = true,
isGuest = false,
)
}
)