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