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