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