blob: 8afe6f216cbb459d81c3db80ba0cf5b063d6de49 [file] [log] [blame]
/*
* 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"
}
}