diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java index 5b522dcc4885e..0c72b78a3c46c 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java +++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java @@ -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, } ) diff --git a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt index 2e9367126a4c0..2095683ccb4c6 100644 --- a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt @@ -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, ) diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt new file mode 100644 index 0000000000000..c1d2f4788147a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt @@ -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 +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt new file mode 100644 index 0000000000000..690e987bd9ab2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt @@ -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() + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt index 398341d256d21..dbb8200d57d8d 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt @@ -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> = userInteractor.users.map { models -> models.map { user -> toViewModel(user) } } @@ -47,9 +55,6 @@ private constructor( val maximumUserColumns: Flow = users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) } - /** Whether the button to open the user action menu is visible. */ - val isOpenMenuButtonVisible: Flow = 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 = _isMenuVisible /** The user action menu. */ val menu: Flow> = - 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 = 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 create(modelClass: Class): T { @Suppress("UNCHECKED_CAST") return UserSwitcherViewModel( userInteractor = userInteractor, + guestUserInteractor = guestUserInteractor, powerInteractor = powerInteractor, + featureFlags = featureFlags, ) as T } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index 3a84064be61db..7fd29af2a80a1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -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, ) } )