• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 package com.android.systemui.qs.footer.ui.viewmodel
18 
19 import android.content.Context
20 import android.util.Log
21 import android.view.ContextThemeWrapper
22 import androidx.lifecycle.DefaultLifecycleObserver
23 import androidx.lifecycle.Lifecycle
24 import androidx.lifecycle.LifecycleOwner
25 import com.android.settingslib.Utils
26 import com.android.systemui.R
27 import com.android.systemui.animation.Expandable
28 import com.android.systemui.common.shared.model.ContentDescription
29 import com.android.systemui.common.shared.model.Icon
30 import com.android.systemui.dagger.SysUISingleton
31 import com.android.systemui.dagger.qualifiers.Application
32 import com.android.systemui.globalactions.GlobalActionsDialogLite
33 import com.android.systemui.plugins.FalsingManager
34 import com.android.systemui.qs.dagger.QSFlagsModule.PM_LITE_ENABLED
35 import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel
36 import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor
37 import com.android.systemui.util.icuMessageFormat
38 import javax.inject.Inject
39 import javax.inject.Named
40 import javax.inject.Provider
41 import kotlin.math.max
42 import kotlinx.coroutines.flow.Flow
43 import kotlinx.coroutines.flow.MutableStateFlow
44 import kotlinx.coroutines.flow.StateFlow
45 import kotlinx.coroutines.flow.asStateFlow
46 import kotlinx.coroutines.flow.collect
47 import kotlinx.coroutines.flow.combine
48 import kotlinx.coroutines.flow.distinctUntilChanged
49 import kotlinx.coroutines.flow.map
50 
51 /** A ViewModel for the footer actions. */
52 class FooterActionsViewModel(
53     @Application appContext: Context,
54     private val footerActionsInteractor: FooterActionsInteractor,
55     private val falsingManager: FalsingManager,
56     private val globalActionsDialogLite: GlobalActionsDialogLite,
57     showPowerButton: Boolean,
58 ) {
59     /** The context themed with the Quick Settings colors. */
60     private val context = ContextThemeWrapper(appContext, R.style.Theme_SystemUI_QuickSettings)
61 
62     /**
63      * Whether the UI rendering this ViewModel should be visible. Note that even when this is false,
64      * the UI should still participate to the layout it is included in (i.e. in the View world it
65      * should be INVISIBLE, not GONE).
66      */
67     private val _isVisible = MutableStateFlow(true)
68     val isVisible: StateFlow<Boolean> = _isVisible.asStateFlow()
69 
70     /** The alpha the UI rendering this ViewModel should have. */
71     private val _alpha = MutableStateFlow(1f)
72     val alpha: StateFlow<Float> = _alpha.asStateFlow()
73 
74     /** The alpha the background of the UI rendering this ViewModel should have. */
75     private val _backgroundAlpha = MutableStateFlow(1f)
76     val backgroundAlpha: StateFlow<Float> = _backgroundAlpha.asStateFlow()
77 
78     /** The model for the security button. */
79     val security: Flow<FooterActionsSecurityButtonViewModel?> =
80         footerActionsInteractor.securityButtonConfig
81             .map { config ->
82                 val (icon, text, isClickable) = config ?: return@map null
83                 FooterActionsSecurityButtonViewModel(
84                     icon,
85                     text,
86                     if (isClickable) this::onSecurityButtonClicked else null,
87                 )
88             }
89             .distinctUntilChanged()
90 
91     /** The model for the foreground services button. */
92     val foregroundServices: Flow<FooterActionsForegroundServicesButtonViewModel?> =
93         combine(
94                 footerActionsInteractor.foregroundServicesCount,
95                 footerActionsInteractor.hasNewForegroundServices,
96                 security,
97             ) { foregroundServicesCount, hasNewChanges, securityModel ->
98                 if (foregroundServicesCount <= 0) {
99                     return@combine null
100                 }
101 
102                 val text =
103                     icuMessageFormat(
104                         context.resources,
105                         R.string.fgs_manager_footer_label,
106                         foregroundServicesCount,
107                     )
108                 FooterActionsForegroundServicesButtonViewModel(
109                     foregroundServicesCount,
110                     text = text,
111                     displayText = securityModel == null,
112                     hasNewChanges = hasNewChanges,
113                     this::onForegroundServiceButtonClicked,
114                 )
115             }
116             .distinctUntilChanged()
117 
118     /** The model for the user switcher button. */
119     val userSwitcher: Flow<FooterActionsButtonViewModel?> =
120         footerActionsInteractor.userSwitcherStatus
121             .map { userSwitcherStatus ->
122                 when (userSwitcherStatus) {
123                     UserSwitcherStatusModel.Disabled -> null
124                     is UserSwitcherStatusModel.Enabled -> {
125                         if (userSwitcherStatus.currentUserImage == null) {
126                             Log.e(
127                                 TAG,
128                                 "Skipped the addition of user switcher button because " +
129                                     "currentUserImage is missing",
130                             )
131                             return@map null
132                         }
133 
134                         userSwitcherButton(userSwitcherStatus)
135                     }
136                 }
137             }
138             .distinctUntilChanged()
139 
140     /** The model for the settings button. */
141     val settings: FooterActionsButtonViewModel =
142         FooterActionsButtonViewModel(
143             id = R.id.settings_button_container,
144             Icon.Resource(
145                 R.drawable.ic_settings,
146                 ContentDescription.Resource(R.string.accessibility_quick_settings_settings)
147             ),
148             iconTint = null,
149             backgroundColor = R.attr.offStateColor,
150             this::onSettingsButtonClicked,
151         )
152 
153     /** The model for the power button. */
154     val power: FooterActionsButtonViewModel? =
155         if (showPowerButton) {
156             FooterActionsButtonViewModel(
157                 id = R.id.pm_lite,
158                 Icon.Resource(
159                     android.R.drawable.ic_lock_power_off,
160                     ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu)
161                 ),
162                 iconTint =
163                     Utils.getColorAttrDefaultColor(
164                         context,
165                         com.android.internal.R.attr.textColorOnAccent,
166                     ),
167                 backgroundColor = com.android.internal.R.attr.colorAccent,
168                 this::onPowerButtonClicked,
169             )
170         } else {
171             null
172         }
173 
174     /** Called when the visibility of the UI rendering this model should be changed. */
175     fun onVisibilityChangeRequested(visible: Boolean) {
176         _isVisible.value = visible
177     }
178 
179     /** Called when the expansion of the Quick Settings changed. */
180     fun onQuickSettingsExpansionChanged(expansion: Float, isInSplitShade: Boolean) {
181         if (isInSplitShade) {
182             // In split shade, we want to fade in the background only at the very end (see
183             // b/240563302).
184             val delay = 0.99f
185             _alpha.value = expansion
186             _backgroundAlpha.value = max(0f, expansion - delay) / (1f - delay)
187         } else {
188             // Only start fading in the footer actions when we are at least 90% expanded.
189             val delay = 0.9f
190             _alpha.value = max(0f, expansion - delay) / (1 - delay)
191             _backgroundAlpha.value = 1f
192         }
193     }
194 
195     /**
196      * Observe the device monitoring dialog requests and show the dialog accordingly. This function
197      * will suspend indefinitely and will need to be cancelled to stop observing.
198      *
199      * Important: [quickSettingsContext] must be the [Context] associated to the
200      * [Quick Settings fragment][com.android.systemui.qs.QSFragment], and the call to this function
201      * must be cancelled when that fragment is destroyed.
202      */
203     suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) {
204         footerActionsInteractor.deviceMonitoringDialogRequests.collect {
205             footerActionsInteractor.showDeviceMonitoringDialog(
206                 quickSettingsContext,
207                 expandable = null,
208             )
209         }
210     }
211 
212     private fun onSecurityButtonClicked(quickSettingsContext: Context, expandable: Expandable) {
213         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
214             return
215         }
216 
217         footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext, expandable)
218     }
219 
220     private fun onForegroundServiceButtonClicked(expandable: Expandable) {
221         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
222             return
223         }
224 
225         footerActionsInteractor.showForegroundServicesDialog(expandable)
226     }
227 
228     private fun onUserSwitcherClicked(expandable: Expandable) {
229         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
230             return
231         }
232 
233         footerActionsInteractor.showUserSwitcher(expandable)
234     }
235 
236     private fun onSettingsButtonClicked(expandable: Expandable) {
237         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
238             return
239         }
240 
241         footerActionsInteractor.showSettings(expandable)
242     }
243 
244     private fun onPowerButtonClicked(expandable: Expandable) {
245         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
246             return
247         }
248 
249         footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, expandable)
250     }
251 
252     private fun userSwitcherButton(
253         status: UserSwitcherStatusModel.Enabled
254     ): FooterActionsButtonViewModel {
255         val icon = status.currentUserImage!!
256 
257         return FooterActionsButtonViewModel(
258             id = R.id.multi_user_switch,
259             icon =
260                 Icon.Loaded(
261                     icon,
262                     ContentDescription.Loaded(
263                         userSwitcherContentDescription(status.currentUserName)
264                     ),
265                 ),
266             iconTint = null,
267             backgroundColor = R.attr.offStateColor,
268             onClick = this::onUserSwitcherClicked,
269         )
270     }
271 
272     private fun userSwitcherContentDescription(currentUser: String?): String? {
273         return currentUser?.let { user ->
274             context.getString(R.string.accessibility_quick_settings_user, user)
275         }
276     }
277 
278     @SysUISingleton
279     class Factory
280     @Inject
281     constructor(
282         @Application private val context: Context,
283         private val falsingManager: FalsingManager,
284         private val footerActionsInteractor: FooterActionsInteractor,
285         private val globalActionsDialogLiteProvider: Provider<GlobalActionsDialogLite>,
286         @Named(PM_LITE_ENABLED) private val showPowerButton: Boolean,
287     ) {
288         /** Create a [FooterActionsViewModel] bound to the lifecycle of [lifecycleOwner]. */
289         fun create(lifecycleOwner: LifecycleOwner): FooterActionsViewModel {
290             val globalActionsDialogLite = globalActionsDialogLiteProvider.get()
291             if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
292                 // This should usually not happen, but let's make sure we already destroy
293                 // globalActionsDialogLite.
294                 globalActionsDialogLite.destroy()
295             } else {
296                 // Destroy globalActionsDialogLite when the lifecycle is destroyed.
297                 lifecycleOwner.lifecycle.addObserver(
298                     object : DefaultLifecycleObserver {
299                         override fun onDestroy(owner: LifecycleOwner) {
300                             globalActionsDialogLite.destroy()
301                         }
302                     }
303                 )
304             }
305 
306             return FooterActionsViewModel(
307                 context,
308                 footerActionsInteractor,
309                 falsingManager,
310                 globalActionsDialogLite,
311                 showPowerButton,
312             )
313         }
314     }
315 
316     companion object {
317         private const val TAG = "FooterActionsViewModel"
318     }
319 }
320