• 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.LifecycleCoroutineScope
25 import androidx.lifecycle.LifecycleOwner
26 import com.android.app.tracing.coroutines.launchTraced as launch
27 import com.android.settingslib.Utils
28 import com.android.systemui.animation.Expandable
29 import com.android.systemui.common.shared.model.ContentDescription
30 import com.android.systemui.common.shared.model.Icon
31 import com.android.systemui.dagger.SysUISingleton
32 import com.android.systemui.globalactions.GlobalActionsDialogLite
33 import com.android.systemui.plugins.ActivityStarter
34 import com.android.systemui.plugins.FalsingManager
35 import com.android.systemui.qs.dagger.QSFlagsModule.PM_LITE_ENABLED
36 import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel
37 import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor
38 import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig
39 import com.android.systemui.res.R
40 import com.android.systemui.shade.ShadeDisplayAware
41 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
42 import com.android.systemui.shade.shared.model.ShadeMode
43 import com.android.systemui.util.icuMessageFormat
44 import javax.inject.Inject
45 import javax.inject.Named
46 import javax.inject.Provider
47 import kotlin.math.max
48 import kotlinx.coroutines.CoroutineStart
49 import kotlinx.coroutines.awaitCancellation
50 import kotlinx.coroutines.flow.Flow
51 import kotlinx.coroutines.flow.MutableStateFlow
52 import kotlinx.coroutines.flow.StateFlow
53 import kotlinx.coroutines.flow.asStateFlow
54 import kotlinx.coroutines.flow.combine
55 import kotlinx.coroutines.flow.distinctUntilChanged
56 import kotlinx.coroutines.flow.flowOf
57 import kotlinx.coroutines.flow.map
58 import kotlinx.coroutines.isActive
59 
60 private const val TAG = "FooterActionsViewModel"
61 
62 /** A ViewModel for the footer actions. */
63 class FooterActionsViewModel(
64     /** The model for the security button. */
65     val security: Flow<FooterActionsSecurityButtonViewModel?>,
66 
67     /** The model for the foreground services button. */
68     val foregroundServices: Flow<FooterActionsForegroundServicesButtonViewModel?>,
69 
70     /** The model for the user switcher button. */
71     val userSwitcher: Flow<FooterActionsButtonViewModel?>,
72 
73     /** The model for the settings button. */
74     val settings: FooterActionsButtonViewModel,
75 
76     /** The model for the power button. */
77     val power: Flow<FooterActionsButtonViewModel?>,
78     val initialPower: () -> FooterActionsButtonViewModel?,
79 
80     /**
81      * Observe the device monitoring dialog requests and show the dialog accordingly. This function
82      * will suspend indefinitely and will need to be cancelled to stop observing.
83      *
84      * Important: [quickSettingsContext] must be the [Context] associated to the
85      * [Quick Settings fragment][com.android.systemui.qs.QSFragmentLegacy], and the call to this
86      * function must be cancelled when that fragment is destroyed.
87      */
88     val observeDeviceMonitoringDialogRequests: suspend (quickSettingsContext: Context) -> Unit,
89 ) {
90     /** The alpha the UI rendering this ViewModel should have. */
91     private val _alpha = MutableStateFlow(1f)
92     val alpha: StateFlow<Float> = _alpha.asStateFlow()
93 
94     /** The alpha the background of the UI rendering this ViewModel should have. */
95     private val _backgroundAlpha = MutableStateFlow(1f)
96     val backgroundAlpha: StateFlow<Float> = _backgroundAlpha.asStateFlow()
97 
98     /** Called when the expansion of the Quick Settings changed. */
99     fun onQuickSettingsExpansionChanged(expansion: Float, isInSplitShade: Boolean) {
100         if (isInSplitShade) {
101             // In split shade, we want to fade in the background when the QS background starts to
102             // show.
103             val delay = 0.15f
104             _alpha.value = expansion
105             _backgroundAlpha.value = max(0f, expansion - delay) / (1f - delay)
106         } else {
107             // Only start fading in the footer actions when we are at least 90% expanded.
108             val delay = 0.9f
109             _alpha.value = max(0f, expansion - delay) / (1 - delay)
110             _backgroundAlpha.value = 1f
111         }
112     }
113 
114     @SysUISingleton
115     class Factory
116     @Inject
117     constructor(
118         @ShadeDisplayAware private val context: Context,
119         private val falsingManager: FalsingManager,
120         private val footerActionsInteractor: FooterActionsInteractor,
121         private val shadeModeInteractor: ShadeModeInteractor,
122         private val globalActionsDialogLiteProvider: Provider<GlobalActionsDialogLite>,
123         private val activityStarter: ActivityStarter,
124         @Named(PM_LITE_ENABLED) private val showPowerButton: Boolean,
125     ) {
126         /** Create a [FooterActionsViewModel] bound to the lifecycle of [lifecycleOwner]. */
127         fun create(lifecycleOwner: LifecycleOwner): FooterActionsViewModel {
128             val globalActionsDialogLite = globalActionsDialogLiteProvider.get()
129             if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
130                 // This should usually not happen, but let's make sure we already destroy
131                 // globalActionsDialogLite.
132                 globalActionsDialogLite.destroy()
133             } else {
134                 // Destroy globalActionsDialogLite when the lifecycle is destroyed.
135                 lifecycleOwner.lifecycle.addObserver(
136                     object : DefaultLifecycleObserver {
137                         override fun onDestroy(owner: LifecycleOwner) {
138                             globalActionsDialogLite.destroy()
139                         }
140                     }
141                 )
142             }
143 
144             return createFooterActionsViewModel(
145                 context,
146                 footerActionsInteractor,
147                 shadeModeInteractor.shadeMode,
148                 falsingManager,
149                 globalActionsDialogLite,
150                 activityStarter,
151                 showPowerButton,
152             )
153         }
154 
155         fun create(lifecycleCoroutineScope: LifecycleCoroutineScope): FooterActionsViewModel {
156             val globalActionsDialogLite = globalActionsDialogLiteProvider.get()
157             if (lifecycleCoroutineScope.isActive) {
158                 lifecycleCoroutineScope.launch(start = CoroutineStart.ATOMIC) {
159                     try {
160                         awaitCancellation()
161                     } finally {
162                         globalActionsDialogLite.destroy()
163                     }
164                 }
165             } else {
166                 globalActionsDialogLite.destroy()
167             }
168 
169             return createFooterActionsViewModel(
170                 context,
171                 footerActionsInteractor,
172                 shadeModeInteractor.shadeMode,
173                 falsingManager,
174                 globalActionsDialogLite,
175                 activityStarter,
176                 showPowerButton,
177             )
178         }
179     }
180 }
181 
createFooterActionsViewModelnull182 fun createFooterActionsViewModel(
183     @ShadeDisplayAware appContext: Context,
184     footerActionsInteractor: FooterActionsInteractor,
185     shadeMode: StateFlow<ShadeMode>,
186     falsingManager: FalsingManager,
187     globalActionsDialogLite: GlobalActionsDialogLite,
188     activityStarter: ActivityStarter,
189     showPowerButton: Boolean,
190 ): FooterActionsViewModel {
191     suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) {
192         footerActionsInteractor.deviceMonitoringDialogRequests.collect {
193             footerActionsInteractor.showDeviceMonitoringDialog(
194                 quickSettingsContext,
195                 expandable = null,
196             )
197         }
198     }
199 
200     fun onSecurityButtonClicked(quickSettingsContext: Context, expandable: Expandable) {
201         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
202             return
203         }
204 
205         footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext, expandable)
206     }
207 
208     fun onForegroundServiceButtonClicked(expandable: Expandable) {
209         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
210             return
211         }
212 
213         activityStarter.dismissKeyguardThenExecute(
214             {
215                 footerActionsInteractor.showForegroundServicesDialog(expandable)
216                 false /* if the dismiss should be deferred */
217             },
218             null /* cancelAction */,
219             true, /* afterKeyguardGone */
220         )
221     }
222 
223     fun onUserSwitcherClicked(expandable: Expandable) {
224         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
225             return
226         }
227 
228         footerActionsInteractor.showUserSwitcher(expandable)
229     }
230 
231     fun onSettingsButtonClicked(expandable: Expandable) {
232         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
233             return
234         }
235 
236         footerActionsInteractor.showSettings(expandable)
237     }
238 
239     fun onPowerButtonClicked(expandable: Expandable) {
240         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
241             return
242         }
243 
244         footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, expandable)
245     }
246 
247     val qsThemedContext = ContextThemeWrapper(appContext, R.style.Theme_SystemUI_QuickSettings)
248 
249     val security =
250         footerActionsInteractor.securityButtonConfig
251             .map { config ->
252                 config?.let { securityButtonViewModel(it, ::onSecurityButtonClicked) }
253             }
254             .distinctUntilChanged()
255 
256     val foregroundServices =
257         combine(
258                 footerActionsInteractor.foregroundServicesCount,
259                 footerActionsInteractor.hasNewForegroundServices,
260                 security,
261             ) { foregroundServicesCount, hasNewChanges, securityModel ->
262                 if (foregroundServicesCount <= 0) {
263                     return@combine null
264                 }
265 
266                 foregroundServicesButtonViewModel(
267                     qsThemedContext,
268                     foregroundServicesCount,
269                     securityModel,
270                     hasNewChanges,
271                     ::onForegroundServiceButtonClicked,
272                 )
273             }
274             .distinctUntilChanged()
275 
276     val userSwitcher =
277         userSwitcherViewModel(qsThemedContext, footerActionsInteractor, ::onUserSwitcherClicked)
278 
279     val settings = settingsButtonViewModel(qsThemedContext, ::onSettingsButtonClicked)
280 
281     val power =
282         if (showPowerButton) {
283             powerButtonViewModel(qsThemedContext, ::onPowerButtonClicked, shadeMode)
284         } else {
285             flowOf(null)
286         }
287 
288     return FooterActionsViewModel(
289         security = security,
290         foregroundServices = foregroundServices,
291         userSwitcher = userSwitcher,
292         settings = settings,
293         power = power,
294         observeDeviceMonitoringDialogRequests = ::observeDeviceMonitoringDialogRequests,
295         initialPower =
296             if (showPowerButton) {
297                 { powerButtonViewModel(qsThemedContext, ::onPowerButtonClicked, shadeMode.value) }
298             } else {
299                 { null }
300             },
301     )
302 }
303 
userSwitcherViewModelnull304 fun userSwitcherViewModel(
305     themedContext: Context,
306     footerActionsInteractor: FooterActionsInteractor,
307     onUserSwitcherClicked: (Expandable) -> Unit,
308 ): Flow<FooterActionsButtonViewModel?> {
309     return footerActionsInteractor.userSwitcherStatus
310         .map { userSwitcherStatus ->
311             when (userSwitcherStatus) {
312                 UserSwitcherStatusModel.Disabled -> null
313                 is UserSwitcherStatusModel.Enabled -> {
314                     if (userSwitcherStatus.currentUserImage == null) {
315                         Log.e(
316                             TAG,
317                             "Skipped the addition of user switcher button because " +
318                                 "currentUserImage is missing",
319                         )
320                         return@map null
321                     }
322 
323                     userSwitcherButtonViewModel(
324                         themedContext,
325                         userSwitcherStatus,
326                         onUserSwitcherClicked,
327                     )
328                 }
329             }
330         }
331         .distinctUntilChanged()
332 }
333 
securityButtonViewModelnull334 fun securityButtonViewModel(
335     config: SecurityButtonConfig,
336     onSecurityButtonClicked: (Context, Expandable) -> Unit,
337 ): FooterActionsSecurityButtonViewModel {
338     val (icon, text, isClickable) = config
339     return FooterActionsSecurityButtonViewModel(
340         icon,
341         text,
342         if (isClickable) onSecurityButtonClicked else null,
343     )
344 }
345 
foregroundServicesButtonViewModelnull346 fun foregroundServicesButtonViewModel(
347     qsThemedContext: Context,
348     foregroundServicesCount: Int,
349     securityModel: FooterActionsSecurityButtonViewModel?,
350     hasNewChanges: Boolean,
351     onForegroundServiceButtonClicked: (Expandable) -> Unit,
352 ): FooterActionsForegroundServicesButtonViewModel {
353     val text =
354         icuMessageFormat(
355             qsThemedContext.resources,
356             R.string.fgs_manager_footer_label,
357             foregroundServicesCount,
358         )
359 
360     return FooterActionsForegroundServicesButtonViewModel(
361         foregroundServicesCount,
362         text = text,
363         displayText = securityModel == null,
364         hasNewChanges = hasNewChanges,
365         onForegroundServiceButtonClicked,
366     )
367 }
368 
userSwitcherButtonViewModelnull369 fun userSwitcherButtonViewModel(
370     qsThemedContext: Context,
371     status: UserSwitcherStatusModel.Enabled,
372     onUserSwitcherClicked: (Expandable) -> Unit,
373 ): FooterActionsButtonViewModel {
374     val icon = status.currentUserImage!!
375     return FooterActionsButtonViewModel(
376         id = R.id.multi_user_switch,
377         icon =
378             Icon.Loaded(
379                 icon,
380                 ContentDescription.Loaded(
381                     userSwitcherContentDescription(qsThemedContext, status.currentUserName)
382                 ),
383             ),
384         iconTint = null,
385         backgroundColor = R.attr.shadeInactive,
386         onClick = onUserSwitcherClicked,
387     )
388 }
389 
userSwitcherContentDescriptionnull390 private fun userSwitcherContentDescription(
391     qsThemedContext: Context,
392     currentUser: String?,
393 ): String? {
394     return currentUser?.let { user ->
395         qsThemedContext.getString(R.string.accessibility_quick_settings_user, user)
396     }
397 }
398 
settingsButtonViewModelnull399 fun settingsButtonViewModel(
400     qsThemedContext: Context,
401     onSettingsButtonClicked: (Expandable) -> Unit,
402 ): FooterActionsButtonViewModel {
403     return FooterActionsButtonViewModel(
404         id = R.id.settings_button_container,
405         Icon.Resource(
406             R.drawable.ic_settings,
407             ContentDescription.Resource(R.string.accessibility_quick_settings_settings),
408         ),
409         iconTint = Utils.getColorAttrDefaultColor(qsThemedContext, R.attr.onShadeInactiveVariant),
410         backgroundColor = R.attr.shadeInactive,
411         onSettingsButtonClicked,
412     )
413 }
414 
powerButtonViewModelnull415 fun powerButtonViewModel(
416     qsThemedContext: Context,
417     onPowerButtonClicked: (Expandable) -> Unit,
418     shadeMode: Flow<ShadeMode>,
419 ): Flow<FooterActionsButtonViewModel?> {
420     return shadeMode.map { mode ->
421         powerButtonViewModel(qsThemedContext, onPowerButtonClicked, mode)
422     }
423 }
424 
powerButtonViewModelnull425 fun powerButtonViewModel(
426     qsThemedContext: Context,
427     onPowerButtonClicked: (Expandable) -> Unit,
428     shadeMode: ShadeMode,
429 ): FooterActionsButtonViewModel {
430     val isDualShade = shadeMode is ShadeMode.Dual
431     return FooterActionsButtonViewModel(
432         id = R.id.pm_lite,
433         Icon.Resource(
434             android.R.drawable.ic_lock_power_off,
435             ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu),
436         ),
437         iconTint =
438             Utils.getColorAttrDefaultColor(
439                 qsThemedContext,
440                 if (isDualShade) R.attr.onShadeInactiveVariant else R.attr.onShadeActive,
441             ),
442         backgroundColor = if (isDualShade) R.attr.shadeInactive else R.attr.shadeActive,
443         onPowerButtonClicked,
444     )
445 }
446