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.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(true) 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 = null, 149 backgroundColor = R.attr.offStateColor, 150 this::onSettingsButtonClicked, 151 ) 152 153 /** The model for the power button. */ 154 val power: FooterActionsButtonViewModel? = 155 if (showPowerButton) { 156 FooterActionsButtonViewModel( 157 id = R.id.pm_lite, 158 Icon.Resource( 159 android.R.drawable.ic_lock_power_off, 160 ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu) 161 ), 162 iconTint = 163 Utils.getColorAttrDefaultColor( 164 context, 165 com.android.internal.R.attr.textColorOnAccent, 166 ), 167 backgroundColor = com.android.internal.R.attr.colorAccent, 168 this::onPowerButtonClicked, 169 ) 170 } else { 171 null 172 } 173 174 /** Called when the visibility of the UI rendering this model should be changed. */ 175 fun onVisibilityChangeRequested(visible: Boolean) { 176 _isVisible.value = visible 177 } 178 179 /** Called when the expansion of the Quick Settings changed. */ 180 fun onQuickSettingsExpansionChanged(expansion: Float, isInSplitShade: Boolean) { 181 if (isInSplitShade) { 182 // In split shade, we want to fade in the background only at the very end (see 183 // b/240563302). 184 val delay = 0.99f 185 _alpha.value = expansion 186 _backgroundAlpha.value = max(0f, expansion - delay) / (1f - delay) 187 } else { 188 // Only start fading in the footer actions when we are at least 90% expanded. 189 val delay = 0.9f 190 _alpha.value = max(0f, expansion - delay) / (1 - delay) 191 _backgroundAlpha.value = 1f 192 } 193 } 194 195 /** 196 * Observe the device monitoring dialog requests and show the dialog accordingly. This function 197 * will suspend indefinitely and will need to be cancelled to stop observing. 198 * 199 * Important: [quickSettingsContext] must be the [Context] associated to the 200 * [Quick Settings fragment][com.android.systemui.qs.QSFragment], and the call to this function 201 * must be cancelled when that fragment is destroyed. 202 */ 203 suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) { 204 footerActionsInteractor.deviceMonitoringDialogRequests.collect { 205 footerActionsInteractor.showDeviceMonitoringDialog( 206 quickSettingsContext, 207 expandable = null, 208 ) 209 } 210 } 211 212 private fun onSecurityButtonClicked(quickSettingsContext: Context, expandable: Expandable) { 213 if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 214 return 215 } 216 217 footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext, expandable) 218 } 219 220 private fun onForegroundServiceButtonClicked(expandable: Expandable) { 221 if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 222 return 223 } 224 225 footerActionsInteractor.showForegroundServicesDialog(expandable) 226 } 227 228 private fun onUserSwitcherClicked(expandable: Expandable) { 229 if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 230 return 231 } 232 233 footerActionsInteractor.showUserSwitcher(expandable) 234 } 235 236 private fun onSettingsButtonClicked(expandable: Expandable) { 237 if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 238 return 239 } 240 241 footerActionsInteractor.showSettings(expandable) 242 } 243 244 private fun onPowerButtonClicked(expandable: Expandable) { 245 if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 246 return 247 } 248 249 footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, expandable) 250 } 251 252 private fun userSwitcherButton( 253 status: UserSwitcherStatusModel.Enabled 254 ): FooterActionsButtonViewModel { 255 val icon = status.currentUserImage!! 256 257 return FooterActionsButtonViewModel( 258 id = R.id.multi_user_switch, 259 icon = 260 Icon.Loaded( 261 icon, 262 ContentDescription.Loaded( 263 userSwitcherContentDescription(status.currentUserName) 264 ), 265 ), 266 iconTint = null, 267 backgroundColor = R.attr.offStateColor, 268 onClick = this::onUserSwitcherClicked, 269 ) 270 } 271 272 private fun userSwitcherContentDescription(currentUser: String?): String? { 273 return currentUser?.let { user -> 274 context.getString(R.string.accessibility_quick_settings_user, user) 275 } 276 } 277 278 @SysUISingleton 279 class Factory 280 @Inject 281 constructor( 282 @Application private val context: Context, 283 private val falsingManager: FalsingManager, 284 private val footerActionsInteractor: FooterActionsInteractor, 285 private val globalActionsDialogLiteProvider: Provider<GlobalActionsDialogLite>, 286 @Named(PM_LITE_ENABLED) private val showPowerButton: Boolean, 287 ) { 288 /** Create a [FooterActionsViewModel] bound to the lifecycle of [lifecycleOwner]. */ 289 fun create(lifecycleOwner: LifecycleOwner): FooterActionsViewModel { 290 val globalActionsDialogLite = globalActionsDialogLiteProvider.get() 291 if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) { 292 // This should usually not happen, but let's make sure we already destroy 293 // globalActionsDialogLite. 294 globalActionsDialogLite.destroy() 295 } else { 296 // Destroy globalActionsDialogLite when the lifecycle is destroyed. 297 lifecycleOwner.lifecycle.addObserver( 298 object : DefaultLifecycleObserver { 299 override fun onDestroy(owner: LifecycleOwner) { 300 globalActionsDialogLite.destroy() 301 } 302 } 303 ) 304 } 305 306 return FooterActionsViewModel( 307 context, 308 footerActionsInteractor, 309 falsingManager, 310 globalActionsDialogLite, 311 showPowerButton, 312 ) 313 } 314 } 315 316 companion object { 317 private const val TAG = "FooterActionsViewModel" 318 } 319 } 320