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