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

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

Use ZenModesBackend for tile state

Also had to move TestModeBuilder to SettingsLib so we can use it in
tests here too.

Bug: 346519570
Test: tested manually that tile state gets updated when toggling dnd&bedtime
Flag: android.app.modes_ui

Change-Id: Ib85e2ede102f98cc5ebde714734aa976c3e1f9e4
parent a959a9d2
Loading
Loading
Loading
Loading
+27 −2
Original line number Diff line number Diff line
@@ -18,6 +18,9 @@ package com.android.settingslib.notification.data.repository

import android.app.NotificationManager
import android.provider.Settings
import com.android.settingslib.notification.modes.TestModeBuilder
import com.android.settingslib.notification.modes.ZenMode
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -32,6 +35,11 @@ class FakeZenModeRepository : ZenModeRepository {
    override val globalZenMode: StateFlow<Int>
        get() = mutableZenMode.asStateFlow()

    private val mutableModesFlow: MutableStateFlow<List<ZenMode>> =
        MutableStateFlow(listOf(TestModeBuilder.EXAMPLE))
    override val modes: Flow<List<ZenMode>>
        get() = mutableModesFlow.asStateFlow()

    init {
        updateNotificationPolicy()
    }
@@ -43,6 +51,20 @@ class FakeZenModeRepository : ZenModeRepository {
    fun updateZenMode(zenMode: Int) {
        mutableZenMode.value = zenMode
    }

    fun addMode(id: String, active: Boolean = false) {
        mutableModesFlow.value += newMode(id, active)
    }

    fun removeMode(id: String) {
        mutableModesFlow.value = mutableModesFlow.value.filter { it.id != id }
    }

    fun deactivateMode(id: String) {
        val oldMode = mutableModesFlow.value.find { it.id == id } ?: return
        removeMode(id)
        mutableModesFlow.value += TestModeBuilder(oldMode).setActive(false).build()
    }
}

fun FakeZenModeRepository.updateNotificationPolicy(
@@ -61,5 +83,8 @@ fun FakeZenModeRepository.updateNotificationPolicy(
            suppressedVisualEffects,
            state,
            priorityConversationSenders,
        )
    )
        ))

private fun newMode(id: String, active: Boolean = false): ZenMode {
    return TestModeBuilder().setId(id).setName("Mode $id").setActive(active).build()
}
+54 −1
Original line number Diff line number Diff line
@@ -18,18 +18,26 @@ package com.android.settingslib.notification.data.repository

import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.database.ContentObserver
import android.os.Handler
import android.provider.Settings
import com.android.settingslib.flags.Flags
import com.android.settingslib.notification.modes.ZenMode
import com.android.settingslib.notification.modes.ZenModesBackend
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
@@ -44,11 +52,16 @@ interface ZenModeRepository {

    /** @see NotificationManager.getZenMode */
    val globalZenMode: StateFlow<Int?>

    /** A list of all existing priority modes. */
    val modes: Flow<List<ZenMode>>
}

class ZenModeRepositoryImpl(
    private val context: Context,
    private val notificationManager: NotificationManager,
    private val backend: ZenModesBackend,
    private val contentResolver: ContentResolver,
    val scope: CoroutineScope,
    val backgroundCoroutineContext: CoroutineContext,
    // This is nullable just to simplify testing, since SettingsLib doesn't have a good way
@@ -87,7 +100,6 @@ class ZenModeRepositoryImpl(
            .let {
                if (Flags.volumePanelBroadcastFix()) {
                    it.flowOn(backgroundCoroutineContext)
                        .stateIn(scope, SharingStarted.WhileSubscribed(), null)
                } else {
                    it.shareIn(
                        started = SharingStarted.WhileSubscribed(),
@@ -121,4 +133,45 @@ class ZenModeRepositoryImpl(
            .onStart { emit(mapper()) }
            .flowOn(backgroundCoroutineContext)
            .stateIn(scope, SharingStarted.WhileSubscribed(), null)

    private val zenConfigChanged by lazy {
        if (android.app.Flags.modesUi()) {
            callbackFlow {
                    // emit an initial value
                    trySend(Unit)

                    val observer =
                        object : ContentObserver(backgroundHandler) {
                            override fun onChange(selfChange: Boolean) {
                                trySend(Unit)
                            }
                        }

                    contentResolver.registerContentObserver(
                        Settings.Global.getUriFor(Settings.Global.ZEN_MODE),
                        /* notifyForDescendants= */ false,
                        observer)
                    contentResolver.registerContentObserver(
                        Settings.Global.getUriFor(Settings.Global.ZEN_MODE_CONFIG_ETAG),
                        /* notifyForDescendants= */ false,
                        observer)

                    awaitClose { contentResolver.unregisterContentObserver(observer) }
                }
                .flowOn(backgroundCoroutineContext)
        } else {
            flowOf(Unit)
        }
    }

    override val modes: Flow<List<ZenMode>> by lazy {
        if (android.app.Flags.modesUi()) {
            zenConfigChanged
                .map { backend.modes }
                .distinctUntilChanged()
                .flowOn(backgroundCoroutineContext)
        } else {
            flowOf(emptyList())
        }
    }
}
+176 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.settingslib.notification.modes;

import android.app.AutomaticZenRule;
import android.app.NotificationManager;
import android.content.ComponentName;
import android.net.Uri;
import android.service.notification.Condition;
import android.service.notification.ZenDeviceEffects;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenPolicy;

import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;

import java.util.Random;

public class TestModeBuilder {

    private String mId;
    private AutomaticZenRule mRule;
    private ZenModeConfig.ZenRule mConfigZenRule;

    public static final ZenMode EXAMPLE = new TestModeBuilder().build();

    public TestModeBuilder() {
        // Reasonable defaults
        int id = new Random().nextInt(1000);
        mId = "rule_" + id;
        mRule = new AutomaticZenRule.Builder("Test Rule #" + id, Uri.parse("rule://" + id))
                .setPackage("some_package")
                .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
                .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build())
                .build();
        mConfigZenRule = new ZenModeConfig.ZenRule();
        mConfigZenRule.enabled = true;
        mConfigZenRule.pkg = "some_package";
    }

    public TestModeBuilder(ZenMode previous) {
        mId = previous.getId();
        mRule = previous.getRule();

        mConfigZenRule = new ZenModeConfig.ZenRule();
        mConfigZenRule.enabled = previous.getRule().isEnabled();
        mConfigZenRule.pkg = previous.getRule().getPackageName();
        setActive(previous.isActive());
    }

    public TestModeBuilder setId(String id) {
        mId = id;
        return this;
    }

    public TestModeBuilder setAzr(AutomaticZenRule rule) {
        mRule = rule;
        mConfigZenRule.pkg = rule.getPackageName();
        mConfigZenRule.conditionId = rule.getConditionId();
        mConfigZenRule.enabled = rule.isEnabled();
        return this;
    }

    public TestModeBuilder setConfigZenRule(ZenModeConfig.ZenRule configZenRule) {
        mConfigZenRule = configZenRule;
        return this;
    }

    public TestModeBuilder setName(String name) {
        mRule.setName(name);
        mConfigZenRule.name = name;
        return this;
    }

    public TestModeBuilder setPackage(String pkg) {
        mRule.setPackageName(pkg);
        mConfigZenRule.pkg = pkg;
        return this;
    }

    public TestModeBuilder setOwner(ComponentName owner) {
        mRule.setOwner(owner);
        mConfigZenRule.component = owner;
        return this;
    }

    public TestModeBuilder setConfigurationActivity(ComponentName configActivity) {
        mRule.setConfigurationActivity(configActivity);
        mConfigZenRule.configurationActivity = configActivity;
        return this;
    }

    public TestModeBuilder setConditionId(Uri conditionId) {
        mRule.setConditionId(conditionId);
        mConfigZenRule.conditionId = conditionId;
        return this;
    }

    public TestModeBuilder setType(@AutomaticZenRule.Type int type) {
        mRule.setType(type);
        mConfigZenRule.type = type;
        return this;
    }

    public TestModeBuilder setInterruptionFilter(
            @NotificationManager.InterruptionFilter int interruptionFilter) {
        mRule.setInterruptionFilter(interruptionFilter);
        mConfigZenRule.zenMode = NotificationManager.zenModeFromInterruptionFilter(
                interruptionFilter, NotificationManager.INTERRUPTION_FILTER_PRIORITY);
        return this;
    }

    public TestModeBuilder setZenPolicy(@Nullable ZenPolicy policy) {
        mRule.setZenPolicy(policy);
        mConfigZenRule.zenPolicy = policy;
        return this;
    }

    public TestModeBuilder setDeviceEffects(@Nullable ZenDeviceEffects deviceEffects) {
        mRule.setDeviceEffects(deviceEffects);
        mConfigZenRule.zenDeviceEffects = deviceEffects;
        return this;
    }

    public TestModeBuilder setEnabled(boolean enabled) {
        mRule.setEnabled(enabled);
        mConfigZenRule.enabled = enabled;
        return this;
    }

    public TestModeBuilder setManualInvocationAllowed(boolean allowed) {
        mRule.setManualInvocationAllowed(allowed);
        mConfigZenRule.allowManualInvocation = allowed;
        return this;
    }

    public TestModeBuilder setTriggerDescription(@Nullable String triggerDescription) {
        mRule.setTriggerDescription(triggerDescription);
        mConfigZenRule.triggerDescription = triggerDescription;
        return this;
    }

    public TestModeBuilder setIconResId(@DrawableRes int iconResId) {
        mRule.setIconResId(iconResId);
        return this;
    }

    public TestModeBuilder setActive(boolean active) {
        if (active) {
            mConfigZenRule.enabled = true;
            mConfigZenRule.condition = new Condition(mRule.getConditionId(), "...",
                    Condition.STATE_TRUE);
        } else {
            mConfigZenRule.condition = null;
        }
        return this;
    }

    public ZenMode build() {
        return new ZenMode(mId, mRule, mConfigZenRule);
    }
}
+66 −0
Original line number Diff line number Diff line
@@ -18,13 +18,18 @@ package com.android.settingslib.notification.data.repository

import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.database.ContentObserver
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.provider.Settings.Global
import androidx.test.filters.SmallTest
import com.android.settingslib.flags.Flags
import com.android.settingslib.notification.modes.TestModeBuilder
import com.android.settingslib.notification.modes.ZenMode
import com.android.settingslib.notification.modes.ZenModesBackend
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
@@ -36,6 +41,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.eq
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.any
@@ -52,8 +58,16 @@ class ZenModeRepositoryTest {

    @Mock private lateinit var notificationManager: NotificationManager

    @Mock private lateinit var zenModesBackend: ZenModesBackend

    @Mock private lateinit var contentResolver: ContentResolver

    @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver>

    @Captor private lateinit var zenModeObserverCaptor: ArgumentCaptor<ContentObserver>

    @Captor private lateinit var zenConfigObserverCaptor: ArgumentCaptor<ContentObserver>

    private lateinit var underTest: ZenModeRepository

    private val testScope: TestScope = TestScope()
@@ -66,6 +80,8 @@ class ZenModeRepositoryTest {
            ZenModeRepositoryImpl(
                context,
                notificationManager,
                zenModesBackend,
                contentResolver,
                testScope.backgroundScope,
                testScope.testScheduler,
                backgroundHandler = null,
@@ -128,11 +144,61 @@ class ZenModeRepositoryTest {
        }
    }

    @EnableFlags(android.app.Flags.FLAG_MODES_UI)
    @Test
    fun modesListEmitsOnSettingsChange() {
        testScope.runTest {
            val values = mutableListOf<List<ZenMode>>()
            val modes1 = listOf(TestModeBuilder().setId("One").build())
            `when`(zenModesBackend.modes).thenReturn(modes1)
            underTest.modes.onEach { values.add(it) }.launchIn(backgroundScope)
            runCurrent()

            // zen mode change triggers update
            val modes2 = listOf(TestModeBuilder().setId("Two").build())
            `when`(zenModesBackend.modes).thenReturn(modes2)
            triggerZenModeSettingUpdate()
            runCurrent()

            // zen config change also triggers update
            val modes3 = listOf(TestModeBuilder().setId("Three").build())
            `when`(zenModesBackend.modes).thenReturn(modes3)
            triggerZenConfigSettingUpdate()
            runCurrent()

            // setting update with no list change doesn't trigger update
            triggerZenModeSettingUpdate()
            runCurrent()

            assertThat(values).containsExactly(modes1, modes2, modes3).inOrder()
        }
    }

    private fun triggerIntent(action: String) {
        verify(context).registerReceiver(receiverCaptor.capture(), any(), any(), any())
        receiverCaptor.value.onReceive(context, Intent(action))
    }

    private fun triggerZenModeSettingUpdate() {
        verify(contentResolver)
            .registerContentObserver(
                eq(Global.getUriFor(Global.ZEN_MODE)),
                eq(false),
                zenModeObserverCaptor.capture(),
            )
        zenModeObserverCaptor.value.onChange(false)
    }

    private fun triggerZenConfigSettingUpdate() {
        verify(contentResolver)
            .registerContentObserver(
                eq(Global.getUriFor(Global.ZEN_MODE_CONFIG_ETAG)),
                eq(false),
                zenConfigObserverCaptor.capture(),
            )
        zenConfigObserverCaptor.value.onChange(false)
    }

    private companion object {
        val testPolicy1 =
            NotificationManager.Policy(
+13 −12
Original line number Diff line number Diff line
@@ -20,9 +20,6 @@ import android.app.Flags
import android.os.UserHandle
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
import android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS
import android.provider.Settings.Global.ZEN_MODE_OFF
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.notification.data.repository.FakeZenModeRepository
@@ -65,24 +62,28 @@ class ModesTileDataInteractorTest : SysuiTestCase() {

    @EnableFlags(Flags.FLAG_MODES_UI)
    @Test
    fun dataMatchesTheRepository() = runTest {
    fun isActivatedWhenModesChange() = runTest {
        val dataList: List<ModesTileModel> by
            collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)))
        runCurrent()
        assertThat(dataList.map { it.isActivated }).containsExactly(false).inOrder()

        // Enable zen mode
        zenModeRepository.updateZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS)
        // Add active mode
        zenModeRepository.addMode(id = "One", active = true)
        runCurrent()
        assertThat(dataList.map { it.isActivated }).containsExactly(false, true).inOrder()

        // Change zen mode: it's still enabled, so this shouldn't cause another emission
        zenModeRepository.updateZenMode(ZEN_MODE_NO_INTERRUPTIONS)
        // Add another mode: state hasn't changed, so this shouldn't cause another emission
        zenModeRepository.addMode(id = "Two", active = true)
        runCurrent()
        assertThat(dataList.map { it.isActivated }).containsExactly(false, true).inOrder()

        // Disable zen mode
        zenModeRepository.updateZenMode(ZEN_MODE_OFF)
        // Remove a mode and disable the other
        zenModeRepository.removeMode("One")
        runCurrent()

        assertThat(dataList.map { it.isActivated }).containsExactly(false, true, false)
        zenModeRepository.deactivateMode("Two")
        runCurrent()
        assertThat(dataList.map { it.isActivated }).containsExactly(false, true, false).inOrder()
    }

    private companion object {
Loading