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:
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user