diff options
7 files changed, 341 insertions, 18 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt index 7f6a8ed1718e..7886e85cbad8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt @@ -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() +} diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt index 72c3c1719f70..ef9452648a70 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt @@ -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()) + } + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java new file mode 100644 index 000000000000..7b994d59d963 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java @@ -0,0 +1,176 @@ +/* + * 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); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt index 06333b61eeb1..6e11e1f612ef 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt @@ -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( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt index 583c10fe429e..09dca25d693e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt @@ -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 { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt index da4d2f1c0085..930109a87545 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt @@ -18,7 +18,6 @@ package com.android.systemui.qs.tiles.impl.modes.domain.interactor import android.app.Flags import android.os.UserHandle -import android.provider.Settings.Global.ZEN_MODE_OFF import com.android.settingslib.notification.data.repository.ZenModeRepository import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor @@ -31,9 +30,10 @@ import kotlinx.coroutines.flow.map class ModesTileDataInteractor @Inject constructor(val zenModeRepository: ZenModeRepository) : QSTileDataInteractor<ModesTileModel> { - // TODO(b/346519570): This should be checking for any enabled modes. private val zenModeActive = - zenModeRepository.globalZenMode.map { it != ZEN_MODE_OFF }.distinctUntilChanged() + zenModeRepository.modes + .map { modes -> modes.any { mode -> mode.isActive } } + .distinctUntilChanged() override fun tileData( user: UserHandle, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index b63ee4c52e14..ca5f49d28823 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -25,6 +25,7 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.settingslib.notification.data.repository.ZenModeRepository; import com.android.settingslib.notification.data.repository.ZenModeRepositoryImpl; import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor; +import com.android.settingslib.notification.modes.ZenModesBackend; import com.android.systemui.CoreStartable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Application; @@ -288,6 +289,7 @@ public interface NotificationsModule { @Background Handler handler ) { return new ZenModeRepositoryImpl(context, notificationManager, + ZenModesBackend.getInstance(context), context.getContentResolver(), coroutineScope, coroutineContext, handler); } |