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

Commit 84258941 authored by Anton Potapov's avatar Anton Potapov
Browse files

Update ZenModeInteractor to return a ZenMode that blocks an audio stream

Flag: android.app.modes_ui
Bug: 372466264
Test: atest ZenModeInteractorTest
Test: atest AudioStreamSliderViewModel
Test: atest VolumePanelScreenshotTest
Test: manual on the phone. Open volume panel with an active mode
blocking different streams

Change-Id: I45f1e4859c8d5ac6e71d041989bb9a679b51c4dd
parent b01ac598
Loading
Loading
Loading
Loading
+50 −41
Original line number Diff line number Diff line
@@ -18,9 +18,8 @@ package com.android.systemui.statusbar.policy.domain.interactor

import android.app.AutomaticZenRule
import android.app.Flags
import android.app.NotificationManager.INTERRUPTION_FILTER_NONE
import android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY
import android.app.NotificationManager.Policy
import android.media.AudioManager
import android.platform.test.annotations.EnableFlags
import android.provider.Settings
import android.provider.Settings.Secure.ZEN_DURATION
@@ -34,6 +33,7 @@ import androidx.test.filters.SmallTest
import com.android.internal.R
import com.android.settingslib.notification.data.repository.updateNotificationPolicy
import com.android.settingslib.notification.modes.TestModeBuilder
import com.android.settingslib.volume.shared.model.AudioStream
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
@@ -402,115 +402,124 @@ class ZenModeInteractorTest : SysuiTestCase() {

    @Test
    @EnableFlags(Flags.FLAG_MODES_UI)
    fun activeModesBlockingEverything_hasModesWithFilterNone() =
    fun activeModesBlockingMedia_hasModesWithPolicyBlockingMedia() =
        testScope.runTest {
            val blockingEverything by collectLastValue(underTest.activeModesBlockingEverything)
            val blockingMedia by
                collectLastValue(
                    underTest.activeModesBlockingStream(AudioStream(AudioManager.STREAM_MUSIC))
                )

            zenModeRepository.addModes(
                listOf(
                    TestModeBuilder()
                        .setName("Filter=None, Not active")
                        .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
                        .setName("Blocks media, Not active")
                        .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
                        .setActive(false)
                        .build(),
                    TestModeBuilder()
                        .setName("Filter=Priority, Active")
                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
                        .setName("Allows media, Active")
                        .setZenPolicy(ZenPolicy.Builder().allowMedia(true).build())
                        .setActive(true)
                        .build(),
                    TestModeBuilder()
                        .setName("Filter=None, Active")
                        .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
                        .setName("Blocks media, Active")
                        .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
                        .setActive(true)
                        .build(),
                    TestModeBuilder()
                        .setName("Filter=None, Active Too")
                        .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
                        .setName("Blocks media, Active Too")
                        .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
                        .setActive(true)
                        .build(),
                )
            )
            runCurrent()

            assertThat(blockingEverything!!.mainMode!!.name).isEqualTo("Filter=None, Active")
            assertThat(blockingEverything!!.modeNames)
                .containsExactly("Filter=None, Active", "Filter=None, Active Too")
            assertThat(blockingMedia!!.mainMode!!.name).isEqualTo("Blocks media, Active")
            assertThat(blockingMedia!!.modeNames)
                .containsExactly("Blocks media, Active", "Blocks media, Active Too")
                .inOrder()
        }

    @Test
    @EnableFlags(Flags.FLAG_MODES_UI)
    fun activeModesBlockingMedia_hasModesWithPolicyBlockingMedia() =
    fun activeModesBlockingAlarms_hasModesWithPolicyBlockingAlarms() =
        testScope.runTest {
            val blockingMedia by collectLastValue(underTest.activeModesBlockingMedia)
            val blockingAlarms by
                collectLastValue(
                    underTest.activeModesBlockingStream(AudioStream(AudioManager.STREAM_ALARM))
                )

            zenModeRepository.addModes(
                listOf(
                    TestModeBuilder()
                        .setName("Blocks media, Not active")
                        .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
                        .setName("Blocks alarms, Not active")
                        .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build())
                        .setActive(false)
                        .build(),
                    TestModeBuilder()
                        .setName("Allows media, Active")
                        .setZenPolicy(ZenPolicy.Builder().allowMedia(true).build())
                        .setName("Allows alarms, Active")
                        .setZenPolicy(ZenPolicy.Builder().allowAlarms(true).build())
                        .setActive(true)
                        .build(),
                    TestModeBuilder()
                        .setName("Blocks media, Active")
                        .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
                        .setName("Blocks alarms, Active")
                        .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build())
                        .setActive(true)
                        .build(),
                    TestModeBuilder()
                        .setName("Blocks media, Active Too")
                        .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
                        .setName("Blocks alarms, Active Too")
                        .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build())
                        .setActive(true)
                        .build(),
                )
            )
            runCurrent()

            assertThat(blockingMedia!!.mainMode!!.name).isEqualTo("Blocks media, Active")
            assertThat(blockingMedia!!.modeNames)
                .containsExactly("Blocks media, Active", "Blocks media, Active Too")
            assertThat(blockingAlarms!!.mainMode!!.name).isEqualTo("Blocks alarms, Active")
            assertThat(blockingAlarms!!.modeNames)
                .containsExactly("Blocks alarms, Active", "Blocks alarms, Active Too")
                .inOrder()
        }

    @Test
    @EnableFlags(Flags.FLAG_MODES_UI)
    fun activeModesBlockingAlarms_hasModesWithPolicyBlockingAlarms() =
    fun activeModesBlockingAlarms_hasModesWithPolicyBlockingSystem() =
        testScope.runTest {
            val blockingAlarms by collectLastValue(underTest.activeModesBlockingAlarms)
            val blockingSystem by
                collectLastValue(
                    underTest.activeModesBlockingStream(AudioStream(AudioManager.STREAM_SYSTEM))
                )

            zenModeRepository.addModes(
                listOf(
                    TestModeBuilder()
                        .setName("Blocks alarms, Not active")
                        .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build())
                        .setName("Blocks system, Not active")
                        .setZenPolicy(ZenPolicy.Builder().allowSystem(false).build())
                        .setActive(false)
                        .build(),
                    TestModeBuilder()
                        .setName("Allows alarms, Active")
                        .setZenPolicy(ZenPolicy.Builder().allowAlarms(true).build())
                        .setName("Allows system, Active")
                        .setZenPolicy(ZenPolicy.Builder().allowSystem(true).build())
                        .setActive(true)
                        .build(),
                    TestModeBuilder()
                        .setName("Blocks alarms, Active")
                        .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build())
                        .setName("Blocks system, Active")
                        .setZenPolicy(ZenPolicy.Builder().allowSystem(false).build())
                        .setActive(true)
                        .build(),
                    TestModeBuilder()
                        .setName("Blocks alarms, Active Too")
                        .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build())
                        .setName("Blocks system, Active Too")
                        .setZenPolicy(ZenPolicy.Builder().allowSystem(false).build())
                        .setActive(true)
                        .build(),
                )
            )
            runCurrent()

            assertThat(blockingAlarms!!.mainMode!!.name).isEqualTo("Blocks alarms, Active")
            assertThat(blockingAlarms!!.modeNames)
                .containsExactly("Blocks alarms, Active", "Blocks alarms, Active Too")
            assertThat(blockingSystem!!.mainMode!!.name).isEqualTo("Blocks system, Active")
            assertThat(blockingSystem!!.modeNames)
                .containsExactly("Blocks system, Active", "Blocks system, Active Too")
                .inOrder()
        }

+23 −50
Original line number Diff line number Diff line
@@ -23,66 +23,40 @@ import android.platform.test.annotations.EnableFlags
import android.service.notification.ZenPolicy
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.uiEventLogger
import com.android.settingslib.notification.modes.TestModeBuilder
import com.android.settingslib.volume.shared.model.AudioStream
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
import com.android.systemui.testKosmos
import com.android.systemui.volume.domain.interactor.audioVolumeInteractor
import com.android.systemui.volume.shared.volumePanelLogger
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class AudioStreamSliderViewModelTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val zenModeRepository = kosmos.fakeZenModeRepository

    private lateinit var mediaStream: AudioStreamSliderViewModel
    private lateinit var alarmsStream: AudioStreamSliderViewModel
    private lateinit var notificationStream: AudioStreamSliderViewModel
    private lateinit var otherStream: AudioStreamSliderViewModel

    @Before
    fun setUp() {
        mediaStream = audioStreamSliderViewModel(AudioManager.STREAM_MUSIC)
        alarmsStream = audioStreamSliderViewModel(AudioManager.STREAM_ALARM)
        notificationStream = audioStreamSliderViewModel(AudioManager.STREAM_NOTIFICATION)
        otherStream = audioStreamSliderViewModel(AudioManager.STREAM_VOICE_CALL)
    }

    private fun audioStreamSliderViewModel(stream: Int): AudioStreamSliderViewModel {
        return AudioStreamSliderViewModel(
    private fun Kosmos.audioStreamSliderViewModel(stream: Int): AudioStreamSliderViewModel {
        return audioStreamSliderViewModelFactory.create(
            AudioStreamSliderViewModel.FactoryAudioStreamWrapper(AudioStream(stream)),
            testScope.backgroundScope,
            context,
            kosmos.audioVolumeInteractor,
            kosmos.zenModeInteractor,
            kosmos.uiEventLogger,
            kosmos.volumePanelLogger,
            kosmos.sliderHapticsViewModelFactory,
            applicationCoroutineScope,
        )
    }

    @Test
    @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
    fun slider_media_hasDisabledByModesText() =
        testScope.runTest {
            val mediaSlider by collectLastValue(mediaStream.slider)
        kosmos.runTest {
            val mediaSlider by
                collectLastValue(audioStreamSliderViewModel(AudioManager.STREAM_MUSIC).slider)

            zenModeRepository.addMode(
                TestModeBuilder()
@@ -112,8 +86,9 @@ class AudioStreamSliderViewModelTest : SysuiTestCase() {
    @Test
    @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
    fun slider_alarms_hasDisabledByModesText() =
        testScope.runTest {
            val alarmsSlider by collectLastValue(alarmsStream.slider)
        kosmos.runTest {
            val alarmsSlider by
                collectLastValue(audioStreamSliderViewModel(AudioManager.STREAM_ALARM).slider)

            zenModeRepository.addMode(
                TestModeBuilder()
@@ -141,9 +116,10 @@ class AudioStreamSliderViewModelTest : SysuiTestCase() {

    @Test
    @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
    fun slider_other_hasDisabledByModesText() =
        testScope.runTest {
            val otherSlider by collectLastValue(otherStream.slider)
    fun slider_other_hasDisabledText() =
        kosmos.runTest {
            val otherSlider by
                collectLastValue(audioStreamSliderViewModel(AudioManager.STREAM_VOICE_CALL).slider)

            zenModeRepository.addMode(
                TestModeBuilder()
@@ -154,20 +130,17 @@ class AudioStreamSliderViewModelTest : SysuiTestCase() {
            )
            runCurrent()

            assertThat(otherSlider!!.disabledMessage)
                .isEqualTo("Unavailable because Everything blocked is on")

            zenModeRepository.clearModes()
            runCurrent()

            assertThat(otherSlider!!.disabledMessage).isEqualTo("Unavailable")
        }

    @Test
    @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
    fun slider_notification_hasSpecialDisabledText() =
        testScope.runTest {
            val notificationSlider by collectLastValue(notificationStream.slider)
        kosmos.runTest {
            val notificationSlider by
                collectLastValue(
                    audioStreamSliderViewModel(AudioManager.STREAM_NOTIFICATION).slider
                )
            runCurrent()

            assertThat(notificationSlider!!.disabledMessage)
+21 −15
Original line number Diff line number Diff line
@@ -16,8 +16,8 @@

package com.android.systemui.statusbar.policy.domain.interactor

import android.app.NotificationManager.INTERRUPTION_FILTER_NONE
import android.content.Context
import android.media.AudioManager
import android.provider.Settings
import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
@@ -29,6 +29,7 @@ import com.android.settingslib.notification.data.repository.ZenModeRepository
import com.android.settingslib.notification.modes.ZenIcon
import com.android.settingslib.notification.modes.ZenIconLoader
import com.android.settingslib.notification.modes.ZenMode
import com.android.settingslib.volume.shared.model.AudioStream
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.modes.shared.ModesUi
import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
@@ -67,6 +68,17 @@ constructor(
    deviceProvisioningRepository: DeviceProvisioningRepository,
    userSetupRepository: UserSetupRepository,
) {
    /**
     * List of predicates to determine if the [ZenMode] blocks an audio stream. Typical use case
     * would be: `zenModeByStreamPredicates[stream](zenMode)`
     */
    private val zenModeByStreamPredicates =
        mapOf<Int, (ZenMode) -> Boolean>(
            AudioManager.STREAM_MUSIC to { it.policy.priorityCategoryMedia == STATE_DISALLOW },
            AudioManager.STREAM_ALARM to { it.policy.priorityCategoryAlarms == STATE_DISALLOW },
            AudioManager.STREAM_SYSTEM to { it.policy.priorityCategorySystem == STATE_DISALLOW },
        )

    val isZenAvailable: Flow<Boolean> =
        combine(
            deviceProvisioningRepository.isDeviceProvisioned,
@@ -125,21 +137,16 @@ constructor(
            .flowOn(bgDispatcher)
            .distinctUntilChanged()

    val activeModesBlockingEverything: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode ->
        mode.interruptionFilter == INTERRUPTION_FILTER_NONE
    }
    fun canBeBlockedByZenMode(stream: AudioStream): Boolean =
        zenModeByStreamPredicates.containsKey(stream.value)

    val activeModesBlockingMedia: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode ->
        mode.policy.priorityCategoryMedia == STATE_DISALLOW
    fun activeModesBlockingStream(stream: AudioStream): Flow<ActiveZenModes> {
        val isBlockingStream = zenModeByStreamPredicates[stream.value]
        require(isBlockingStream != null) {
            "$stream is unsupported. Use canBeBlockedByZenMode to check if the stream can be affected by the Zen Mode."
        }

    val activeModesBlockingAlarms: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode ->
        mode.policy.priorityCategoryAlarms == STATE_DISALLOW
    }

    private fun getFilteredActiveModesFlow(predicate: (ZenMode) -> Boolean): Flow<ActiveZenModes> {
        return modes
            .map { modes -> modes.filter { mode -> predicate(mode) } }
            .map { modes -> modes.filter { isBlockingStream(it) } }
            .map { modes -> buildActiveZenModes(modes) }
            .flowOn(bgDispatcher)
            .distinctUntilChanged()
@@ -194,7 +201,6 @@ constructor(
                        )
                        null
                    }

                    ZEN_DURATION_FOREVER -> null
                    else -> Duration.ofMinutes(zenDuration.toLong())
                }
+36 −78
Original line number Diff line number Diff line
@@ -18,9 +18,6 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel

import android.content.Context
import android.media.AudioManager
import android.media.AudioManager.STREAM_ALARM
import android.media.AudioManager.STREAM_MUSIC
import android.media.AudioManager.STREAM_NOTIFICATION
import android.util.Log
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.logging.UiEventLogger
@@ -34,8 +31,6 @@ import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
import com.android.systemui.modes.shared.ModesUiIcons
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes
import com.android.systemui.util.kotlin.combine
import com.android.systemui.volume.panel.shared.VolumePanelLogger
import com.android.systemui.volume.panel.ui.VolumePanelUiEvent
import dagger.assisted.Assisted
@@ -43,12 +38,15 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn

@@ -101,48 +99,16 @@ constructor(
        )

    override val slider: StateFlow<SliderState> =
        if (ModesUiIcons.isEnabled) {
        combine(
                audioVolumeInteractor.getAudioStream(audioStream),
                audioVolumeInteractor.canChangeVolume(audioStream),
                audioVolumeInteractor.ringerMode,
                    zenModeInteractor.activeModesBlockingEverything,
                    zenModeInteractor.activeModesBlockingAlarms,
                    zenModeInteractor.activeModesBlockingMedia,
                ) {
                    model,
                    isEnabled,
                    ringerMode,
                    modesBlockingEverything,
                    modesBlockingAlarms,
                    modesBlockingMedia ->
                streamDisabledMessage(),
            ) { model, isEnabled, ringerMode, streamDisabledMessage ->
                volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume)
                    model.toState(
                        isEnabled,
                        ringerMode,
                        getStreamDisabledMessage(
                            modesBlockingEverything,
                            modesBlockingAlarms,
                            modesBlockingMedia,
                        ),
                    )
                }
                .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
        } else {
            combine(
                    audioVolumeInteractor.getAudioStream(audioStream),
                    audioVolumeInteractor.canChangeVolume(audioStream),
                    audioVolumeInteractor.ringerMode,
                ) { model, isEnabled, ringerMode ->
                    volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume)
                    model.toState(
                        isEnabled,
                        ringerMode,
                        getStreamDisabledMessageWithoutModes(audioStream),
                    )
                model.toState(isEnabled, ringerMode, streamDisabledMessage)
            }
            .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
        }

    init {
        volumeChanges
@@ -229,41 +195,33 @@ constructor(
        )
    }

    private fun getStreamDisabledMessage(
        blockingEverything: ActiveZenModes,
        blockingAlarms: ActiveZenModes,
        blockingMedia: ActiveZenModes,
    ): String {
    // TODO: b/372213356 - Figure out the correct messages for VOICE_CALL and RING.
    //  In fact, VOICE_CALL should not be affected by interruption filtering at all.
        return if (audioStream.value == STREAM_NOTIFICATION) {
            context.getString(R.string.stream_notification_unavailable)
    private fun streamDisabledMessage(): Flow<String> {
        return if (ModesUiIcons.isEnabled) {
            if (audioStream.value == AudioManager.STREAM_NOTIFICATION) {
                flowOf(context.getString(R.string.stream_notification_unavailable))
            } else {
            val blockingModeName =
                when {
                    blockingEverything.mainMode != null -> blockingEverything.mainMode.name
                    audioStream.value == STREAM_ALARM -> blockingAlarms.mainMode?.name
                    audioStream.value == STREAM_MUSIC -> blockingMedia.mainMode?.name
                    else -> null
                if (zenModeInteractor.canBeBlockedByZenMode(audioStream)) {
                    zenModeInteractor.activeModesBlockingStream(audioStream).map { blockingZenModes
                        ->
                        blockingZenModes.mainMode?.name?.let {
                            context.getString(R.string.stream_unavailable_by_modes, it)
                        } ?: context.getString(R.string.stream_unavailable_by_unknown)
                    }

            if (blockingModeName != null) {
                context.getString(R.string.stream_unavailable_by_modes, blockingModeName)
                } else {
                // Should not actually be visible, but as a catch-all.
                context.getString(R.string.stream_unavailable_by_unknown)
                    flowOf(context.getString(R.string.stream_unavailable_by_unknown))
                }
            }
    }

    private fun getStreamDisabledMessageWithoutModes(audioStream: AudioStream): String {
        // TODO: b/372213356 - Figure out the correct messages for VOICE_CALL and RING.
        //  In fact, VOICE_CALL should not be affected by interruption filtering at all.
        return if (audioStream.value == STREAM_NOTIFICATION) {
        } else {
            flowOf(
                if (audioStream.value == AudioManager.STREAM_NOTIFICATION) {
                    context.getString(R.string.stream_notification_unavailable)
                } else {
                    context.getString(R.string.stream_alarm_unavailable)
                }
            )
        }
    }

    private fun AudioStreamModel.getIcon(ringerMode: RingerMode): Icon {