| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.qs.footer.ui.viewmodel |
| |
| import android.content.Context |
| import android.util.Log |
| import android.view.View |
| import androidx.lifecycle.DefaultLifecycleObserver |
| import androidx.lifecycle.Lifecycle |
| import androidx.lifecycle.LifecycleOwner |
| import com.android.settingslib.Utils |
| import com.android.settingslib.drawable.UserIconDrawable |
| import com.android.systemui.R |
| import com.android.systemui.animation.Expandable |
| import com.android.systemui.common.shared.model.ContentDescription |
| import com.android.systemui.common.shared.model.Icon |
| import com.android.systemui.dagger.SysUISingleton |
| import com.android.systemui.dagger.qualifiers.Application |
| import com.android.systemui.globalactions.GlobalActionsDialogLite |
| import com.android.systemui.plugins.FalsingManager |
| import com.android.systemui.qs.dagger.QSFlagsModule.PM_LITE_ENABLED |
| import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel |
| import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor |
| import com.android.systemui.util.icuMessageFormat |
| import javax.inject.Inject |
| import javax.inject.Named |
| import javax.inject.Provider |
| import kotlin.math.max |
| import kotlinx.coroutines.flow.Flow |
| import kotlinx.coroutines.flow.MutableStateFlow |
| import kotlinx.coroutines.flow.StateFlow |
| import kotlinx.coroutines.flow.asStateFlow |
| import kotlinx.coroutines.flow.collect |
| import kotlinx.coroutines.flow.combine |
| import kotlinx.coroutines.flow.map |
| |
| /** A ViewModel for the footer actions. */ |
| class FooterActionsViewModel( |
| @Application private val context: Context, |
| private val footerActionsInteractor: FooterActionsInteractor, |
| private val falsingManager: FalsingManager, |
| private val globalActionsDialogLite: GlobalActionsDialogLite, |
| showPowerButton: Boolean, |
| ) { |
| /** |
| * Whether the UI rendering this ViewModel should be visible. Note that even when this is false, |
| * the UI should still participate to the layout it is included in (i.e. in the View world it |
| * should be INVISIBLE, not GONE). |
| */ |
| private val _isVisible = MutableStateFlow(true) |
| val isVisible: StateFlow<Boolean> = _isVisible.asStateFlow() |
| |
| /** The alpha the UI rendering this ViewModel should have. */ |
| private val _alpha = MutableStateFlow(1f) |
| val alpha: StateFlow<Float> = _alpha.asStateFlow() |
| |
| /** The alpha the background of the UI rendering this ViewModel should have. */ |
| private val _backgroundAlpha = MutableStateFlow(1f) |
| val backgroundAlpha: StateFlow<Float> = _backgroundAlpha.asStateFlow() |
| |
| /** The model for the security button. */ |
| val security: Flow<FooterActionsSecurityButtonViewModel?> = |
| footerActionsInteractor.securityButtonConfig.map { config -> |
| val (icon, text, isClickable) = config ?: return@map null |
| FooterActionsSecurityButtonViewModel( |
| icon, |
| text, |
| if (isClickable) this::onSecurityButtonClicked else null, |
| ) |
| } |
| |
| /** The model for the foreground services button. */ |
| val foregroundServices: Flow<FooterActionsForegroundServicesButtonViewModel?> = |
| combine( |
| footerActionsInteractor.foregroundServicesCount, |
| footerActionsInteractor.hasNewForegroundServices, |
| security, |
| ) { foregroundServicesCount, hasNewChanges, securityModel -> |
| if (foregroundServicesCount <= 0) { |
| return@combine null |
| } |
| |
| val text = |
| icuMessageFormat( |
| context.resources, |
| R.string.fgs_manager_footer_label, |
| foregroundServicesCount, |
| ) |
| FooterActionsForegroundServicesButtonViewModel( |
| foregroundServicesCount, |
| text = text, |
| displayText = securityModel == null, |
| hasNewChanges = hasNewChanges, |
| this::onForegroundServiceButtonClicked, |
| ) |
| } |
| |
| /** The model for the user switcher button. */ |
| val userSwitcher: Flow<FooterActionsButtonViewModel?> = |
| footerActionsInteractor.userSwitcherStatus.map { userSwitcherStatus -> |
| when (userSwitcherStatus) { |
| UserSwitcherStatusModel.Disabled -> null |
| is UserSwitcherStatusModel.Enabled -> { |
| if (userSwitcherStatus.currentUserImage == null) { |
| Log.e( |
| TAG, |
| "Skipped the addition of user switcher button because " + |
| "currentUserImage is missing", |
| ) |
| return@map null |
| } |
| |
| userSwitcherButton(userSwitcherStatus) |
| } |
| } |
| } |
| |
| /** The model for the settings button. */ |
| val settings: FooterActionsButtonViewModel = |
| FooterActionsButtonViewModel( |
| Icon.Resource(R.drawable.ic_settings), |
| iconTint = null, |
| R.drawable.qs_footer_action_circle, |
| ContentDescription.Resource(R.string.accessibility_quick_settings_settings), |
| this::onSettingsButtonClicked, |
| ) |
| |
| /** The model for the power button. */ |
| val power: FooterActionsButtonViewModel? = |
| if (showPowerButton) { |
| FooterActionsButtonViewModel( |
| Icon.Resource(android.R.drawable.ic_lock_power_off), |
| iconTint = |
| Utils.getColorAttrDefaultColor( |
| context, |
| com.android.internal.R.attr.textColorOnAccent, |
| ), |
| R.drawable.qs_footer_action_circle_color, |
| ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu), |
| this::onPowerButtonClicked, |
| ) |
| } else { |
| null |
| } |
| |
| /** Called when the visibility of the UI rendering this model should be changed. */ |
| fun onVisibilityChangeRequested(visible: Boolean) { |
| _isVisible.value = visible |
| } |
| |
| /** Called when the expansion of the Quick Settings changed. */ |
| fun onQuickSettingsExpansionChanged(expansion: Float, isInSplitShade: Boolean) { |
| if (isInSplitShade) { |
| // In split shade, we want to fade in the background only at the very end (see |
| // b/240563302). |
| val delay = 0.99f |
| _alpha.value = expansion |
| _backgroundAlpha.value = max(0f, expansion - delay) / (1f - delay) |
| } else { |
| // Only start fading in the footer actions when we are at least 90% expanded. |
| val delay = 0.9f |
| _alpha.value = max(0f, expansion - delay) / (1 - delay) |
| _backgroundAlpha.value = 1f |
| } |
| } |
| |
| /** |
| * Observe the device monitoring dialog requests and show the dialog accordingly. This function |
| * will suspend indefinitely and will need to be cancelled to stop observing. |
| * |
| * Important: [quickSettingsContext] must be the [Context] associated to the [Quick Settings |
| * fragment][com.android.systemui.qs.QSFragment], and the call to this function must be |
| * cancelled when that fragment is destroyed. |
| */ |
| suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) { |
| footerActionsInteractor.deviceMonitoringDialogRequests.collect { |
| footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext) |
| } |
| } |
| |
| private fun onSecurityButtonClicked(view: View) { |
| if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { |
| return |
| } |
| |
| footerActionsInteractor.showDeviceMonitoringDialog(view) |
| } |
| |
| private fun onForegroundServiceButtonClicked(view: View) { |
| if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { |
| return |
| } |
| |
| footerActionsInteractor.showForegroundServicesDialog(view) |
| } |
| |
| private fun onUserSwitcherClicked(view: View) { |
| if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { |
| return |
| } |
| |
| footerActionsInteractor.showUserSwitcher(view) |
| } |
| |
| // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog |
| // or activity. |
| private fun onSettingsButtonClicked(view: View) { |
| if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { |
| return |
| } |
| |
| footerActionsInteractor.showSettings(Expandable.fromView(view)) |
| } |
| |
| private fun onPowerButtonClicked(view: View) { |
| if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { |
| return |
| } |
| |
| footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, view) |
| } |
| |
| private fun userSwitcherButton( |
| status: UserSwitcherStatusModel.Enabled |
| ): FooterActionsButtonViewModel { |
| val icon = status.currentUserImage!! |
| val iconTint = |
| if (status.isGuestUser && icon !is UserIconDrawable) { |
| Utils.getColorAttrDefaultColor(context, android.R.attr.colorForeground) |
| } else { |
| null |
| } |
| |
| return FooterActionsButtonViewModel( |
| Icon.Loaded(icon), |
| iconTint, |
| R.drawable.qs_footer_action_circle, |
| ContentDescription.Loaded(userSwitcherContentDescription(status.currentUserName)), |
| this::onUserSwitcherClicked, |
| ) |
| } |
| |
| private fun userSwitcherContentDescription(currentUser: String?): String? { |
| return currentUser?.let { user -> |
| context.getString(R.string.accessibility_quick_settings_user, user) |
| } |
| } |
| |
| @SysUISingleton |
| class Factory |
| @Inject |
| constructor( |
| @Application private val context: Context, |
| private val falsingManager: FalsingManager, |
| private val footerActionsInteractor: FooterActionsInteractor, |
| private val globalActionsDialogLiteProvider: Provider<GlobalActionsDialogLite>, |
| @Named(PM_LITE_ENABLED) private val showPowerButton: Boolean, |
| ) { |
| /** Create a [FooterActionsViewModel] bound to the lifecycle of [lifecycleOwner]. */ |
| fun create(lifecycleOwner: LifecycleOwner): FooterActionsViewModel { |
| val globalActionsDialogLite = globalActionsDialogLiteProvider.get() |
| if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) { |
| // This should usually not happen, but let's make sure we already destroy |
| // globalActionsDialogLite. |
| globalActionsDialogLite.destroy() |
| } else { |
| // Destroy globalActionsDialogLite when the lifecycle is destroyed. |
| lifecycleOwner.lifecycle.addObserver( |
| object : DefaultLifecycleObserver { |
| override fun onDestroy(owner: LifecycleOwner) { |
| globalActionsDialogLite.destroy() |
| } |
| } |
| ) |
| } |
| |
| return FooterActionsViewModel( |
| context, |
| footerActionsInteractor, |
| falsingManager, |
| globalActionsDialogLite, |
| showPowerButton, |
| ) |
| } |
| } |
| |
| companion object { |
| private const val TAG = "FooterActionsViewModel" |
| } |
| } |