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