• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settingslib.notification.modes;
18 
19 import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
20 import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME;
21 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS;
22 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
23 import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
24 import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
25 import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleEvent;
26 import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleTime;
27 import static android.service.notification.ZenModeConfig.tryParseEventConditionId;
28 import static android.service.notification.ZenModeConfig.tryParseScheduleConditionId;
29 
30 import static com.google.common.base.Preconditions.checkNotNull;
31 import static com.google.common.base.Preconditions.checkState;
32 
33 import static java.util.Objects.requireNonNull;
34 
35 import android.annotation.SuppressLint;
36 import android.app.AutomaticZenRule;
37 import android.app.NotificationManager;
38 import android.content.ComponentName;
39 import android.content.Context;
40 import android.net.Uri;
41 import android.os.Parcel;
42 import android.os.Parcelable;
43 import android.service.notification.SystemZenRules;
44 import android.service.notification.ZenDeviceEffects;
45 import android.service.notification.ZenModeConfig;
46 import android.service.notification.ZenPolicy;
47 import android.util.Log;
48 
49 import androidx.annotation.DrawableRes;
50 import androidx.annotation.NonNull;
51 import androidx.annotation.Nullable;
52 
53 import com.google.common.base.Strings;
54 import com.google.common.collect.ImmutableList;
55 
56 import java.util.Comparator;
57 import java.util.Objects;
58 
59 /**
60  * Represents either an {@link AutomaticZenRule} or the manual DND rule in a unified way.
61  *
62  * <p>It also adapts other rule features that we don't want to expose in the UI, such as
63  * interruption filters other than {@code PRIORITY}, rules without specific icons, etc.
64  */
65 public class ZenMode implements Parcelable {
66 
67     private static final String TAG = "ZenMode";
68 
69     static final String MANUAL_DND_MODE_ID = ZenModeConfig.MANUAL_RULE_ID;
70     static final String TEMP_NEW_MODE_ID = "temp_new_mode";
71 
72     private static final Comparator<Integer> PRIORITIZED_TYPE_COMPARATOR = new Comparator<>() {
73 
74         private static final ImmutableList</* @AutomaticZenRule.Type */ Integer>
75                 PRIORITIZED_TYPES = ImmutableList.of(
76                         AutomaticZenRule.TYPE_BEDTIME,
77                         AutomaticZenRule.TYPE_DRIVING);
78 
79         @Override
80         public int compare(Integer first, Integer second) {
81             if (PRIORITIZED_TYPES.contains(first) && PRIORITIZED_TYPES.contains(second)) {
82                 return PRIORITIZED_TYPES.indexOf(first) - PRIORITIZED_TYPES.indexOf(second);
83             } else if (PRIORITIZED_TYPES.contains(first)) {
84                 return -1;
85             } else if (PRIORITIZED_TYPES.contains(second)) {
86                 return 1;
87             } else {
88                 return 0;
89             }
90         }
91     };
92 
93     // Manual DND first, Bedtime/Driving, then alphabetically.
94     public static final Comparator<ZenMode> PRIORITIZING_COMPARATOR = Comparator
95             .comparing(ZenMode::isManualDnd).reversed()
96             .thenComparing(ZenMode::getType, PRIORITIZED_TYPE_COMPARATOR)
97             .thenComparing(ZenMode::getName);
98 
99     public enum Kind {
100         /** A "normal" mode, created by apps or users via {@code addAutomaticZenRule()}. */
101         NORMAL,
102 
103         /** The special, built-in "Do Not Disturb" mode. */
104         MANUAL_DND,
105 
106         /**
107          * An implicit mode, automatically created and managed by the system on behalf of apps that
108          * call {@code setInterruptionFilter()} or {@code setNotificationPolicy()} (with some
109          * exceptions).
110          */
111         IMPLICIT,
112     }
113 
114     public enum Status {
115         ENABLED,
116         ENABLED_AND_ACTIVE,
117         DISABLED_BY_USER,
118         DISABLED_BY_OTHER
119     }
120 
121     /**
122      * Information about the owner of a {@link ZenMode}. {@link #packageName()} is
123      * {@link SystemZenRules#PACKAGE_ANDROID} if the mode is system-owned; it may also be
124      * {@code null}, but only as an artifact of very old modes.
125      */
Owner(@ullable String packageName, @Nullable ComponentName configurationActivity, @Nullable ComponentName conditionProvider)126     public record Owner(@Nullable String packageName, @Nullable ComponentName configurationActivity,
127                            @Nullable ComponentName conditionProvider) { }
128 
129     private final String mId;
130     private final AutomaticZenRule mRule;
131     private final Kind mKind;
132     private final Status mStatus;
133 
134     /**
135      * Initializes a {@link ZenMode}, mainly based on the information from the
136      * {@link AutomaticZenRule}.
137      *
138      * <p>Some pieces which are not part of the public API (such as whether the mode is currently
139      * active, or the reason it was disabled) are read from the {@link ZenModeConfig.ZenRule} --
140      * see {@link #computeStatus}.
141      */
ZenMode(String id, @NonNull AutomaticZenRule rule, @NonNull ZenModeConfig.ZenRule zenRuleExtraData)142     ZenMode(String id, @NonNull AutomaticZenRule rule,
143             @NonNull ZenModeConfig.ZenRule zenRuleExtraData) {
144         this(id, rule,
145                 ZenModeConfig.isImplicitRuleId(id) ? Kind.IMPLICIT : Kind.NORMAL,
146                 computeStatus(zenRuleExtraData));
147     }
148 
computeStatus(@onNull ZenModeConfig.ZenRule zenRuleExtraData)149     private static Status computeStatus(@NonNull ZenModeConfig.ZenRule zenRuleExtraData) {
150         if (zenRuleExtraData.enabled) {
151             if (zenRuleExtraData.isActive()) {
152                 return Status.ENABLED_AND_ACTIVE;
153             } else {
154                 return Status.ENABLED;
155             }
156         } else {
157             if (zenRuleExtraData.disabledOrigin == ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI) {
158                 return Status.DISABLED_BY_USER;
159             } else {
160                 return Status.DISABLED_BY_OTHER; // by APP, SYSTEM, UNKNOWN.
161             }
162         }
163     }
164 
manualDndMode(AutomaticZenRule manualRule, boolean isActive)165     static ZenMode manualDndMode(AutomaticZenRule manualRule, boolean isActive) {
166         return new ZenMode(
167                 MANUAL_DND_MODE_ID,
168                 manualRule,
169                 Kind.MANUAL_DND,
170                 isActive ? Status.ENABLED_AND_ACTIVE : Status.ENABLED);
171     }
172 
173     /**
174      * Returns a new {@link ZenMode} instance that can represent a custom_manual mode that is in the
175      * process of being created (and not yet saved).
176      *
177      * @param name mode name
178      * @param iconResId resource id of the chosen icon, {code 0} if none.
179      */
newCustomManual(String name, @DrawableRes int iconResId)180     public static ZenMode newCustomManual(String name, @DrawableRes int iconResId) {
181         AutomaticZenRule rule = new AutomaticZenRule.Builder(name,
182                 ZenModeConfig.toCustomManualConditionId())
183                 .setPackage(ZenModeConfig.getCustomManualConditionProvider().getPackageName())
184                 .setType(AutomaticZenRule.TYPE_OTHER)
185                 .setOwner(ZenModeConfig.getCustomManualConditionProvider())
186                 .setIconResId(iconResId)
187                 .setManualInvocationAllowed(true)
188                 .build();
189         return new ZenMode(TEMP_NEW_MODE_ID, rule, Kind.NORMAL, Status.ENABLED);
190     }
191 
ZenMode(String id, @NonNull AutomaticZenRule rule, Kind kind, Status status)192     private ZenMode(String id, @NonNull AutomaticZenRule rule, Kind kind, Status status) {
193         mId = id;
194         mRule = rule;
195         mKind = kind;
196         mStatus = status;
197     }
198 
199     /** Creates a deep copy of this object. */
copy()200     public ZenMode copy() {
201         return new ZenMode(mId, new AutomaticZenRule.Builder(mRule).build(), mKind, mStatus);
202     }
203 
204     @NonNull
getId()205     public String getId() {
206         return mId;
207     }
208 
209     @NonNull
getRule()210     AutomaticZenRule getRule() {
211         return mRule;
212     }
213 
214     @NonNull
getName()215     public String getName() {
216         return Strings.nullToEmpty(mRule.getName());
217     }
218 
setName(@onNull String name)219     public void setName(@NonNull String name) {
220         mRule.setName(name);
221     }
222 
223     @NonNull
getKind()224     public Kind getKind() {
225         return mKind;
226     }
227 
228     @NonNull
getStatus()229     public Status getStatus() {
230         return mStatus;
231     }
232 
233     @NonNull
getOwner()234     public Owner getOwner() {
235         return new Owner(mRule.getPackageName(), mRule.getConfigurationActivity(),
236                 mRule.getOwner());
237     }
238 
239     @Nullable
getOwnerPackage()240     public String getOwnerPackage() {
241         return getOwner().packageName();
242     }
243 
244     @AutomaticZenRule.Type
getType()245     public int getType() {
246         return mRule.getType();
247     }
248 
249     /** Returns the trigger description of the mode. */
250     @Nullable
getTriggerDescription()251     public String getTriggerDescription() {
252         return mRule.getTriggerDescription();
253     }
254 
255     /**
256      * Returns the {@link ZenIcon.Key} corresponding to the icon resource for this mode. This can be
257      * either app-provided (via {@link AutomaticZenRule#setIconResId}, user-chosen (via the icon
258      * picker in Settings), or a default icon based on the mode {@link Kind} and {@link #getType}.
259      */
260     @NonNull
getIconKey()261     public ZenIcon.Key getIconKey() {
262         if (isManualDnd()) {
263             return ZenIconKeys.MANUAL_DND;
264         }
265         if (mRule.getIconResId() != 0) {
266             if (isSystemOwned()) {
267                 // System-owned rules can only have system icons.
268                 return ZenIcon.Key.forSystemResource(mRule.getIconResId());
269             } else {
270                 // Technically, the icon of an app-provided rule could be a system icon if the
271                 // user chose one with the picker. However, we cannot know for sure.
272                 return new ZenIcon.Key(mRule.getPackageName(), mRule.getIconResId());
273             }
274         } else {
275             // Using a default icon (which is always a system icon).
276             if (mKind == Kind.IMPLICIT) {
277                 return ZenIconKeys.IMPLICIT_MODE_DEFAULT;
278             } else {
279                 return ZenIconKeys.forType(getType());
280             }
281         }
282     }
283 
284     /**
285      * Returns the resource id of the icon for this mode. Note that this is the <em>stored</em>
286      * resource id, and thus can be different from the value in {@link #getIconKey()} -- in
287      * particular, for modes without a custom icon set, this method returns {@code 0} whereas
288      * {@link #getIconKey()} will return a default icon based on other mode properties.
289      *
290      * <p>Most callers are interested in {@link #getIconKey()}, unless they are editing the icon.
291      */
getIconResId()292     public int getIconResId() {
293         return mRule.getIconResId();
294     }
295 
296     /**
297      * Sets the resource id of the icon for this mode.
298      * @see #getIconResId()
299      */
setIconResId(@rawableRes int iconResId)300     public void setIconResId(@DrawableRes int iconResId) {
301         mRule.setIconResId(iconResId);
302     }
303 
304     /** Returns the interruption filter of the mode. */
305     @NotificationManager.InterruptionFilter
getInterruptionFilter()306     public int getInterruptionFilter() {
307         return mRule.getInterruptionFilter();
308     }
309 
310     /**
311      * Sets the interruption filter of the mode. This is valid for {@link AutomaticZenRule}-backed
312      * modes (and not manual DND).
313      */
setInterruptionFilter(@otificationManager.InterruptionFilter int filter)314     public void setInterruptionFilter(@NotificationManager.InterruptionFilter int filter) {
315         if (isManualDnd() || !canEditPolicy()) {
316             throw new IllegalStateException("Cannot update interruption filter for mode " + this);
317         }
318         mRule.setInterruptionFilter(filter);
319     }
320 
321     @NonNull
getPolicy()322     public ZenPolicy getPolicy() {
323         switch (mRule.getInterruptionFilter()) {
324             case INTERRUPTION_FILTER_PRIORITY:
325             case NotificationManager.INTERRUPTION_FILTER_ALL:
326                 return requireNonNull(mRule.getZenPolicy());
327 
328             case NotificationManager.INTERRUPTION_FILTER_ALARMS:
329                 return new ZenPolicy.Builder(ZenModeConfig.getDefaultZenPolicy()).build()
330                         .overwrittenWith(ZenPolicy.getBasePolicyInterruptionFilterAlarms());
331 
332             case NotificationManager.INTERRUPTION_FILTER_NONE:
333                 return new ZenPolicy.Builder(ZenModeConfig.getDefaultZenPolicy()).build()
334                         .overwrittenWith(ZenPolicy.getBasePolicyInterruptionFilterNone());
335 
336             case NotificationManager.INTERRUPTION_FILTER_UNKNOWN:
337             default:
338                 Log.wtf(TAG, "Rule " + mId + " with unexpected interruptionFilter "
339                         + mRule.getInterruptionFilter());
340                 return requireNonNull(mRule.getZenPolicy());
341         }
342     }
343 
344     /**
345      * Updates the {@link ZenPolicy} of the associated {@link AutomaticZenRule} based on the
346      * supplied policy. In some cases this involves conversions, so that the following call
347      * to {@link #getPolicy} might return a different policy from the one supplied here.
348      */
349     @SuppressLint("WrongConstant")
setPolicy(@onNull ZenPolicy policy)350     public void setPolicy(@NonNull ZenPolicy policy) {
351         if (!canEditPolicy()) {
352             throw new IllegalStateException("Cannot update ZenPolicy for mode " + this);
353         }
354 
355         ZenPolicy currentPolicy = getPolicy();
356         if (currentPolicy.equals(policy)) {
357             return;
358         }
359 
360         if (mRule.getInterruptionFilter() == INTERRUPTION_FILTER_ALL) {
361             Log.wtf(TAG, "Able to change policy without filtering being enabled");
362         }
363 
364         // If policy is customized from any of the "special" ones, make the rule PRIORITY.
365         if (mRule.getInterruptionFilter() != INTERRUPTION_FILTER_PRIORITY) {
366             mRule.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY);
367         }
368         mRule.setZenPolicy(policy);
369     }
370 
371     /**
372      * Returns the {@link ZenDeviceEffects} of the mode.
373      *
374      * <p>This is never {@code null}; if the backing AutomaticZenRule doesn't have effects set then
375      * a default (empty) effects set is returned.
376      */
377     @NonNull
getDeviceEffects()378     public ZenDeviceEffects getDeviceEffects() {
379         return mRule.getDeviceEffects() != null
380                 ? mRule.getDeviceEffects()
381                 : new ZenDeviceEffects.Builder().build();
382     }
383 
384     /** Sets the {@link ZenDeviceEffects} of the mode. */
setDeviceEffects(@onNull ZenDeviceEffects effects)385     public void setDeviceEffects(@NonNull ZenDeviceEffects effects) {
386         checkNotNull(effects);
387         if (!canEditPolicy()) {
388             throw new IllegalStateException("Cannot update device effects for mode " + this);
389         }
390         mRule.setDeviceEffects(effects);
391     }
392 
setCustomModeConditionId(Context context, Uri conditionId)393     public void setCustomModeConditionId(Context context, Uri conditionId) {
394         checkState(SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName()),
395                 "Trying to change condition of non-system-owned rule %s (to %s)",
396                 mRule, conditionId);
397 
398         Uri oldCondition = mRule.getConditionId();
399         mRule.setConditionId(conditionId);
400 
401         ZenModeConfig.ScheduleInfo scheduleInfo = tryParseScheduleConditionId(conditionId);
402         if (scheduleInfo != null) {
403             mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_TIME);
404             mRule.setOwner(ZenModeConfig.getScheduleConditionProvider());
405             mRule.setTriggerDescription(
406                     getTriggerDescriptionForScheduleTime(context, scheduleInfo));
407             return;
408         }
409 
410         ZenModeConfig.EventInfo eventInfo = tryParseEventConditionId(conditionId);
411         if (eventInfo != null) {
412             mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_CALENDAR);
413             mRule.setOwner(ZenModeConfig.getEventConditionProvider());
414             mRule.setTriggerDescription(getTriggerDescriptionForScheduleEvent(context, eventInfo));
415             return;
416         }
417 
418         if (ZenModeConfig.isValidCustomManualConditionId(conditionId)) {
419             mRule.setType(AutomaticZenRule.TYPE_OTHER);
420             mRule.setOwner(ZenModeConfig.getCustomManualConditionProvider());
421             mRule.setTriggerDescription("");
422             return;
423         }
424 
425         Log.wtf(TAG, String.format(
426                 "Changed condition of rule %s (%s -> %s) but cannot recognize which kind of "
427                         + "condition it was!",
428                 mRule, oldCondition, conditionId));
429     }
430 
canEditNameAndIcon()431     public boolean canEditNameAndIcon() {
432         return !isManualDnd();
433     }
434 
435     /**
436      * Whether the mode has an editable policy. Calling {@link #setPolicy},
437      * {@link #setDeviceEffects}, or {@link #setInterruptionFilter} is not valid for modes with a
438      * read-only policy.
439      */
canEditPolicy()440     public boolean canEditPolicy() {
441         // Cannot edit the policy of a temporarily active non-PRIORITY DND mode.
442         // Note that it's fine to edit the policy of an *AutomaticZenRule* with non-PRIORITY filter;
443         // the filter will we set to PRIORITY if you do.
444         return !isManualDndWithSpecialFilter();
445     }
446 
canBeDeleted()447     public boolean canBeDeleted() {
448         return !isManualDnd();
449     }
450 
isManualDnd()451     public boolean isManualDnd() {
452         return mKind == Kind.MANUAL_DND;
453     }
454 
isManualDndWithSpecialFilter()455     private boolean isManualDndWithSpecialFilter() {
456         return isManualDnd()
457                 && (mRule.getInterruptionFilter() == INTERRUPTION_FILTER_ALARMS
458                 || mRule.getInterruptionFilter() == INTERRUPTION_FILTER_NONE);
459     }
460 
461     /**
462      * A <em>custom manual</em> mode is a mode created by the user, and not yet assigned an
463      * automatic trigger condition (neither time schedule nor a calendar).
464      */
isCustomManual()465     public boolean isCustomManual() {
466         return isSystemOwned()
467                 && getType() != TYPE_SCHEDULE_TIME
468                 && getType() != TYPE_SCHEDULE_CALENDAR
469                 && !isManualDnd();
470     }
471 
isEnabled()472     public boolean isEnabled() {
473         return mRule.isEnabled();
474     }
475 
476     /**
477      * Enables or disables the mode.
478      *
479      * <p>The DND mode cannot be disabled; trying to do so will fail.
480      */
setEnabled(boolean enabled)481     public void setEnabled(boolean enabled) {
482         if (isManualDnd()) {
483             throw new IllegalStateException("Cannot update enabled for manual DND mode " + this);
484         }
485         mRule.setEnabled(enabled);
486     }
487 
isActive()488     public boolean isActive() {
489         return mStatus == Status.ENABLED_AND_ACTIVE;
490     }
491 
isManualInvocationAllowed()492     public boolean isManualInvocationAllowed() {
493         return mRule.isManualInvocationAllowed();
494     }
495 
isSystemOwned()496     public boolean isSystemOwned() {
497         return SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName());
498     }
499 
500     @Override
equals(@ullable Object obj)501     public boolean equals(@Nullable Object obj) {
502         return obj instanceof ZenMode other
503                 && mId.equals(other.mId)
504                 && mRule.equals(other.mRule)
505                 && mKind.equals(other.mKind)
506                 && mStatus.equals(other.mStatus);
507     }
508 
509     @Override
hashCode()510     public int hashCode() {
511         return Objects.hash(mId, mRule, mKind, mStatus);
512     }
513 
514     @Override
toString()515     public String toString() {
516         return mId + " (" + mKind + ", " + mStatus + ") -> " + mRule;
517     }
518 
519     @Override
describeContents()520     public int describeContents() {
521         return 0;
522     }
523 
524     @Override
writeToParcel(@onNull Parcel dest, int flags)525     public void writeToParcel(@NonNull Parcel dest, int flags) {
526         dest.writeString(mId);
527         dest.writeParcelable(mRule, 0);
528         dest.writeString(mKind.name());
529         dest.writeString(mStatus.name());
530     }
531 
532     public static final Creator<ZenMode> CREATOR = new Creator<ZenMode>() {
533         @Override
534         public ZenMode createFromParcel(Parcel in) {
535             return new ZenMode(
536                     in.readString(),
537                     checkNotNull(in.readParcelable(AutomaticZenRule.class.getClassLoader(),
538                             AutomaticZenRule.class)),
539                     Kind.valueOf(in.readString()),
540                     Status.valueOf(in.readString()));
541         }
542 
543         @Override
544         public ZenMode[] newArray(int size) {
545             return new ZenMode[size];
546         }
547     };
548 }
549