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