Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 01b97d27 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Remove the dependency on Mockito to create QS actions fakes (1/2)

This CL refactors FooterActionsViewModel so that it can be created
easily with fake values in the Gallery app and screenshot tests. That
way, we don't need to depend on Mockito to create fake dependencies of
the ViewModel, which is necessary for deviceless RNG tests (and better
architecture) and Compose @Previews.

This is a pure refactoring that doesn't change any logic in the code.

Bug: 304719047
Test: atest FooterActionsScreenshotTest
Change-Id: If5841b32b84ff3f7ac8d9d1d42449da481dc9b55
parent f3a383c6
Loading
Loading
Loading
Loading
+244 −180
Original line number Diff line number Diff line
@@ -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
    val qsThemedContext = ContextThemeWrapper(appContext, R.style.Theme_SystemUI_QuickSettings)

    val security =
        footerActionsInteractor.securityButtonConfig
            .map { config ->
                config?.let { securityButtonViewModel(it, ::onSecurityButtonClicked) }
            }
            .distinctUntilChanged()

    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()

    val settings = settingsButtonViewModel(qsThemedContext, ::onSettingsButtonClicked)
    val power =
        if (showPowerButton) {
            powerButtonViewModel(qsThemedContext, ::onPowerButtonClicked)
        } else {
            null
        }

    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(status.currentUserName)
                    userSwitcherContentDescription(qsThemedContext, status.currentUserName)
                ),
            ),
        iconTint = null,
        backgroundColor = R.attr.shadeInactive,
            onClick = this::onUserSwitcherClicked,
        onClick = onUserSwitcherClicked,
    )
}

    private fun userSwitcherContentDescription(currentUser: String?): String? {
private fun userSwitcherContentDescription(
    qsThemedContext: Context,
    currentUser: String?
): String? {
    return currentUser?.let { user ->
            context.getString(R.string.accessibility_quick_settings_user, user)
        qsThemedContext.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()
                        }
                    }
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,
    )
}

            return FooterActionsViewModel(
                context,
                footerActionsInteractor,
                falsingManager,
                globalActionsDialogLite,
                showPowerButton,
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,
    )
}
    }

    companion object {
        private const val TAG = "FooterActionsViewModel"
    }
}
+2 −4
Original line number Diff line number Diff line
@@ -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)