1 /* <lambda>null2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package com.android.systemui.user.ui.viewmodel 19 20 import com.android.systemui.common.shared.model.Text 21 import com.android.systemui.common.ui.drawable.CircularDrawable 22 import com.android.systemui.dagger.SysUISingleton 23 import com.android.systemui.user.domain.interactor.GuestUserInteractor 24 import com.android.systemui.user.domain.interactor.UserSwitcherInteractor 25 import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper 26 import com.android.systemui.user.shared.model.UserActionModel 27 import com.android.systemui.user.shared.model.UserModel 28 import javax.inject.Inject 29 import kotlin.math.ceil 30 import kotlinx.coroutines.flow.Flow 31 import kotlinx.coroutines.flow.MutableStateFlow 32 import kotlinx.coroutines.flow.combine 33 import kotlinx.coroutines.flow.map 34 35 /** Models UI state for the user switcher feature. */ 36 @SysUISingleton 37 class UserSwitcherViewModel 38 @Inject 39 constructor( 40 private val userSwitcherInteractor: UserSwitcherInteractor, 41 private val guestUserInteractor: GuestUserInteractor, 42 ) { 43 44 /** The currently selected user. */ 45 val selectedUser: Flow<UserViewModel> = 46 userSwitcherInteractor.selectedUser.map { user -> toViewModel(user) } 47 48 /** On-device users. */ 49 val users: Flow<List<UserViewModel>> = 50 userSwitcherInteractor.users.map { models -> models.map { user -> toViewModel(user) } } 51 52 /** The maximum number of columns that the user selection grid should use. */ 53 val maximumUserColumns: Flow<Int> = users.map { getMaxUserSwitcherItemColumns(it.size) } 54 55 private val _isMenuVisible = MutableStateFlow(false) 56 /** 57 * Whether the user action menu should be shown. Once the action menu is dismissed/closed, the 58 * consumer must invoke [onMenuClosed]. 59 */ 60 val isMenuVisible: Flow<Boolean> = _isMenuVisible 61 /** The user action menu. */ 62 val menu: Flow<List<UserActionViewModel>> = 63 userSwitcherInteractor.actions.map { actions -> 64 actions.map { action -> toViewModel(action) } 65 } 66 67 /** Whether the button to open the user action menu is visible. */ 68 val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() } 69 70 private val hasCancelButtonBeenClicked = MutableStateFlow(false) 71 private val isFinishRequiredDueToExecutedAction = MutableStateFlow(false) 72 private val userSwitched = MutableStateFlow(false) 73 74 /** 75 * Whether the observer should finish the experience. Once consumed, [onFinished] must be called 76 * by the consumer. 77 */ 78 val isFinishRequested: Flow<Boolean> = createFinishRequestedFlow() 79 80 /** Notifies that the user has clicked the cancel button. */ 81 fun onCancelButtonClicked() { 82 hasCancelButtonBeenClicked.value = true 83 } 84 85 /** 86 * Notifies that the user experience is finished. 87 * 88 * Call this after consuming [isFinishRequested] with a `true` value in order to mark it as 89 * consumed such that the next consumer doesn't immediately finish itself. 90 */ 91 fun onFinished() { 92 hasCancelButtonBeenClicked.value = false 93 isFinishRequiredDueToExecutedAction.value = false 94 userSwitched.value = false 95 } 96 97 /** Notifies that the user has clicked the "open menu" button. */ 98 fun onOpenMenuButtonClicked() { 99 _isMenuVisible.value = true 100 } 101 102 /** 103 * Notifies that the user has dismissed or closed the user action menu. 104 * 105 * Call this after consuming [isMenuVisible] with a `true` value in order to reset it to `false` 106 * such that the next consumer doesn't immediately show the menu again. 107 */ 108 fun onMenuClosed() { 109 _isMenuVisible.value = false 110 } 111 112 /** Returns the maximum number of columns for user items in the user switcher. */ 113 private fun getMaxUserSwitcherItemColumns(userCount: Int): Int { 114 return if (userCount < 5) { 115 4 116 } else { 117 ceil(userCount / 2.0).toInt() 118 } 119 } 120 121 private fun createFinishRequestedFlow(): Flow<Boolean> = 122 combine( 123 // When the cancel button is clicked, we should finish. 124 hasCancelButtonBeenClicked, 125 // If an executed action told us to finish, we should finish, 126 isFinishRequiredDueToExecutedAction, 127 userSwitched, 128 ) { cancelButtonClicked, executedActionFinish, userSwitched -> 129 cancelButtonClicked || executedActionFinish || userSwitched 130 } 131 132 private fun toViewModel( 133 model: UserModel, 134 ): UserViewModel { 135 return UserViewModel( 136 viewKey = model.id, 137 name = 138 if (model.isGuest && model.isSelected) { 139 Text.Resource(com.android.settingslib.R.string.guest_exit_quick_settings_button) 140 } else { 141 model.name 142 }, 143 image = CircularDrawable(model.image), 144 isSelectionMarkerVisible = model.isSelected, 145 alpha = 146 if (model.isSelectable) { 147 LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA 148 } else { 149 LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_NOT_SELECTABLE_ALPHA 150 }, 151 onClicked = createOnSelectedCallback(model), 152 ) 153 } 154 155 private fun toViewModel( 156 model: UserActionModel, 157 ): UserActionViewModel { 158 return UserActionViewModel( 159 viewKey = model.ordinal.toLong(), 160 iconResourceId = 161 LegacyUserUiHelper.getUserSwitcherActionIconResourceId( 162 isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER, 163 isAddUser = model == UserActionModel.ADD_USER, 164 isGuest = model == UserActionModel.ENTER_GUEST_MODE, 165 isManageUsers = model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, 166 isTablet = true, 167 ), 168 textResourceId = 169 LegacyUserUiHelper.getUserSwitcherActionTextResourceId( 170 isGuest = model == UserActionModel.ENTER_GUEST_MODE, 171 isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated, 172 isGuestUserResetting = guestUserInteractor.isGuestUserResetting, 173 isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER, 174 isAddUser = model == UserActionModel.ADD_USER, 175 isManageUsers = model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, 176 isTablet = true, 177 ), 178 onClicked = { 179 userSwitcherInteractor.executeAction(action = model) 180 // We don't finish because we want to show a dialog over the full-screen UI and 181 // that dialog can be dismissed in case the user changes their mind and decides not 182 // to add a user. 183 // 184 // We finish for all other actions because they navigate us away from the 185 // full-screen experience or are destructive (like changing to the guest user). 186 val shouldFinish = model != UserActionModel.ADD_USER 187 if (shouldFinish) { 188 isFinishRequiredDueToExecutedAction.value = true 189 } 190 }, 191 ) 192 } 193 194 private fun createOnSelectedCallback(model: UserModel): (() -> Unit)? { 195 return if (!model.isSelectable) { 196 null 197 } else { 198 { 199 userSwitcherInteractor.selectUser(model.id) 200 userSwitched.value = true 201 } 202 } 203 } 204 } 205