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