diff options
2 files changed, 246 insertions, 184 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt index 64fa33ce8118..eff3e76f43be 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt @@ -33,6 +33,7 @@ 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.qs.footer.domain.model.SecurityButtonConfig import com.android.systemui.res.R import com.android.systemui.util.icuMessageFormat import javax.inject.Inject @@ -47,17 +48,35 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map +private const val TAG = "FooterActionsViewModel" + /** A ViewModel for the footer actions. */ class FooterActionsViewModel( - @Application appContext: Context, - private val footerActionsInteractor: FooterActionsInteractor, - private val falsingManager: FalsingManager, - private val globalActionsDialogLite: GlobalActionsDialogLite, - showPowerButton: Boolean, -) { - /** The context themed with the Quick Settings colors. */ - private val context = ContextThemeWrapper(appContext, R.style.Theme_SystemUI_QuickSettings) + /** The model for the security button. */ + val security: Flow<FooterActionsSecurityButtonViewModel?>, + + /** The model for the foreground services button. */ + val foregroundServices: Flow<FooterActionsForegroundServicesButtonViewModel?>, + /** The model for the user switcher button. */ + val userSwitcher: Flow<FooterActionsButtonViewModel?>, + + /** The model for the settings button. */ + val settings: FooterActionsButtonViewModel, + + /** The model for the power button. */ + val power: FooterActionsButtonViewModel?, + + /** + * 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.QSFragmentLegacy], and the call to this + * function must be cancelled when that fragment is destroyed. + */ + val observeDeviceMonitoringDialogRequests: suspend (quickSettingsContext: Context) -> Unit, +) { /** * 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 @@ -74,107 +93,6 @@ class FooterActionsViewModel( 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, - ) - } - .distinctUntilChanged() - - /** 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, - ) - } - .distinctUntilChanged() - - /** 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) - } - } - } - .distinctUntilChanged() - - /** The model for the settings button. */ - val settings: FooterActionsButtonViewModel = - FooterActionsButtonViewModel( - id = R.id.settings_button_container, - Icon.Resource( - R.drawable.ic_settings, - ContentDescription.Resource(R.string.accessibility_quick_settings_settings) - ), - iconTint = - Utils.getColorAttrDefaultColor( - context, - R.attr.onShadeInactiveVariant, - ), - backgroundColor = R.attr.shadeInactive, - this::onSettingsButtonClicked, - ) - - /** The model for the power button. */ - val power: FooterActionsButtonViewModel? = - if (showPowerButton) { - FooterActionsButtonViewModel( - id = R.id.pm_lite, - Icon.Resource( - android.R.drawable.ic_lock_power_off, - ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu) - ), - iconTint = - Utils.getColorAttrDefaultColor( - context, - R.attr.onShadeActive, - ), - backgroundColor = R.attr.shadeActive, - this::onPowerButtonClicked, - ) - } else { - null - } - - /** Called when the visibility of the UI rendering this model should be changed. */ fun onVisibilityChangeRequested(visible: Boolean) { _isVisible.value = visible } @@ -195,14 +113,52 @@ class FooterActionsViewModel( } } - /** - * 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.QSFragmentLegacy], and the call to this - * function must be cancelled when that fragment is destroyed. - */ + @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, + ) + } + } +} + +fun FooterActionsViewModel( + @Application appContext: Context, + footerActionsInteractor: FooterActionsInteractor, + falsingManager: FalsingManager, + globalActionsDialogLite: GlobalActionsDialogLite, + showPowerButton: Boolean, +): FooterActionsViewModel { suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) { footerActionsInteractor.deviceMonitoringDialogRequests.collect { footerActionsInteractor.showDeviceMonitoringDialog( @@ -212,7 +168,7 @@ class FooterActionsViewModel( } } - private fun onSecurityButtonClicked(quickSettingsContext: Context, expandable: Expandable) { + fun onSecurityButtonClicked(quickSettingsContext: Context, expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } @@ -220,7 +176,7 @@ class FooterActionsViewModel( footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext, expandable) } - private fun onForegroundServiceButtonClicked(expandable: Expandable) { + fun onForegroundServiceButtonClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } @@ -228,7 +184,7 @@ class FooterActionsViewModel( footerActionsInteractor.showForegroundServicesDialog(expandable) } - private fun onUserSwitcherClicked(expandable: Expandable) { + fun onUserSwitcherClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } @@ -236,7 +192,7 @@ class FooterActionsViewModel( footerActionsInteractor.showUserSwitcher(expandable) } - private fun onSettingsButtonClicked(expandable: Expandable) { + fun onSettingsButtonClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } @@ -244,7 +200,7 @@ class FooterActionsViewModel( footerActionsInteractor.showSettings(expandable) } - private fun onPowerButtonClicked(expandable: Expandable) { + fun onPowerButtonClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } @@ -252,71 +208,179 @@ class FooterActionsViewModel( footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, expandable) } - private fun userSwitcherButton( - status: UserSwitcherStatusModel.Enabled - ): FooterActionsButtonViewModel { - val icon = status.currentUserImage!! - - return FooterActionsButtonViewModel( - id = R.id.multi_user_switch, - icon = - Icon.Loaded( - icon, - ContentDescription.Loaded( - userSwitcherContentDescription(status.currentUserName) - ), - ), - iconTint = null, - backgroundColor = R.attr.shadeInactive, - onClick = this::onUserSwitcherClicked, - ) - } + val qsThemedContext = ContextThemeWrapper(appContext, R.style.Theme_SystemUI_QuickSettings) - private fun userSwitcherContentDescription(currentUser: String?): String? { - return currentUser?.let { user -> - context.getString(R.string.accessibility_quick_settings_user, user) - } - } + val security = + footerActionsInteractor.securityButtonConfig + .map { config -> + config?.let { securityButtonViewModel(it, ::onSecurityButtonClicked) } + } + .distinctUntilChanged() - @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() + val foregroundServices = + combine( + footerActionsInteractor.foregroundServicesCount, + footerActionsInteractor.hasNewForegroundServices, + security, + ) { foregroundServicesCount, hasNewChanges, securityModel -> + if (foregroundServicesCount <= 0) { + return@combine null + } + + foregroundServicesButtonViewModel( + qsThemedContext, + foregroundServicesCount, + securityModel, + hasNewChanges, + ::onForegroundServiceButtonClicked, + ) + } + .distinctUntilChanged() + + val userSwitcher = + 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 } + + userSwitcherButtonViewModel( + qsThemedContext, + userSwitcherStatus, + ::onUserSwitcherClicked + ) } - ) + } } + .distinctUntilChanged() - return FooterActionsViewModel( - context, - footerActionsInteractor, - falsingManager, - globalActionsDialogLite, - showPowerButton, - ) + val settings = settingsButtonViewModel(qsThemedContext, ::onSettingsButtonClicked) + val power = + if (showPowerButton) { + powerButtonViewModel(qsThemedContext, ::onPowerButtonClicked) + } else { + null } - } - companion object { - private const val TAG = "FooterActionsViewModel" + return FooterActionsViewModel( + security = security, + foregroundServices = foregroundServices, + userSwitcher = userSwitcher, + settings = settings, + power = power, + observeDeviceMonitoringDialogRequests = ::observeDeviceMonitoringDialogRequests, + ) +} + +fun securityButtonViewModel( + config: SecurityButtonConfig, + onSecurityButtonClicked: (Context, Expandable) -> Unit, +): FooterActionsSecurityButtonViewModel { + val (icon, text, isClickable) = config + return FooterActionsSecurityButtonViewModel( + icon, + text, + if (isClickable) onSecurityButtonClicked else null, + ) +} + +fun foregroundServicesButtonViewModel( + qsThemedContext: Context, + foregroundServicesCount: Int, + securityModel: FooterActionsSecurityButtonViewModel?, + hasNewChanges: Boolean, + onForegroundServiceButtonClicked: (Expandable) -> Unit, +): FooterActionsForegroundServicesButtonViewModel { + val text = + icuMessageFormat( + qsThemedContext.resources, + R.string.fgs_manager_footer_label, + foregroundServicesCount, + ) + + return FooterActionsForegroundServicesButtonViewModel( + foregroundServicesCount, + text = text, + displayText = securityModel == null, + hasNewChanges = hasNewChanges, + onForegroundServiceButtonClicked, + ) +} + +fun userSwitcherButtonViewModel( + qsThemedContext: Context, + status: UserSwitcherStatusModel.Enabled, + onUserSwitcherClicked: (Expandable) -> Unit, +): FooterActionsButtonViewModel { + val icon = status.currentUserImage!! + return FooterActionsButtonViewModel( + id = R.id.multi_user_switch, + icon = + Icon.Loaded( + icon, + ContentDescription.Loaded( + userSwitcherContentDescription(qsThemedContext, status.currentUserName) + ), + ), + iconTint = null, + backgroundColor = R.attr.shadeInactive, + onClick = onUserSwitcherClicked, + ) +} + +private fun userSwitcherContentDescription( + qsThemedContext: Context, + currentUser: String? +): String? { + return currentUser?.let { user -> + qsThemedContext.getString(R.string.accessibility_quick_settings_user, user) } } + +fun settingsButtonViewModel( + qsThemedContext: Context, + onSettingsButtonClicked: (Expandable) -> Unit, +): FooterActionsButtonViewModel { + return FooterActionsButtonViewModel( + id = R.id.settings_button_container, + Icon.Resource( + R.drawable.ic_settings, + ContentDescription.Resource(R.string.accessibility_quick_settings_settings) + ), + iconTint = + Utils.getColorAttrDefaultColor( + qsThemedContext, + R.attr.onShadeInactiveVariant, + ), + backgroundColor = R.attr.shadeInactive, + onSettingsButtonClicked, + ) +} + +fun powerButtonViewModel( + qsThemedContext: Context, + onPowerButtonClicked: (Expandable) -> Unit, +): FooterActionsButtonViewModel { + return FooterActionsButtonViewModel( + id = R.id.pm_lite, + Icon.Resource( + android.R.drawable.ic_lock_power_off, + ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu) + ), + iconTint = + Utils.getColorAttrDefaultColor( + qsThemedContext, + R.attr.onShadeActive, + ), + backgroundColor = R.attr.shadeActive, + onPowerButtonClicked, + ) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt index 4ada44c40f49..8e8a0c81e73d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt @@ -25,7 +25,6 @@ import android.view.ContextThemeWrapper import androidx.test.filters.SmallTest import com.android.settingslib.Utils import com.android.settingslib.drawable.UserIconDrawable -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.shared.model.ContentDescription @@ -35,6 +34,7 @@ import com.android.systemui.qs.FakeFgsManagerController import com.android.systemui.qs.QSSecurityFooterUtils import com.android.systemui.qs.footer.FooterActionsTestUtils import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig +import com.android.systemui.res.R import com.android.systemui.security.data.model.SecurityModel import com.android.systemui.settings.FakeUserTracker import com.android.systemui.statusbar.policy.FakeSecurityController @@ -372,9 +372,7 @@ class FooterActionsViewModelTest : SysuiTestCase() { ), ) - val job = launch { - underTest.observeDeviceMonitoringDialogRequests(quickSettingsContext = mock()) - } + val job = launch { underTest.observeDeviceMonitoringDialogRequests(mock()) } advanceUntilIdle() assertThat(nDialogRequests).isEqualTo(3) |