Merge "Move ZenModesBackend to SettingsLib." into main
diff --git a/packages/SettingsLib/res/drawable/ic_do_not_disturb_on_24dp.xml b/packages/SettingsLib/res/drawable/ic_do_not_disturb_on_24dp.xml
new file mode 100644
index 0000000..06c0d8c
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_do_not_disturb_on_24dp.xml
@@ -0,0 +1,28 @@
+<!--
+ 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorControlNormal">
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10c5.52,0 10,-4.48 10,-10C22,6.48 17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8c0,-4.41 3.59,-8 8,-8c4.41,0 8,3.59 8,8C20,16.41 16.41,20 12,20z"/>
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M7,11h10v2h-10z"/>
+</vector>
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index d373201..27c386e 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1361,6 +1361,9 @@
<!-- Keywords for setting screen for controlling apps that can schedule alarms [CHAR LIMIT=100] -->
<string name="keywords_alarms_and_reminders">schedule, alarm, reminder, clock</string>
+ <!-- Sound: Title for the Do not Disturb option and associated settings page. [CHAR LIMIT=50]-->
+ <string name="zen_mode_settings_title">Do Not Disturb</string>
+
<!-- Do not disturb: Label for button in enable zen dialog that will turn on zen mode. [CHAR LIMIT=30] -->
<string name="zen_mode_enable_dialog_turn_on">Turn on</string>
<!-- Do not disturb: Title for the Do not Disturb dialog to turn on Do not disturb. [CHAR LIMIT=50]-->
@@ -1387,6 +1390,9 @@
<!-- Do not disturb: Duration option to always have DND on until it is manually turned off [CHAR LIMIT=60] -->
<string name="zen_mode_forever">Until you turn off</string>
+ <!-- [CHAR LIMIT=50] Zen mode settings: placeholder for a Contact name when the name is empty -->
+ <string name="zen_mode_starred_contacts_empty_name">(No name)</string>
+
<!-- time label for event have that happened very recently [CHAR LIMIT=60] -->
<string name="time_unit_just_now">Just now</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java
new file mode 100644
index 0000000..3f19830
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java
@@ -0,0 +1,160 @@
+/*
+ * 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 static com.google.common.util.concurrent.Futures.immediateFuture;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.Nullable;
+import android.app.AutomaticZenRule;
+import android.content.Context;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.service.notification.SystemZenRules;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.LruCache;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.content.res.AppCompatResources;
+
+import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class ZenIconLoader {
+
+ private static final String TAG = "ZenIconLoader";
+
+ private static final Drawable MISSING = new ColorDrawable();
+
+ @Nullable // Until first usage
+ private static ZenIconLoader sInstance;
+
+ private final LruCache<String, Drawable> mCache;
+ private final ListeningExecutorService mBackgroundExecutor;
+
+ public static ZenIconLoader getInstance() {
+ if (sInstance == null) {
+ sInstance = new ZenIconLoader();
+ }
+ return sInstance;
+ }
+
+ private ZenIconLoader() {
+ this(Executors.newFixedThreadPool(4));
+ }
+
+ @VisibleForTesting
+ ZenIconLoader(ExecutorService backgroundExecutor) {
+ mCache = new LruCache<>(50);
+ mBackgroundExecutor =
+ MoreExecutors.listeningDecorator(backgroundExecutor);
+ }
+
+ @NonNull
+ ListenableFuture<Drawable> getIcon(Context context, @NonNull AutomaticZenRule rule) {
+ if (rule.getIconResId() == 0) {
+ return Futures.immediateFuture(getFallbackIcon(context, rule.getType()));
+ }
+
+ return FluentFuture.from(loadIcon(context, rule.getPackageName(), rule.getIconResId()))
+ .transform(icon ->
+ icon != null ? icon : getFallbackIcon(context, rule.getType()),
+ MoreExecutors.directExecutor());
+ }
+
+ @NonNull
+ private ListenableFuture</* @Nullable */ Drawable> loadIcon(Context context, String pkg,
+ int iconResId) {
+ String cacheKey = pkg + ":" + iconResId;
+ synchronized (mCache) {
+ Drawable cachedValue = mCache.get(cacheKey);
+ if (cachedValue != null) {
+ return immediateFuture(cachedValue != MISSING ? cachedValue : null);
+ }
+ }
+
+ return FluentFuture.from(mBackgroundExecutor.submit(() -> {
+ if (TextUtils.isEmpty(pkg) || SystemZenRules.PACKAGE_ANDROID.equals(pkg)) {
+ return context.getDrawable(iconResId);
+ } else {
+ Context appContext = context.createPackageContext(pkg, 0);
+ Drawable appDrawable = AppCompatResources.getDrawable(appContext, iconResId);
+ return getMonochromeIconIfPresent(appDrawable);
+ }
+ })).catching(Exception.class, ex -> {
+ // If we cannot resolve the icon, then store MISSING in the cache below, so
+ // we don't try again.
+ Log.e(TAG, "Error while loading icon " + cacheKey, ex);
+ return null;
+ }, MoreExecutors.directExecutor()).transform(drawable -> {
+ synchronized (mCache) {
+ mCache.put(cacheKey, drawable != null ? drawable : MISSING);
+ }
+ return drawable;
+ }, MoreExecutors.directExecutor());
+ }
+
+ private static Drawable getFallbackIcon(Context context, int ruleType) {
+ int iconResIdFromType = switch (ruleType) {
+ case AutomaticZenRule.TYPE_UNKNOWN ->
+ com.android.internal.R.drawable.ic_zen_mode_type_unknown;
+ case AutomaticZenRule.TYPE_OTHER ->
+ com.android.internal.R.drawable.ic_zen_mode_type_other;
+ case AutomaticZenRule.TYPE_SCHEDULE_TIME ->
+ com.android.internal.R.drawable.ic_zen_mode_type_schedule_time;
+ case AutomaticZenRule.TYPE_SCHEDULE_CALENDAR ->
+ com.android.internal.R.drawable.ic_zen_mode_type_schedule_calendar;
+ case AutomaticZenRule.TYPE_BEDTIME ->
+ com.android.internal.R.drawable.ic_zen_mode_type_bedtime;
+ case AutomaticZenRule.TYPE_DRIVING ->
+ com.android.internal.R.drawable.ic_zen_mode_type_driving;
+ case AutomaticZenRule.TYPE_IMMERSIVE ->
+ com.android.internal.R.drawable.ic_zen_mode_type_immersive;
+ case AutomaticZenRule.TYPE_THEATER ->
+ com.android.internal.R.drawable.ic_zen_mode_type_theater;
+ case AutomaticZenRule.TYPE_MANAGED ->
+ com.android.internal.R.drawable.ic_zen_mode_type_managed;
+ default -> com.android.internal.R.drawable.ic_zen_mode_type_unknown;
+ };
+ return requireNonNull(context.getDrawable(iconResIdFromType));
+ }
+
+ private static Drawable getMonochromeIconIfPresent(Drawable icon) {
+ // For created rules, the app should've provided a monochrome Drawable. However, implicit
+ // rules have the app's icon, which is not -- but might have a monochrome layer. Thus
+ // we choose it, if present.
+ if (icon instanceof AdaptiveIconDrawable adaptiveIcon) {
+ if (adaptiveIcon.getMonochrome() != null) {
+ // Wrap with negative inset => scale icon (inspired from BaseIconFactory)
+ return new InsetDrawable(adaptiveIcon.getMonochrome(),
+ -2.0f * AdaptiveIconDrawable.getExtraInsetFraction());
+ }
+ }
+ return icon;
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
new file mode 100644
index 0000000..598db2a9
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
@@ -0,0 +1,257 @@
+/*
+ * 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 static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleEvent;
+import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleTime;
+import static android.service.notification.ZenModeConfig.tryParseEventConditionId;
+import static android.service.notification.ZenModeConfig.tryParseScheduleConditionId;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+import android.app.AutomaticZenRule;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.service.notification.SystemZenRules;
+import android.service.notification.ZenDeviceEffects;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenPolicy;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.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.
+ *
+ * <p>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.
+ */
+public class ZenMode {
+
+ private static final String TAG = "ZenMode";
+
+ public static final String MANUAL_DND_MODE_ID = "manual_dnd";
+
+ // 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 AutomaticZenRule mRule;
+ private final boolean mIsActive;
+ private final boolean mIsManualDnd;
+
+ public ZenMode(String id, AutomaticZenRule rule, boolean isActive) {
+ this(id, rule, isActive, false);
+ }
+
+ private ZenMode(String id, AutomaticZenRule rule, boolean isActive, boolean isManualDnd) {
+ mId = id;
+ mRule = rule;
+ mIsActive = isActive;
+ mIsManualDnd = isManualDnd;
+ }
+
+ public static ZenMode manualDndMode(AutomaticZenRule manualRule, boolean isActive) {
+ return new ZenMode(MANUAL_DND_MODE_ID, manualRule, isActive, true);
+ }
+
+ @NonNull
+ public String getId() {
+ return mId;
+ }
+
+ @NonNull
+ public AutomaticZenRule getRule() {
+ return mRule;
+ }
+
+ @NonNull
+ public ListenableFuture<Drawable> getIcon(@NonNull Context context,
+ @NonNull ZenIconLoader iconLoader) {
+ if (mIsManualDnd) {
+ return Futures.immediateFuture(requireNonNull(
+ context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
+ }
+
+ return iconLoader.getIcon(context, mRule);
+ }
+
+ @NonNull
+ public ZenPolicy getPolicy() {
+ switch (mRule.getInterruptionFilter()) {
+ case INTERRUPTION_FILTER_PRIORITY:
+ case NotificationManager.INTERRUPTION_FILTER_ALL:
+ return requireNonNull(mRule.getZenPolicy());
+
+ 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());
+ }
+ }
+
+ /**
+ * Updates the {@link ZenPolicy} of the associated {@link AutomaticZenRule} based on the
+ * supplied policy. In some cases this involves conversions, so that the following call
+ * to {@link #getPolicy} might return a different policy from the one supplied here.
+ */
+ @SuppressLint("WrongConstant")
+ public void setPolicy(@NonNull ZenPolicy policy) {
+ ZenPolicy currentPolicy = getPolicy();
+ if (currentPolicy.equals(policy)) {
+ return;
+ }
+
+ if (mRule.getInterruptionFilter() == INTERRUPTION_FILTER_ALL) {
+ Log.wtf(TAG, "Able to change policy without filtering being enabled");
+ }
+
+ // If policy is customized from any of the "special" ones, make the rule PRIORITY.
+ if (mRule.getInterruptionFilter() != INTERRUPTION_FILTER_PRIORITY) {
+ mRule.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY);
+ }
+ mRule.setZenPolicy(policy);
+ }
+
+ @NonNull
+ public ZenDeviceEffects getDeviceEffects() {
+ return mRule.getDeviceEffects() != null
+ ? mRule.getDeviceEffects()
+ : new ZenDeviceEffects.Builder().build();
+ }
+
+ public void setCustomModeConditionId(Context context, Uri conditionId) {
+ checkState(SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName()),
+ "Trying to change condition of non-system-owned rule %s (to %s)",
+ mRule, conditionId);
+
+ Uri oldCondition = mRule.getConditionId();
+ mRule.setConditionId(conditionId);
+
+ ZenModeConfig.ScheduleInfo scheduleInfo = tryParseScheduleConditionId(conditionId);
+ if (scheduleInfo != null) {
+ mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_TIME);
+ mRule.setOwner(ZenModeConfig.getScheduleConditionProvider());
+ mRule.setTriggerDescription(
+ getTriggerDescriptionForScheduleTime(context, scheduleInfo));
+ return;
+ }
+
+ ZenModeConfig.EventInfo eventInfo = tryParseEventConditionId(conditionId);
+ if (eventInfo != null) {
+ mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_CALENDAR);
+ mRule.setOwner(ZenModeConfig.getEventConditionProvider());
+ mRule.setTriggerDescription(getTriggerDescriptionForScheduleEvent(context, eventInfo));
+ return;
+ }
+
+ if (ZenModeConfig.isValidCustomManualConditionId(conditionId)) {
+ mRule.setType(AutomaticZenRule.TYPE_OTHER);
+ mRule.setOwner(ZenModeConfig.getCustomManualConditionProvider());
+ mRule.setTriggerDescription("");
+ return;
+ }
+
+ Log.wtf(TAG, String.format(
+ "Changed condition of rule %s (%s -> %s) but cannot recognize which kind of "
+ + "condition it was!",
+ mRule, oldCondition, conditionId));
+ }
+
+ public boolean canEditName() {
+ return !isManualDnd();
+ }
+
+ public boolean canEditIcon() {
+ return !isManualDnd();
+ }
+
+ public boolean canBeDeleted() {
+ return !isManualDnd();
+ }
+
+ public boolean isManualDnd() {
+ return mIsManualDnd;
+ }
+
+ public boolean isActive() {
+ return mIsActive;
+ }
+
+ public boolean isSystemOwned() {
+ return SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName());
+ }
+
+ @AutomaticZenRule.Type
+ public int getType() {
+ return mRule.getType();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ return obj instanceof ZenMode other
+ && mId.equals(other.mId)
+ && mRule.equals(other.mRule)
+ && mIsActive == other.mIsActive;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mId, mRule, mIsActive);
+ }
+
+ @Override
+ public String toString() {
+ return mId + "(" + (mIsActive ? "active" : "inactive") + ") -> " + mRule;
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java
new file mode 100644
index 0000000..d07d743
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java
@@ -0,0 +1,207 @@
+/*
+ * 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.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.ZenModeConfig;
+import android.util.Log;
+
+import com.android.settingslib.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.
+ *
+ * <p>This class converts {@link AutomaticZenRule} instances, as well as the manual zen mode,
+ * into the unified {@link ZenMode} format.
+ */
+public class ZenModesBackend {
+
+ private static final String TAG = "ZenModeBackend";
+
+ @Nullable // Until first usage
+ private static ZenModesBackend sInstance;
+
+ private final NotificationManager mNotificationManager;
+
+ private final Context mContext;
+
+ public 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);
+ }
+
+ public List<ZenMode> getModes() {
+ ArrayList<ZenMode> modes = new ArrayList<>();
+ ZenModeConfig currentConfig = mNotificationManager.getZenModeConfig();
+ modes.add(getManualDndMode(currentConfig));
+
+ Map<String, AutomaticZenRule> zenRules = mNotificationManager.getAutomaticZenRules();
+ for (Map.Entry<String, AutomaticZenRule> zenRuleEntry : zenRules.entrySet()) {
+ String ruleId = zenRuleEntry.getKey();
+ modes.add(new ZenMode(ruleId, zenRuleEntry.getValue(),
+ isRuleActive(ruleId, currentConfig)));
+ }
+
+ modes.sort((l, r) -> {
+ if (l.isManualDnd()) {
+ return -1;
+ } else if (r.isManualDnd()) {
+ return 1;
+ }
+ return l.getRule().getName().compareTo(r.getRule().getName());
+ });
+
+ return modes;
+ }
+
+ @Nullable
+ public ZenMode getMode(String id) {
+ ZenModeConfig currentConfig = mNotificationManager.getZenModeConfig();
+ if (ZenMode.MANUAL_DND_MODE_ID.equals(id)) {
+ return getManualDndMode(currentConfig);
+ } else {
+ AutomaticZenRule rule = mNotificationManager.getAutomaticZenRule(id);
+ if (rule == null) {
+ return null;
+ }
+ return new ZenMode(id, rule, isRuleActive(id, currentConfig));
+ }
+ }
+
+ private ZenMode getManualDndMode(ZenModeConfig config) {
+ ZenModeConfig.ZenRule manualRule = config.manualRule;
+ // TODO: b/333682392 - Replace with final strings for name & trigger description
+ AutomaticZenRule manualDndRule = new AutomaticZenRule.Builder(
+ mContext.getString(R.string.zen_mode_settings_title), manualRule.conditionId)
+ .setType(manualRule.type)
+ .setZenPolicy(manualRule.zenPolicy)
+ .setDeviceEffects(manualRule.zenDeviceEffects)
+ .setManualInvocationAllowed(manualRule.allowManualInvocation)
+ .setConfigurationActivity(null) // No further settings
+ .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
+ .build();
+
+ return ZenMode.manualDndMode(manualDndRule, config != null && config.isManualActive());
+ }
+
+ private static boolean isRuleActive(String id, ZenModeConfig config) {
+ if (config == null) {
+ // shouldn't happen if the config is coming from NM, but be safe
+ return false;
+ }
+ ZenModeConfig.ZenRule configRule = config.automaticRules.get(id);
+ return configRule != null && configRule.isAutomaticActive();
+ }
+
+ public void updateMode(ZenMode mode) {
+ if (mode.isManualDnd()) {
+ try {
+ NotificationManager.Policy dndPolicy =
+ new ZenModeConfig().toNotificationPolicy(mode.getPolicy());
+ mNotificationManager.setNotificationPolicy(dndPolicy, /* fromUser= */ true);
+
+ mNotificationManager.setManualZenRuleDeviceEffects(
+ mode.getRule().getDeviceEffects());
+ } catch (Exception e) {
+ Log.w(TAG, "Error updating manual mode", e);
+ }
+ } else {
+ mNotificationManager.updateAutomaticZenRule(mode.getId(), mode.getRule(),
+ /* fromUser= */ true);
+ }
+ }
+
+ public 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));
+ }
+ }
+
+ public void deactivateMode(ZenMode mode) {
+ if (mode.isManualDnd()) {
+ // When calling with fromUser=true this will not snooze other modes.
+ 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));
+ }
+ }
+
+ public void removeMode(ZenMode mode) {
+ if (!mode.canBeDeleted()) {
+ throw new IllegalArgumentException("Mode " + mode + " cannot be deleted!");
+ }
+ mNotificationManager.removeAutomaticZenRule(mode.getId(), /* fromUser= */ true);
+ }
+
+ /**
+ * Creates a new custom mode with the provided {@code name}. The mode will be "manual" (i.e.
+ * not have a schedule), this can be later updated by the user in the mode settings page.
+ *
+ * @return the created mode. Only {@code null} if creation failed due to an internal error
+ */
+ @Nullable
+ public ZenMode addCustomMode(String name) {
+ AutomaticZenRule rule = new AutomaticZenRule.Builder(name,
+ ZenModeConfig.toCustomManualConditionId())
+ .setPackage(ZenModeConfig.getCustomManualConditionProvider().getPackageName())
+ .setType(AutomaticZenRule.TYPE_OTHER)
+ .setOwner(ZenModeConfig.getCustomManualConditionProvider())
+ .setManualInvocationAllowed(true)
+ .build();
+
+ String ruleId = mNotificationManager.addAutomaticZenRule(rule);
+ return getMode(ruleId);
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenIconLoaderTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenIconLoaderTest.java
new file mode 100644
index 0000000..20461e3
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenIconLoaderTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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 static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AutomaticZenRule;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.service.notification.ZenPolicy;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class ZenIconLoaderTest {
+
+ private Context mContext;
+ private ZenIconLoader mLoader;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mLoader = new ZenIconLoader(MoreExecutors.newDirectExecutorService());
+ }
+
+ @Test
+ public void getIcon_systemOwnedRuleWithIcon_loads() throws Exception {
+ AutomaticZenRule systemRule = newRuleBuilder()
+ .setPackage("android")
+ .setIconResId(android.R.drawable.ic_media_play)
+ .build();
+
+ ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, systemRule);
+ assertThat(loadFuture.isDone()).isTrue();
+ assertThat(loadFuture.get()).isNotNull();
+ }
+
+ @Test
+ public void getIcon_ruleWithoutSpecificIcon_loadsFallback() throws Exception {
+ AutomaticZenRule rule = newRuleBuilder()
+ .setType(AutomaticZenRule.TYPE_DRIVING)
+ .setPackage("com.blah")
+ .build();
+
+ ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, rule);
+ assertThat(loadFuture.isDone()).isTrue();
+ assertThat(loadFuture.get()).isNotNull();
+ }
+
+ @Test
+ public void getIcon_ruleWithAppIconWithLoadFailure_loadsFallback() throws Exception {
+ AutomaticZenRule rule = newRuleBuilder()
+ .setType(AutomaticZenRule.TYPE_DRIVING)
+ .setPackage("com.blah")
+ .setIconResId(-123456)
+ .build();
+
+ ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, rule);
+ assertThat(loadFuture.get()).isNotNull();
+ }
+
+ private static AutomaticZenRule.Builder newRuleBuilder() {
+ return new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder().build());
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
new file mode 100644
index 0000000..43aba45
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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 static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS;
+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, true);
+
+ assertThat(zenMode.getId()).isEqualTo("id");
+ assertThat(zenMode.getRule()).isEqualTo(ZEN_RULE);
+ assertThat(zenMode.isManualDnd()).isFalse();
+ assertThat(zenMode.canBeDeleted()).isTrue();
+ assertThat(zenMode.isActive()).isTrue();
+
+ ZenMode manualMode = ZenMode.manualDndMode(ZEN_RULE, false);
+ assertThat(manualMode.getId()).isEqualTo(ZenMode.MANUAL_DND_MODE_ID);
+ assertThat(manualMode.isManualDnd()).isTrue();
+ assertThat(manualMode.canBeDeleted()).isFalse();
+ assertThat(manualMode.isActive()).isFalse();
+ }
+
+ @Test
+ public void getPolicy_interruptionFilterPriority_returnsZenPolicy() {
+ ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(ZEN_POLICY)
+ .build(), false);
+
+ assertThat(zenMode.getPolicy()).isEqualTo(ZEN_POLICY);
+ }
+
+ @Test
+ public void getPolicy_interruptionFilterAlarms_returnsPolicyAllowingAlarms() {
+ ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+ .setZenPolicy(ZEN_POLICY) // should be ignored
+ .build(), false);
+
+ assertThat(zenMode.getPolicy()).isEqualTo(
+ new ZenPolicy.Builder()
+ .disallowAllSounds()
+ .allowAlarms(true)
+ .allowMedia(true)
+ .allowPriorityChannels(false)
+ .build());
+ }
+
+ @Test
+ public void getPolicy_interruptionFilterNone_returnsPolicyAllowingNothing() {
+ ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
+ .setZenPolicy(ZEN_POLICY) // should be ignored
+ .build(), false);
+
+ assertThat(zenMode.getPolicy()).isEqualTo(
+ new ZenPolicy.Builder()
+ .disallowAllSounds()
+ .hideAllVisualEffects()
+ .allowPriorityChannels(false)
+ .build());
+ }
+
+ @Test
+ public void setPolicy_setsInterruptionFilterPriority() {
+ ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+ .build(), false);
+
+ zenMode.setPolicy(ZEN_POLICY);
+
+ assertThat(zenMode.getRule().getInterruptionFilter()).isEqualTo(
+ INTERRUPTION_FILTER_PRIORITY);
+ assertThat(zenMode.getPolicy()).isEqualTo(ZEN_POLICY);
+ assertThat(zenMode.getRule().getZenPolicy()).isEqualTo(ZEN_POLICY);
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java
new file mode 100644
index 0000000..7c7972d
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java
@@ -0,0 +1,365 @@
+/*
+ * 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 static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+import static android.provider.Settings.Global.ZEN_MODE_OFF;
+import static android.service.notification.Condition.SOURCE_UNKNOWN;
+import static android.service.notification.Condition.STATE_FALSE;
+import static android.service.notification.Condition.STATE_TRUE;
+import static android.service.notification.ZenPolicy.STATE_ALLOW;
+
+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.Flags;
+import android.app.NotificationManager;
+import android.app.NotificationManager.Policy;
+import android.content.Context;
+import android.net.Uri;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+import android.service.notification.Condition;
+import android.service.notification.ZenAdapters;
+import android.service.notification.ZenDeviceEffects;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenPolicy;
+
+import com.android.settingslib.R;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Before;
+import org.junit.Rule;
+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)
+@EnableFlags(Flags.FLAG_MODES_UI)
+public class ZenModesBackendTest {
+
+ private static final String ZEN_RULE_ID = "rule";
+ 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 AutomaticZenRule MANUAL_DND_RULE =
+ new AutomaticZenRule.Builder("Do Not Disturb", Uri.EMPTY)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+ .build();
+
+ @Mock
+ private NotificationManager mNm;
+
+ private Context mContext;
+ private ZenModesBackend mBackend;
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(
+ SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT);
+
+ // Helper methods to add active/inactive rule state to a config. Returns a copy.
+ private ZenModeConfig configWithManualRule(ZenModeConfig base, boolean active) {
+ ZenModeConfig out = base.copy();
+
+ if (active) {
+ out.manualRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+ out.manualRule.condition =
+ new Condition(out.manualRule.conditionId, "", STATE_TRUE, SOURCE_UNKNOWN);
+ } else {
+ out.manualRule.zenMode = ZEN_MODE_OFF;
+ out.manualRule.condition =
+ new Condition(out.manualRule.conditionId, "", STATE_FALSE, SOURCE_UNKNOWN);
+ }
+ return out;
+ }
+
+ private ZenModeConfig configWithRule(ZenModeConfig base, String ruleId, AutomaticZenRule rule,
+ boolean active) {
+ ZenModeConfig out = base.copy();
+
+ // Note that there are many other fields of zenRule, but here we only set the ones
+ // relevant to determining whether or not it is active.
+ ZenModeConfig.ZenRule zenRule = new ZenModeConfig.ZenRule();
+ zenRule.pkg = "package";
+ zenRule.enabled = active;
+ zenRule.snoozing = false;
+ zenRule.condition = new Condition(rule.getConditionId(), "",
+ active ? Condition.STATE_TRUE : Condition.STATE_FALSE,
+ Condition.SOURCE_USER_ACTION);
+ out.automaticRules.put(ruleId, zenRule);
+
+ return out;
+ }
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ ShadowApplication shadowApplication = ShadowApplication.getInstance();
+ shadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNm);
+
+ mContext = RuntimeEnvironment.application;
+ mBackend = new ZenModesBackend(mContext);
+
+ // Default catch-all case with no data. This isn't realistic, but tests below that rely
+ // on the config to get data on rules active will create those individually.
+ when(mNm.getZenModeConfig()).thenReturn(new ZenModeConfig());
+ }
+
+ @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));
+ ZenModeConfig config = new ZenModeConfig();
+ config.applyNotificationPolicy(dndPolicy);
+ assertThat(config.manualRule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_ALLOW);
+ when(mNm.getZenModeConfig()).thenReturn(config);
+
+ List<ZenMode> modes = mBackend.getModes();
+
+ // all modes exist, but none of them are currently active
+ ZenPolicy zenPolicy = ZenAdapters.notificationPolicyToZenPolicy(dndPolicy);
+ 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(zenPolicy)
+ .setManualInvocationAllowed(true)
+ .build(),
+ false),
+ new ZenMode("rule2", rule2, false),
+ new ZenMode("rule1", ZEN_RULE, false))
+ .inOrder();
+ }
+
+ @Test
+ public void getMode_manualDnd_returnsMode() {
+ Policy dndPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS,
+ Policy.PRIORITY_SENDERS_CONTACTS, Policy.PRIORITY_SENDERS_CONTACTS);
+ ZenModeConfig config = new ZenModeConfig();
+ config.applyNotificationPolicy(dndPolicy);
+ when(mNm.getZenModeConfig()).thenReturn(config);
+
+ 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))
+ .setManualInvocationAllowed(true)
+ .build(), false));
+ }
+
+ @Test
+ public void getMode_zenRule_returnsMode() {
+ when(mNm.getAutomaticZenRule(eq(ZEN_RULE_ID))).thenReturn(ZEN_RULE);
+
+ ZenMode mode = mBackend.getMode(ZEN_RULE_ID);
+
+ assertThat(mode).isEqualTo(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false));
+ }
+
+ @Test
+ public void getMode_missingRule_returnsNull() {
+ when(mNm.getAutomaticZenRule(any())).thenReturn(null);
+
+ ZenMode mode = mBackend.getMode(ZEN_RULE_ID);
+
+ assertThat(mode).isNull();
+ verify(mNm).getAutomaticZenRule(eq(ZEN_RULE_ID));
+ }
+
+ @Test
+ public void getMode_manualDnd_returnsCorrectActiveState() {
+ // Set up a base config with an active rule to make sure we're looking at the correct info
+ ZenModeConfig configWithActiveRule = configWithRule(new ZenModeConfig(), ZEN_RULE_ID,
+ ZEN_RULE, true);
+
+ // Equivalent to disallowAllSounds()
+ Policy dndPolicy = new Policy(0, 0, 0);
+ configWithActiveRule.applyNotificationPolicy(dndPolicy);
+ when(mNm.getZenModeConfig()).thenReturn(configWithActiveRule);
+
+ ZenMode mode = mBackend.getMode(ZenMode.MANUAL_DND_MODE_ID);
+
+ // By default, manual rule is inactive
+ assertThat(mode.isActive()).isFalse();
+
+ // Now the returned config will represent the manual rule being active
+ when(mNm.getZenModeConfig()).thenReturn(configWithManualRule(configWithActiveRule, true));
+ ZenMode activeMode = mBackend.getMode(ZenMode.MANUAL_DND_MODE_ID);
+ assertThat(activeMode.isActive()).isTrue();
+ }
+
+ @Test
+ public void getMode_zenRule_returnsCorrectActiveState() {
+ // Set up a base config that has an active manual rule and "rule2", to make sure we're
+ // looking at the correct rule's info.
+ ZenModeConfig configWithActiveRules = configWithRule(
+ configWithManualRule(new ZenModeConfig(), true), // active manual rule
+ "rule2", ZEN_RULE, true); // active rule 2
+
+ when(mNm.getAutomaticZenRule(eq(ZEN_RULE_ID))).thenReturn(ZEN_RULE);
+ when(mNm.getZenModeConfig()).thenReturn(
+ configWithRule(configWithActiveRules, ZEN_RULE_ID, ZEN_RULE, false));
+
+ // Round 1: the current config should indicate that the rule is not active
+ ZenMode mode = mBackend.getMode(ZEN_RULE_ID);
+ assertThat(mode.isActive()).isFalse();
+
+ when(mNm.getZenModeConfig()).thenReturn(
+ configWithRule(configWithActiveRules, ZEN_RULE_ID, ZEN_RULE, true));
+ ZenMode activeMode = mBackend.getMode(ZEN_RULE_ID);
+ assertThat(activeMode.isActive()).isTrue();
+ }
+
+ @Test
+ public void updateMode_manualDnd_setsDeviceEffects() throws Exception {
+ ZenMode manualDnd = ZenMode.manualDndMode(
+ new AutomaticZenRule.Builder("DND", Uri.EMPTY)
+ .setZenPolicy(new ZenPolicy())
+ .setDeviceEffects(new ZenDeviceEffects.Builder()
+ .setShouldDimWallpaper(true)
+ .build())
+ .build(), false);
+
+ mBackend.updateMode(manualDnd);
+
+ verify(mNm).setManualZenRuleDeviceEffects(new ZenDeviceEffects.Builder()
+ .setShouldDimWallpaper(true)
+ .build());
+ }
+
+ @Test
+ public void updateMode_manualDnd_setsNotificationPolicy() {
+ ZenMode manualDnd = ZenMode.manualDndMode(
+ new AutomaticZenRule.Builder("DND", Uri.EMPTY)
+ .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+ .build(), false);
+
+ 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, false);
+
+ mBackend.updateMode(ruleMode);
+
+ verify(mNm).updateAutomaticZenRule(eq("rule"), eq(ZEN_RULE), eq(true));
+ }
+
+ @Test
+ public void activateMode_manualDnd_setsZenModeImportant() {
+ mBackend.activateMode(ZenMode.manualDndMode(MANUAL_DND_RULE, false), null);
+
+ verify(mNm).setZenMode(eq(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null),
+ any(), eq(true));
+ }
+
+ @Test
+ public void activateMode_manualDndWithDuration_setsZenModeImportantWithCondition() {
+ mBackend.activateMode(ZenMode.manualDndMode(MANUAL_DND_RULE, false),
+ 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(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false), null);
+
+ verify(mNm).setAutomaticZenRuleState(eq(ZEN_RULE_ID),
+ eq(new Condition(ZEN_RULE.getConditionId(), "", Condition.STATE_TRUE,
+ Condition.SOURCE_USER_ACTION)));
+ }
+
+ @Test
+ public void activateMode_zenRuleWithDuration_fails() {
+ assertThrows(IllegalArgumentException.class,
+ () -> mBackend.activateMode(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false),
+ Duration.ofMinutes(30)));
+ }
+
+ @Test
+ public void deactivateMode_manualDnd_setsZenModeOff() {
+ mBackend.deactivateMode(ZenMode.manualDndMode(MANUAL_DND_RULE, true));
+
+ verify(mNm).setZenMode(eq(ZEN_MODE_OFF), eq(null), any(), eq(true));
+ }
+
+ @Test
+ public void deactivateMode_zenRule_setsRuleStateInactive() {
+ mBackend.deactivateMode(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false));
+
+ verify(mNm).setAutomaticZenRuleState(eq(ZEN_RULE_ID),
+ eq(new Condition(ZEN_RULE.getConditionId(), "", Condition.STATE_FALSE,
+ Condition.SOURCE_USER_ACTION)));
+ }
+
+ @Test
+ public void removeMode_zenRule_deletesRule() {
+ mBackend.removeMode(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false));
+
+ verify(mNm).removeAutomaticZenRule(ZEN_RULE_ID, true);
+ }
+
+ @Test
+ public void removeMode_manualDnd_fails() {
+ assertThrows(IllegalArgumentException.class,
+ () -> mBackend.removeMode(ZenMode.manualDndMode(MANUAL_DND_RULE, false)));
+ }
+}