From 59c6a66f5c918a3608902976d52eb8c5f3b213c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= Date: Mon, 8 Apr 2024 18:50:32 +0200 Subject: [PATCH] Introduce ZenModesBackend and ZenMode Test: atest ZenModesBackendTest ZenModeTest Bug: 327419222 Change-Id: Ic2871a6124b2df4b77275b54a940f7b47666991c --- .../settings/notification/modes/ZenMode.java | 209 +++++++++++++++ .../notification/modes/ZenModesBackend.java | 161 ++++++++++++ .../notification/modes/ZenModeTest.java | 107 ++++++++ .../modes/ZenModesBackendTest.java | 243 ++++++++++++++++++ 4 files changed, 720 insertions(+) create mode 100644 src/com/android/settings/notification/modes/ZenMode.java create mode 100644 src/com/android/settings/notification/modes/ZenModesBackend.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModeTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModesBackendTest.java diff --git a/src/com/android/settings/notification/modes/ZenMode.java b/src/com/android/settings/notification/modes/ZenMode.java new file mode 100644 index 00000000000..b0036788ecd --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenMode.java @@ -0,0 +1,209 @@ +/* + * 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.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; + +import static java.util.Objects.requireNonNull; + +import android.app.AutomaticZenRule; +import android.app.NotificationManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.service.notification.SystemZenRules; +import android.service.notification.ZenPolicy; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; + +import com.android.settings.R; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.Objects; + +/** + * Represents either an {@link AutomaticZenRule} or the manual DND rule in a unified way. + * + *

It also adapts other rule features that we don't want to expose in the UI, such as + * interruption filters other than {@code PRIORITY}, rules without specific icons, etc. + */ +class ZenMode { + + private static final String TAG = "ZenMode"; + + static final String MANUAL_DND_MODE_ID = "manual_dnd"; + + private static final ZenPolicy POLICY_INTERRUPTION_FILTER_ALL = + // TODO: b/331267485 - Support "allow all channels"! + new ZenPolicy.Builder().allowAllSounds().showAllVisualEffects().build(); + + // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy. + private static final ZenPolicy POLICY_INTERRUPTION_FILTER_ALARMS = + new ZenPolicy.Builder() + .disallowAllSounds() + .allowAlarms(true) + .allowMedia(true) + .allowPriorityChannels(false) + .build(); + + // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy. + private static final ZenPolicy POLICY_INTERRUPTION_FILTER_NONE = + new ZenPolicy.Builder() + .disallowAllSounds() + .hideAllVisualEffects() + .allowPriorityChannels(false) + .build(); + + private final String mId; + private final AutomaticZenRule mRule; + private final boolean mIsManualDnd; + + ZenMode(String id, AutomaticZenRule rule) { + this(id, rule, false); + } + + private ZenMode(String id, AutomaticZenRule rule, boolean isManualDnd) { + mId = id; + mRule = rule; + mIsManualDnd = isManualDnd; + } + + static ZenMode manualDndMode(AutomaticZenRule dndPolicyAsRule) { + return new ZenMode(MANUAL_DND_MODE_ID, dndPolicyAsRule, true); + } + + @NonNull + public String getId() { + return mId; + } + + @NonNull + public AutomaticZenRule getRule() { + return mRule; + } + + @NonNull + public ListenableFuture getIcon(@NonNull Context context) { + // TODO: b/333528586 - Load the icons asynchronously, and cache them + if (mIsManualDnd) { + return Futures.immediateFuture( + requireNonNull(context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp))); + } + + int iconResId = mRule.getIconResId(); + Drawable customIcon = null; + if (iconResId != 0) { + if (SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName())) { + customIcon = context.getDrawable(mRule.getIconResId()); + } else { + try { + Context appContext = context.createPackageContext(mRule.getPackageName(), 0); + customIcon = AppCompatResources.getDrawable(appContext, mRule.getIconResId()); + } catch (PackageManager.NameNotFoundException e) { + Log.wtf(TAG, + "Package " + mRule.getPackageName() + " used in rule " + mId + + " not found?", e); + // Continue down to use a default icon. + } + } + } + if (customIcon != null) { + return Futures.immediateFuture(customIcon); + } + + // Derive a default icon from the rule type. + // TODO: b/333528437 - Use correct icons + int iconResIdFromType = switch (mRule.getType()) { + case AutomaticZenRule.TYPE_UNKNOWN -> R.drawable.ic_do_not_disturb_on_24dp; + case AutomaticZenRule.TYPE_OTHER -> R.drawable.ic_do_not_disturb_on_24dp; + case AutomaticZenRule.TYPE_SCHEDULE_TIME -> R.drawable.ic_do_not_disturb_on_24dp; + case AutomaticZenRule.TYPE_SCHEDULE_CALENDAR -> R.drawable.ic_do_not_disturb_on_24dp; + case AutomaticZenRule.TYPE_BEDTIME -> R.drawable.ic_do_not_disturb_on_24dp; + case AutomaticZenRule.TYPE_DRIVING -> R.drawable.ic_do_not_disturb_on_24dp; + case AutomaticZenRule.TYPE_IMMERSIVE -> R.drawable.ic_do_not_disturb_on_24dp; + case AutomaticZenRule.TYPE_THEATER -> R.drawable.ic_do_not_disturb_on_24dp; + case AutomaticZenRule.TYPE_MANAGED -> R.drawable.ic_do_not_disturb_on_24dp; + default -> R.drawable.ic_do_not_disturb_on_24dp; + }; + return Futures.immediateFuture(requireNonNull(context.getDrawable(iconResIdFromType))); + } + + @NonNull + public ZenPolicy getPolicy() { + switch (mRule.getInterruptionFilter()) { + case INTERRUPTION_FILTER_PRIORITY: + return requireNonNull(mRule.getZenPolicy()); + + case NotificationManager.INTERRUPTION_FILTER_ALL: + return POLICY_INTERRUPTION_FILTER_ALL; + + case NotificationManager.INTERRUPTION_FILTER_ALARMS: + return POLICY_INTERRUPTION_FILTER_ALARMS; + + case NotificationManager.INTERRUPTION_FILTER_NONE: + return POLICY_INTERRUPTION_FILTER_NONE; + + case NotificationManager.INTERRUPTION_FILTER_UNKNOWN: + default: + Log.wtf(TAG, "Rule " + mId + " with unexpected interruptionFilter " + + mRule.getInterruptionFilter()); + return requireNonNull(mRule.getZenPolicy()); + } + } + + public void setZenPolicy(@NonNull ZenPolicy policy) { + // TODO: b/331267485 - A policy with apps=ALL should be mapped to INTERRUPTION_FILTER_ALL. + if (mRule.getInterruptionFilter() != INTERRUPTION_FILTER_PRIORITY) { + ZenPolicy currentPolicy = getPolicy(); + if (!currentPolicy.equals(policy)) { + // If policy is customized from any of the "special" ones, make the rule PRIORITY. + mRule.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY); + } + } + mRule.setZenPolicy(policy); + } + + public boolean canBeDeleted() { + return !mIsManualDnd; + } + + public boolean isManualDnd() { + return mIsManualDnd; + } + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof ZenMode other + && mId.equals(other.mId) + && mRule.equals(other.mRule); + } + + @Override + public int hashCode() { + return Objects.hash(mId, mRule); + } + + @Override + public String toString() { + return mId + " -> " + mRule; + } +} diff --git a/src/com/android/settings/notification/modes/ZenModesBackend.java b/src/com/android/settings/notification/modes/ZenModesBackend.java new file mode 100644 index 00000000000..388f13b15f8 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModesBackend.java @@ -0,0 +1,161 @@ +/* + * 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.settings.notification.modes; + +import static java.util.Objects.requireNonNull; + +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.AutomaticZenRule; +import android.app.NotificationManager; +import android.content.Context; +import android.net.Uri; +import android.provider.Settings; +import android.service.notification.Condition; +import android.service.notification.ZenAdapters; +import android.service.notification.ZenModeConfig; + +import com.android.settings.R; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Class used for Settings-NMS interactions related to Mode management. + * + *

This class converts {@link AutomaticZenRule} instances, as well as the manual zen mode, + * into the unified {@link ZenMode} format. + */ +class ZenModesBackend { + + private static final String TAG = "ZenModeBackend"; + + @Nullable // Until first usage + private static ZenModesBackend sInstance; + + private final NotificationManager mNotificationManager; + + private final Context mContext; + + static ZenModesBackend getInstance(Context context) { + if (sInstance == null) { + sInstance = new ZenModesBackend(context.getApplicationContext()); + } + return sInstance; + } + + ZenModesBackend(Context context) { + mContext = context; + mNotificationManager = context.getSystemService(NotificationManager.class); + } + + List getModes() { + ArrayList modes = new ArrayList<>(); + modes.add(getManualDndMode()); + + Map zenRules = mNotificationManager.getAutomaticZenRules(); + for (Map.Entry zenRuleEntry : zenRules.entrySet()) { + modes.add(new ZenMode(zenRuleEntry.getKey(), zenRuleEntry.getValue())); + } + + // TODO: b/331429435 - Sort modes. + return modes; + } + + @Nullable + ZenMode getMode(String id) { + if (ZenMode.MANUAL_DND_MODE_ID.equals(id)) { + return getManualDndMode(); + } else { + AutomaticZenRule rule = mNotificationManager.getAutomaticZenRule(id); + return rule != null ? new ZenMode(id, rule) : null; + } + } + + private ZenMode getManualDndMode() { + // TODO: b/333530553 - Read ZenDeviceEffects of manual DND. + // TODO: b/333682392 - Replace with final strings for name & trigger description + AutomaticZenRule manualDndRule = new AutomaticZenRule.Builder( + mContext.getString(R.string.zen_mode_settings_title), Uri.EMPTY) + .setType(AutomaticZenRule.TYPE_OTHER) + .setZenPolicy(ZenAdapters.notificationPolicyToZenPolicy( + mNotificationManager.getNotificationPolicy())) + .setDeviceEffects(null) + .setTriggerDescription(mContext.getString(R.string.zen_mode_settings_summary)) + .setManualInvocationAllowed(true) + .setConfigurationActivity(null) // No further settings + .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY) + .build(); + + return ZenMode.manualDndMode(manualDndRule); + } + + void updateMode(ZenMode mode) { + if (mode.isManualDnd()) { + NotificationManager.Policy dndPolicy = + new ZenModeConfig().toNotificationPolicy(requireNonNull(mode.getPolicy())); + mNotificationManager.setNotificationPolicy(dndPolicy, /* fromUser= */ true); + // TODO: b/333530553 - Update ZenDeviceEffects of the manual DND too. + } else { + mNotificationManager.updateAutomaticZenRule(mode.getId(), mode.getRule(), + /* fromUser= */ true); + } + } + + void activateMode(ZenMode mode, @Nullable Duration forDuration) { + if (mode.isManualDnd()) { + Uri durationConditionId = null; + if (forDuration != null) { + durationConditionId = ZenModeConfig.toTimeCondition(mContext, + (int) forDuration.toMinutes(), ActivityManager.getCurrentUser(), true).id; + } + mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, + durationConditionId, TAG, /* fromUser= */ true); + + } else { + if (forDuration != null) { + throw new IllegalArgumentException( + "Only the manual DND mode can be activated for a specific duration"); + } + mNotificationManager.setAutomaticZenRuleState(mode.getId(), + new Condition(mode.getRule().getConditionId(), "", Condition.STATE_TRUE, + Condition.SOURCE_USER_ACTION)); + } + } + + void deactivateMode(ZenMode mode) { + if (mode.isManualDnd()) { + // TODO: b/326061620 - This shouldn't snooze any rules that are active. + mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_OFF, null, TAG, + /* fromUser= */ true); + } else { + // TODO: b/333527800 - This should (potentially) snooze the rule if it was active. + mNotificationManager.setAutomaticZenRuleState(mode.getId(), + new Condition(mode.getRule().getConditionId(), "", Condition.STATE_FALSE, + Condition.SOURCE_USER_ACTION)); + } + } + + void removeMode(ZenMode mode) { + if (!mode.canBeDeleted()) { + throw new IllegalArgumentException("Mode " + mode + " cannot be deleted!"); + } + mNotificationManager.removeAutomaticZenRule(mode.getId(), /* fromUser= */ true); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeTest.java new file mode 100644 index 00000000000..52680ca639c --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeTest.java @@ -0,0 +1,107 @@ +/* + * 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.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS; +import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; +import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE; +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.AutomaticZenRule; +import android.net.Uri; +import android.service.notification.ZenPolicy; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ZenModeTest { + + private static final ZenPolicy ZEN_POLICY = new ZenPolicy.Builder().allowAllSounds().build(); + + private static final AutomaticZenRule ZEN_RULE = + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(ZEN_POLICY) + .build(); + + @Test + public void testBasicMethods() { + ZenMode zenMode = new ZenMode("id", ZEN_RULE); + + assertThat(zenMode.getId()).isEqualTo("id"); + assertThat(zenMode.getRule()).isEqualTo(ZEN_RULE); + assertThat(zenMode.isManualDnd()).isFalse(); + assertThat(zenMode.canBeDeleted()).isTrue(); + } + + @Test + public void getZenPolicy_interruptionFilterPriority_returnsZenPolicy() { + ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(ZEN_POLICY) + .build()); + + assertThat(zenMode.getPolicy()).isEqualTo(ZEN_POLICY); + } + + @Test + public void getZenPolicy_interruptionFilterAll_returnsPolicyAllowingAll() { + ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY) + .setInterruptionFilter(INTERRUPTION_FILTER_ALL) + .setZenPolicy(ZEN_POLICY) // should be ignored + .build()); + + assertThat(zenMode.getPolicy()).isEqualTo( + new ZenPolicy.Builder().allowAllSounds().showAllVisualEffects().build()); + } + + @Test + public void getZenPolicy_interruptionFilterAlarms_returnsPolicyAllowingAlarms() { + ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY) + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setZenPolicy(ZEN_POLICY) // should be ignored + .build()); + + assertThat(zenMode.getPolicy()).isEqualTo( + new ZenPolicy.Builder() + .disallowAllSounds() + .allowAlarms(true) + .allowMedia(true) + .allowPriorityChannels(false) + .build()); + } + + @Test + public void getZenPolicy_interruptionFilterNone_returnsPolicyAllowingNothing() { + ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY) + .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setZenPolicy(ZEN_POLICY) // should be ignored + .build()); + + assertThat(zenMode.getPolicy()).isEqualTo( + new ZenPolicy.Builder() + .disallowAllSounds() + .hideAllVisualEffects() + .allowPriorityChannels(false) + .build()); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesBackendTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesBackendTest.java new file mode 100644 index 00000000000..06ce80b24d2 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesBackendTest.java @@ -0,0 +1,243 @@ +/* + * 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.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AutomaticZenRule; +import android.app.NotificationManager; +import android.app.NotificationManager.Policy; +import android.content.Context; +import android.net.Uri; +import android.provider.Settings; +import android.service.notification.Condition; +import android.service.notification.ZenAdapters; +import android.service.notification.ZenModeConfig; +import android.service.notification.ZenPolicy; + +import com.android.settings.R; + +import com.google.common.collect.ImmutableMap; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowApplication; + +import java.time.Duration; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class ZenModesBackendTest { + + private static final AutomaticZenRule ZEN_RULE = + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build()) + .build(); + + private static final ZenMode MANUAL_DND_MODE = ZenMode.manualDndMode( + new AutomaticZenRule.Builder("Do Not Disturb", Uri.EMPTY) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build()) + .build()); + + private static final ZenMode ZEN_RULE_MODE = new ZenMode("rule", ZEN_RULE); + + @Mock + private NotificationManager mNm; + + private Context mContext; + private ZenModesBackend mBackend; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + ShadowApplication shadowApplication = ShadowApplication.getInstance(); + shadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNm); + + mContext = RuntimeEnvironment.application; + mBackend = new ZenModesBackend(mContext); + } + + @Test + public void getModes_containsManualDndAndZenRules() { + AutomaticZenRule rule2 = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build()) + .build(); + Policy dndPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS, + Policy.PRIORITY_SENDERS_CONTACTS, Policy.PRIORITY_SENDERS_CONTACTS); + when(mNm.getAutomaticZenRules()).thenReturn( + ImmutableMap.of("rule1", ZEN_RULE, "rule2", rule2)); + when(mNm.getNotificationPolicy()).thenReturn(dndPolicy); + + List modes = mBackend.getModes(); + + assertThat(modes).containsExactly( + ZenMode.manualDndMode( + new AutomaticZenRule.Builder( + mContext.getString(R.string.zen_mode_settings_title), Uri.EMPTY) + .setType(AutomaticZenRule.TYPE_OTHER) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(ZenAdapters.notificationPolicyToZenPolicy(dndPolicy)) + .setTriggerDescription( + mContext.getString(R.string.zen_mode_settings_summary)) + .setManualInvocationAllowed(true) + .build()), + new ZenMode("rule1", ZEN_RULE), + new ZenMode("rule2", rule2)) + .inOrder(); + } + + @Test + public void getMode_manualDnd_returnsMode() { + Policy dndPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS, + Policy.PRIORITY_SENDERS_CONTACTS, Policy.PRIORITY_SENDERS_CONTACTS); + when(mNm.getNotificationPolicy()).thenReturn(dndPolicy); + + ZenMode mode = mBackend.getMode(ZenMode.MANUAL_DND_MODE_ID); + + assertThat(mode).isEqualTo( + ZenMode.manualDndMode( + new AutomaticZenRule.Builder( + mContext.getString(R.string.zen_mode_settings_title), Uri.EMPTY) + .setType(AutomaticZenRule.TYPE_OTHER) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(ZenAdapters.notificationPolicyToZenPolicy(dndPolicy)) + .setTriggerDescription( + mContext.getString(R.string.zen_mode_settings_summary)) + .setManualInvocationAllowed(true) + .build())); + } + + @Test + public void getMode_zenRule_returnsMode() { + when(mNm.getAutomaticZenRule(eq("rule"))).thenReturn(ZEN_RULE); + + ZenMode mode = mBackend.getMode("rule"); + + assertThat(mode).isEqualTo(new ZenMode("rule", ZEN_RULE)); + } + + @Test + public void getMode_missingRule_returnsNull() { + when(mNm.getAutomaticZenRule(any())).thenReturn(null); + + ZenMode mode = mBackend.getMode("rule"); + + assertThat(mode).isNull(); + verify(mNm).getAutomaticZenRule(eq("rule")); + } + + @Test + public void updateMode_manualDnd_setsNotificationPolicy() { + ZenMode manualDnd = ZenMode.manualDndMode( + new AutomaticZenRule.Builder("DND", Uri.EMPTY) + .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build()) + .build()); + + mBackend.updateMode(manualDnd); + + verify(mNm).setNotificationPolicy(eq(new ZenModeConfig().toNotificationPolicy( + new ZenPolicy.Builder().allowAllSounds().build())), eq(true)); + } + + @Test + public void updateMode_zenRule_updatesRule() { + ZenMode ruleMode = new ZenMode("rule", ZEN_RULE); + + mBackend.updateMode(ruleMode); + + verify(mNm).updateAutomaticZenRule(eq("rule"), eq(ZEN_RULE), eq(true)); + } + + @Test + public void activateMode_manualDnd_setsZenModeImportant() { + mBackend.activateMode(MANUAL_DND_MODE, null); + + verify(mNm).setZenMode(eq(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null), + any(), eq(true)); + } + + @Test + public void activateMode_manualDndWithDuration_setsZenModeImportantWithCondition() { + mBackend.activateMode(MANUAL_DND_MODE, Duration.ofMinutes(30)); + + verify(mNm).setZenMode(eq(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS), + eq(ZenModeConfig.toTimeCondition(mContext, 30, 0, true).id), + any(), + eq(true)); + } + + @Test + public void activateMode_zenRule_setsRuleStateActive() { + mBackend.activateMode(ZEN_RULE_MODE, null); + + verify(mNm).setAutomaticZenRuleState(eq(ZEN_RULE_MODE.getId()), + eq(new Condition(ZEN_RULE.getConditionId(), "", Condition.STATE_TRUE, + Condition.SOURCE_USER_ACTION))); + } + + @Test + public void activateMode_zenRuleWithDuration_fails() { + assertThrows(IllegalArgumentException.class, + () -> mBackend.activateMode(ZEN_RULE_MODE, Duration.ofMinutes(30))); + } + + @Test + public void deactivateMode_manualDnd_setsZenModeOff() { + mBackend.deactivateMode(MANUAL_DND_MODE); + + verify(mNm).setZenMode(eq(Settings.Global.ZEN_MODE_OFF), eq(null), any(), eq(true)); + } + + @Test + public void deactivateMode_zenRule_setsRuleStateInactive() { + mBackend.deactivateMode(ZEN_RULE_MODE); + + verify(mNm).setAutomaticZenRuleState(eq(ZEN_RULE_MODE.getId()), + eq(new Condition(ZEN_RULE.getConditionId(), "", Condition.STATE_FALSE, + Condition.SOURCE_USER_ACTION))); + } + + @Test + public void removeMode_zenRule_deletesRule() { + mBackend.removeMode(ZEN_RULE_MODE); + + verify(mNm).removeAutomaticZenRule(ZEN_RULE_MODE.getId(), true); + } + + @Test + public void removeMode_manualDnd_fails() { + assertThrows(IllegalArgumentException.class, () -> mBackend.removeMode(MANUAL_DND_MODE)); + } +}