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

Commit da44fb35 authored by Ioana Alexandru's avatar Ioana Alexandru
Browse files

Launch modes settings from empty shade view

When modes are blocking notifications in the shade, launch the
corresponding modes setting when pressing on the text. If only one mode
is blocking notifications, launch that specific mode's settings page. If
multiple modes are blocking notifications, launch the general Modes
settings page.

In order to do this nicely, I am replacing
NotificationActivityStarter#startHistoryIntent with a more generic
NotificationActivityStarter#startSettingsIntent. Also using the same
approach for the FooterView for consistency, as this would allow us to
also launch other things from there in the future. Another benefit is
also that it allows us to test this logic (since it's in the viewModel
instead of the binder now).

Bug: 366003631
Test: manually tested that the intents are launched correctly (and the
back stack is correct) + viewmodel unit tests
Flag: android.app.modes_ui_empty_shade

Change-Id: I3c0f9e9109c6a007749e12be95f5f858388d04a5
parent 0187fa31
Loading
Loading
Loading
Loading
+82 −0
Original line number Original line Diff line number Diff line
@@ -30,6 +30,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.andSceneContainer
import com.android.systemui.flags.andSceneContainer
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
@@ -53,6 +54,7 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
    private val testScope = kosmos.testScope
    private val testScope = kosmos.testScope
    private val zenModeRepository = kosmos.zenModeRepository
    private val zenModeRepository = kosmos.zenModeRepository
    private val activeNotificationListRepository = kosmos.activeNotificationListRepository
    private val activeNotificationListRepository = kosmos.activeNotificationListRepository
    private val fakeSecureSettingsRepository = kosmos.fakeSecureSettingsRepository


    private val underTest = kosmos.emptyShadeViewModel
    private val underTest = kosmos.emptyShadeViewModel


@@ -205,4 +207,84 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {


            assertThat(footerVisible).isTrue()
            assertThat(footerVisible).isTrue()
        }
        }

    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
    @Test
    fun onClick_whenHistoryDisabled_leadsToSettingsPage() =
        testScope.runTest {
            val onClick by collectLastValue(underTest.onClick)
            runCurrent()

            fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0)

            assertThat(onClick?.targetIntent?.action)
                .isEqualTo(Settings.ACTION_NOTIFICATION_SETTINGS)
            assertThat(onClick?.backStack).isEmpty()
        }

    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
    @Test
    fun onClick_whenHistoryEnabled_leadsToHistoryPage() =
        testScope.runTest {
            val onClick by collectLastValue(underTest.onClick)
            runCurrent()

            fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 1)

            assertThat(onClick?.targetIntent?.action)
                .isEqualTo(Settings.ACTION_NOTIFICATION_HISTORY)
            assertThat(onClick?.backStack?.map { it.action })
                .containsExactly(Settings.ACTION_NOTIFICATION_SETTINGS)
        }

    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
    @Test
    fun onClick_whenOneModeHidingNotifications_leadsToModeSettings() =
        testScope.runTest {
            val onClick by collectLastValue(underTest.onClick)
            runCurrent()

            zenModeRepository.addMode(
                TestModeBuilder()
                    .setId("ID")
                    .setActive(true)
                    .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
                    .build()
            )
            runCurrent()

            assertThat(onClick?.targetIntent?.action)
                .isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS)
            assertThat(
                    onClick?.targetIntent?.extras?.getString(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID)
                )
                .isEqualTo("ID")
            assertThat(onClick?.backStack?.map { it.action })
                .containsExactly(Settings.ACTION_ZEN_MODE_SETTINGS)
        }

    @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
    @Test
    fun onClick_whenMultipleModesHidingNotifications_leadsToGeneralModesSettings() =
        testScope.runTest {
            val onClick by collectLastValue(underTest.onClick)
            runCurrent()

            zenModeRepository.addMode(
                TestModeBuilder()
                    .setActive(true)
                    .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
                    .build()
            )
            zenModeRepository.addMode(
                TestModeBuilder()
                    .setActive(true)
                    .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
                    .build()
            )
            runCurrent()

            assertThat(onClick?.targetIntent?.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS)
            assertThat(onClick?.backStack).isEmpty()
        }
}
}
+57 −2
Original line number Original line Diff line number Diff line
@@ -16,6 +16,11 @@
package com.android.systemui.statusbar.notification
package com.android.systemui.statusbar.notification


import android.content.Intent
import android.content.Intent
import android.provider.Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS
import android.provider.Settings.ACTION_NOTIFICATION_HISTORY
import android.provider.Settings.ACTION_NOTIFICATION_SETTINGS
import android.provider.Settings.ACTION_ZEN_MODE_SETTINGS
import android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID
import android.view.View
import android.view.View
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
@@ -25,6 +30,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 * (e.g. clicking on a notification, tapping on the settings icon in the notification guts)
 * (e.g. clicking on a notification, tapping on the settings icon in the notification guts)
 */
 */
interface NotificationActivityStarter {
interface NotificationActivityStarter {

    /** Called when the user clicks on the notification bubble icon. */
    /** Called when the user clicks on the notification bubble icon. */
    fun onNotificationBubbleIconClicked(entry: NotificationEntry?)
    fun onNotificationBubbleIconClicked(entry: NotificationEntry?)


@@ -35,14 +41,63 @@ interface NotificationActivityStarter {
    fun startNotificationGutsIntent(intent: Intent?, appUid: Int, row: ExpandableNotificationRow?)
    fun startNotificationGutsIntent(intent: Intent?, appUid: Int, row: ExpandableNotificationRow?)


    /**
    /**
     * Called when the user clicks "Manage" or "History" in the Shade, or the "No notifications"
     * Called when the user clicks "Manage" or "History" in the Shade. Prefer using
     * text.
     * [startSettingsIntent] instead.
     */
     */
    fun startHistoryIntent(view: View?, showHistory: Boolean)
    fun startHistoryIntent(view: View?, showHistory: Boolean)


    /**
     * Called to open a settings intent from a launchable view (such as the "Manage" or "History"
     * button in the shade, or the "No notifications" text).
     *
     * @param view the view to perform the launch animation from (must extend [LaunchableView])
     * @param intentInfo information about the (settings) intent to be launched
     */
    fun startSettingsIntent(view: View, intentInfo: SettingsIntent)

    /** Called when the user succeed to drop notification to proper target view. */
    /** Called when the user succeed to drop notification to proper target view. */
    fun onDragSuccess(entry: NotificationEntry?)
    fun onDragSuccess(entry: NotificationEntry?)


    val isCollapsingToShowActivityOverLockscreen: Boolean
    val isCollapsingToShowActivityOverLockscreen: Boolean
        get() = false
        get() = false

    /**
     * Information about a settings intent to be launched.
     *
     * If the [targetIntent] is T and [backStack] is [A, B, C], the stack will look like
     * [A, B, C, T].
     */
    data class SettingsIntent(
        var targetIntent: Intent,
        var backStack: List<Intent> = emptyList(),
        var cujType: Int? = null,
    ) {
        // Utility factory methods for known intents
        companion object {
            fun forNotificationSettings(cujType: Int? = null) =
                SettingsIntent(
                    targetIntent = Intent(ACTION_NOTIFICATION_SETTINGS),
                    cujType = cujType,
                )

            fun forNotificationHistory(cujType: Int? = null) =
                SettingsIntent(
                    targetIntent = Intent(ACTION_NOTIFICATION_HISTORY),
                    backStack = listOf(Intent(ACTION_NOTIFICATION_SETTINGS)),
                    cujType = cujType,
                )

            fun forModesSettings(cujType: Int? = null) =
                SettingsIntent(targetIntent = Intent(ACTION_ZEN_MODE_SETTINGS), cujType = cujType)

            fun forModeSettings(modeId: String, cujType: Int? = null) =
                SettingsIntent(
                    targetIntent =
                        Intent(ACTION_AUTOMATIC_ZEN_RULE_SETTINGS)
                            .putExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID, modeId),
                    backStack = listOf(Intent(ACTION_ZEN_MODE_SETTINGS)),
                    cujType = cujType,
                )
        }
    }
}
}
+6 −7
Original line number Original line Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.emptyshade.ui.viewbinder
package com.android.systemui.statusbar.notification.emptyshade.ui.viewbinder


import android.view.View
import android.view.View
import com.android.systemui.statusbar.notification.NotificationActivityStarter
import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView
import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView
import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.coroutineScope
@@ -26,18 +27,16 @@ object EmptyShadeViewBinder {
    suspend fun bind(
    suspend fun bind(
        view: EmptyShadeView,
        view: EmptyShadeView,
        viewModel: EmptyShadeViewModel,
        viewModel: EmptyShadeViewModel,
        launchNotificationSettings: View.OnClickListener,
        notificationActivityStarter: NotificationActivityStarter,
        launchNotificationHistory: View.OnClickListener,
    ) = coroutineScope {
    ) = coroutineScope {
        launch { viewModel.text.collect { view.setText(it) } }
        launch { viewModel.text.collect { view.setText(it) } }


        launch {
        launch {
            viewModel.tappingShouldLaunchHistory.collect { shouldLaunchHistory ->
            viewModel.onClick.collect { settingsIntent ->
                if (shouldLaunchHistory) {
                val onClickListener = { view: View ->
                    view.setOnClickListener(launchNotificationHistory)
                    notificationActivityStarter.startSettingsIntent(view, settingsIntent)
                } else {
                    view.setOnClickListener(launchNotificationSettings)
                }
                }
                view.setOnClickListener(onClickListener)
            }
            }
        }
        }


+22 −4
Original line number Original line Diff line number Diff line
@@ -22,6 +22,7 @@ import com.android.systemui.dump.DumpManager
import com.android.systemui.modes.shared.ModesUi
import com.android.systemui.modes.shared.ModesUi
import com.android.systemui.res.R
import com.android.systemui.res.R
import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
import com.android.systemui.statusbar.notification.NotificationActivityStarter.SettingsIntent
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
@@ -34,6 +35,7 @@ import java.util.Locale
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.map


@@ -80,8 +82,7 @@ constructor(
            if (ModesUi.isEnabled) {
            if (ModesUi.isEnabled) {
                zenModeInteractor.modesHidingNotifications.map { modes ->
                zenModeInteractor.modesHidingNotifications.map { modes ->
                    // Create a string that is either "No notifications" if no modes are filtering
                    // Create a string that is either "No notifications" if no modes are filtering
                    // them
                    // them out, or something like "Notifications paused by SomeMode" otherwise.
                    // out, or something like "Notifications paused by SomeMode" otherwise.
                    val msgFormat =
                    val msgFormat =
                        MessageFormat(
                        MessageFormat(
                            context.getString(R.string.modes_suppressing_shade_text),
                            context.getString(R.string.modes_suppressing_shade_text),
@@ -116,9 +117,26 @@ constructor(
        )
        )
    }
    }


    val tappingShouldLaunchHistory by lazy {
    val onClick: Flow<SettingsIntent> by lazy {
        ModesEmptyShadeFix.assertInNewMode()
        ModesEmptyShadeFix.assertInNewMode()
        notificationSettingsInteractor.isNotificationHistoryEnabled
        combine(
            zenModeInteractor.modesHidingNotifications,
            notificationSettingsInteractor.isNotificationHistoryEnabled,
        ) { modes, isNotificationHistoryEnabled ->
            if (modes.isNotEmpty()) {
                if (modes.size == 1) {
                    SettingsIntent.forModeSettings(modes[0].id)
                } else {
                    SettingsIntent.forModesSettings()
                }
            } else {
                if (isNotificationHistoryEnabled) {
                    SettingsIntent.forNotificationHistory()
                } else {
                    SettingsIntent.forNotificationSettings()
                }
            }
        }
    }
    }


    @AssistedFactory
    @AssistedFactory
+21 −5
Original line number Original line Diff line number Diff line
@@ -19,6 +19,8 @@ package com.android.systemui.statusbar.notification.footer.ui.viewbinder
import android.view.View
import android.view.View
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.lifecycleScope
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.notification.NotificationActivityStarter
import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
import com.android.systemui.util.ui.isAnimating
import com.android.systemui.util.ui.isAnimating
@@ -36,6 +38,7 @@ object FooterViewBinder {
        clearAllNotifications: View.OnClickListener,
        clearAllNotifications: View.OnClickListener,
        launchNotificationSettings: View.OnClickListener,
        launchNotificationSettings: View.OnClickListener,
        launchNotificationHistory: View.OnClickListener,
        launchNotificationHistory: View.OnClickListener,
        notificationActivityStarter: NotificationActivityStarter,
    ): DisposableHandle {
    ): DisposableHandle {
        return footer.repeatWhenAttached {
        return footer.repeatWhenAttached {
            lifecycleScope.launch {
            lifecycleScope.launch {
@@ -45,6 +48,7 @@ object FooterViewBinder {
                    clearAllNotifications,
                    clearAllNotifications,
                    launchNotificationSettings,
                    launchNotificationSettings,
                    launchNotificationHistory,
                    launchNotificationHistory,
                    notificationActivityStarter,
                )
                )
            }
            }
        }
        }
@@ -56,6 +60,7 @@ object FooterViewBinder {
        clearAllNotifications: View.OnClickListener,
        clearAllNotifications: View.OnClickListener,
        launchNotificationSettings: View.OnClickListener,
        launchNotificationSettings: View.OnClickListener,
        launchNotificationHistory: View.OnClickListener,
        launchNotificationHistory: View.OnClickListener,
        notificationActivityStarter: NotificationActivityStarter,
    ) = coroutineScope {
    ) = coroutineScope {
        launch { bindClearAllButton(footer, viewModel, clearAllNotifications) }
        launch { bindClearAllButton(footer, viewModel, clearAllNotifications) }
        launch {
        launch {
@@ -64,6 +69,7 @@ object FooterViewBinder {
                viewModel,
                viewModel,
                launchNotificationSettings,
                launchNotificationSettings,
                launchNotificationHistory,
                launchNotificationHistory,
                notificationActivityStarter,
            )
            )
        }
        }
        launch { bindMessage(footer, viewModel) }
        launch { bindMessage(footer, viewModel) }
@@ -113,8 +119,17 @@ object FooterViewBinder {
        viewModel: FooterViewModel,
        viewModel: FooterViewModel,
        launchNotificationSettings: View.OnClickListener,
        launchNotificationSettings: View.OnClickListener,
        launchNotificationHistory: View.OnClickListener,
        launchNotificationHistory: View.OnClickListener,
        notificationActivityStarter: NotificationActivityStarter,
    ) = coroutineScope {
    ) = coroutineScope {
        launch {
        launch {
            if (ModesEmptyShadeFix.isEnabled) {
                viewModel.manageOrHistoryButtonClick.collect { settingsIntent ->
                    val onClickListener = { view: View ->
                        notificationActivityStarter.startSettingsIntent(view, settingsIntent)
                    }
                    footer.setManageButtonClickListener(onClickListener)
                }
            } else {
                viewModel.manageButtonShouldLaunchHistory.collect { shouldLaunchHistory ->
                viewModel.manageButtonShouldLaunchHistory.collect { shouldLaunchHistory ->
                    if (shouldLaunchHistory) {
                    if (shouldLaunchHistory) {
                        footer.setManageButtonClickListener(launchNotificationHistory)
                        footer.setManageButtonClickListener(launchNotificationHistory)
@@ -123,6 +138,7 @@ object FooterViewBinder {
                    }
                    }
                }
                }
            }
            }
        }


        launch {
        launch {
            viewModel.manageOrHistoryButton.labelId.collect { textId ->
            viewModel.manageOrHistoryButton.labelId.collect { textId ->
Loading