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 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(false)
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 =
149                 Utils.getColorAttrDefaultColor(
150                     context,
151                     R.attr.onShadeInactiveVariant,
152                 ),
153             backgroundColor = R.attr.shadeInactive,
154             this::onSettingsButtonClicked,
155         )
156 
157     /** The model for the power button. */
158     val power: FooterActionsButtonViewModel? =
159         if (showPowerButton) {
160             FooterActionsButtonViewModel(
161                 id = R.id.pm_lite,
162                 Icon.Resource(
163                     android.R.drawable.ic_lock_power_off,
164                     ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu)
165                 ),
166                 iconTint =
167                     Utils.getColorAttrDefaultColor(
168                         context,
169                         R.attr.onShadeActive,
170                     ),
171                 backgroundColor = R.attr.shadeActive,
172                 this::onPowerButtonClicked,
173             )
174         } else {
175             null
176         }
177 
178     /** Called when the visibility of the UI rendering this model should be changed. */
179     fun onVisibilityChangeRequested(visible: Boolean) {
180         _isVisible.value = visible
181     }
182 
183     /** Called when the expansion of the Quick Settings changed. */
184     fun onQuickSettingsExpansionChanged(expansion: Float, isInSplitShade: Boolean) {
185         if (isInSplitShade) {
186             // In split shade, we want to fade in the background only at the very end (see
187             // b/240563302).
188             val delay = 0.99f
189             _alpha.value = expansion
190             _backgroundAlpha.value = max(0f, expansion - delay) / (1f - delay)
191         } else {
192             // Only start fading in the footer actions when we are at least 90% expanded.
193             val delay = 0.9f
194             _alpha.value = max(0f, expansion - delay) / (1 - delay)
195             _backgroundAlpha.value = 1f
196         }
197     }
198 
199     /**
200      * Observe the device monitoring dialog requests and show the dialog accordingly. This function
201      * will suspend indefinitely and will need to be cancelled to stop observing.
202      *
203      * Important: [quickSettingsContext] must be the [Context] associated to the
204      * [Quick Settings fragment][com.android.systemui.qs.QSFragment], and the call to this function
205      * must be cancelled when that fragment is destroyed.
206      */
207     suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) {
208         footerActionsInteractor.deviceMonitoringDialogRequests.collect {
209             footerActionsInteractor.showDeviceMonitoringDialog(
210                 quickSettingsContext,
211                 expandable = null,
212             )
213         }
214     }
215 
216     private fun onSecurityButtonClicked(quickSettingsContext: Context, expandable: Expandable) {
217         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
218             return
219         }
220 
221         footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext, expandable)
222     }
223 
224     private fun onForegroundServiceButtonClicked(expandable: Expandable) {
225         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
226             return
227         }
228 
229         footerActionsInteractor.showForegroundServicesDialog(expandable)
230     }
231 
232     private fun onUserSwitcherClicked(expandable: Expandable) {
233         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
234             return
235         }
236 
237         footerActionsInteractor.showUserSwitcher(expandable)
238     }
239 
240     private fun onSettingsButtonClicked(expandable: Expandable) {
241         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
242             return
243         }
244 
245         footerActionsInteractor.showSettings(expandable)
246     }
247 
248     private fun onPowerButtonClicked(expandable: Expandable) {
249         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
250             return
251         }
252 
253         footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, expandable)
254     }
255 
256     private fun userSwitcherButton(
257         status: UserSwitcherStatusModel.Enabled
258     ): FooterActionsButtonViewModel {
259         val icon = status.currentUserImage!!
260 
261         return FooterActionsButtonViewModel(
262             id = R.id.multi_user_switch,
263             icon =
264                 Icon.Loaded(
265                     icon,
266                     ContentDescription.Loaded(
267                         userSwitcherContentDescription(status.currentUserName)
268                     ),
269                 ),
270             iconTint = null,
271             backgroundColor = R.attr.shadeInactive,
272             onClick = this::onUserSwitcherClicked,
273         )
274     }
275 
276     private fun userSwitcherContentDescription(currentUser: String?): String? {
277         return currentUser?.let { user ->
278             context.getString(R.string.accessibility_quick_settings_user, user)
279         }
280     }
281 
282     @SysUISingleton
283     class Factory
284     @Inject
285     constructor(
286         @Application private val context: Context,
287         private val falsingManager: FalsingManager,
288         private val footerActionsInteractor: FooterActionsInteractor,
289         private val globalActionsDialogLiteProvider: Provider<GlobalActionsDialogLite>,
290         @Named(PM_LITE_ENABLED) private val showPowerButton: Boolean,
291     ) {
292         /** Create a [FooterActionsViewModel] bound to the lifecycle of [lifecycleOwner]. */
293         fun create(lifecycleOwner: LifecycleOwner): FooterActionsViewModel {
294             val globalActionsDialogLite = globalActionsDialogLiteProvider.get()
295             if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
296                 // This should usually not happen, but let's make sure we already destroy
297                 // globalActionsDialogLite.
298                 globalActionsDialogLite.destroy()
299             } else {
300                 // Destroy globalActionsDialogLite when the lifecycle is destroyed.
301                 lifecycleOwner.lifecycle.addObserver(
302                     object : DefaultLifecycleObserver {
303                         override fun onDestroy(owner: LifecycleOwner) {
304                             globalActionsDialogLite.destroy()
305                         }
306                     }
307                 )
308             }
309 
310             return FooterActionsViewModel(
311                 context,
312                 footerActionsInteractor,
313                 falsingManager,
314                 globalActionsDialogLite,
315                 showPowerButton,
316             )
317         }
318     }
319 
320     companion object {
321         private const val TAG = "FooterActionsViewModel"
322     }
323 }
324