1 /*
2  * Copyright (C) 2023 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 android.service.notification;
18 
19 import android.annotation.IntDef;
20 import android.annotation.Nullable;
21 import android.util.ArrayMap;
22 import android.util.ArraySet;
23 
24 import java.lang.annotation.Retention;
25 import java.lang.annotation.RetentionPolicy;
26 import java.util.Objects;
27 import java.util.Set;
28 
29 /**
30  * ZenModeDiff is a utility class meant to encapsulate the diff between ZenModeConfigs and their
31  * subcomponents (automatic and manual ZenRules).
32  * @hide
33  */
34 public class ZenModeDiff {
35     /**
36      * Enum representing whether the existence of a config or rule has changed (added or removed,
37      * or "none" meaning there is no change, which may either mean both null, or there exists a
38      * diff in fields rather than add/remove).
39      */
40     @IntDef(value = {
41             NONE,
42             ADDED,
43             REMOVED,
44     })
45     @Retention(RetentionPolicy.SOURCE)
46     public @interface ExistenceChange{}
47 
48     public static final int NONE = 0;
49     public static final int ADDED = 1;
50     public static final int REMOVED = 2;
51 
52     /**
53      * Diff class representing an individual field diff.
54      * @param <T> The type of the field.
55      */
56     public static class FieldDiff<T> {
57         private final T mFrom;
58         private final T mTo;
59 
60         /**
61          * Constructor to create a FieldDiff object with the given values.
62          * @param from from (old) value
63          * @param to to (new) value
64          */
FieldDiff(@ullable T from, @Nullable T to)65         public FieldDiff(@Nullable T from, @Nullable T to) {
66             mFrom = from;
67             mTo = to;
68         }
69 
70         /**
71          * Get the "from" value
72          */
from()73         public T from() {
74             return mFrom;
75         }
76 
77         /**
78          * Get the "to" value
79          */
to()80         public T to() {
81             return mTo;
82         }
83 
84         /**
85          * Get the string representation of this field diff, in the form of "from->to".
86          */
87         @Override
toString()88         public String toString() {
89             return mFrom + "->" + mTo;
90         }
91 
92         /**
93          * Returns whether this represents an actual diff.
94          */
hasDiff()95         public boolean hasDiff() {
96             // note that Objects.equals handles null values gracefully.
97             return !Objects.equals(mFrom, mTo);
98         }
99     }
100 
101     /**
102      * Base diff class that contains info about whether something was added, and a set of named
103      * fields that changed.
104      * Extend for diffs of specific types of objects.
105      */
106     private abstract static class BaseDiff {
107         // Whether the diff was added or removed
108         @ExistenceChange private int mExists = NONE;
109 
110         // Map from field name to diffs for any standalone fields in the object.
111         private ArrayMap<String, FieldDiff> mFields = new ArrayMap<>();
112 
113         // Functions for actually diffing objects and string representations have to be implemented
114         // by subclasses.
115 
116         /**
117          * Return whether this diff represents any changes.
118          */
hasDiff()119         public abstract boolean hasDiff();
120 
121         /**
122          * Return a string representation of the diff.
123          */
toString()124         public abstract String toString();
125 
126         /**
127          * Constructor that takes the two objects meant to be compared. This constructor sets
128          * whether there is an existence change (added or removed).
129          * @param from previous Object
130          * @param to new Object
131          */
BaseDiff(Object from, Object to)132         BaseDiff(Object from, Object to) {
133             if (from == null) {
134                 if (to != null) {
135                     mExists = ADDED;
136                 }
137                 // If both are null, there isn't an existence change; callers/inheritors must handle
138                 // the both null case.
139             } else if (to == null) {
140                 // in this case, we know that from != null
141                 mExists = REMOVED;
142             }
143 
144             // Subclasses should implement the actual diffing functionality in their own
145             // constructors.
146         }
147 
148         /**
149          * Add a diff for a specific field to the map.
150          * @param name field name
151          * @param diff FieldDiff object representing the diff
152          */
addField(String name, FieldDiff diff)153         final void addField(String name, FieldDiff diff) {
154             mFields.put(name, diff);
155         }
156 
157         /**
158          * Returns whether this diff represents a config being newly added.
159          */
wasAdded()160         public final boolean wasAdded() {
161             return mExists == ADDED;
162         }
163 
164         /**
165          * Returns whether this diff represents a config being removed.
166          */
wasRemoved()167         public final boolean wasRemoved() {
168             return mExists == REMOVED;
169         }
170 
171         /**
172          * Returns whether this diff represents an object being either added or removed.
173          */
hasExistenceChange()174         public final boolean hasExistenceChange() {
175             return mExists != NONE;
176         }
177 
178         /**
179          * Returns whether there are any individual field diffs.
180          */
hasFieldDiffs()181         public final boolean hasFieldDiffs() {
182             return mFields.size() > 0;
183         }
184 
185         /**
186          * Returns the diff for the specific named field if it exists
187          */
getDiffForField(String name)188         public final FieldDiff getDiffForField(String name) {
189             return mFields.getOrDefault(name, null);
190         }
191 
192         /**
193          * Get the set of all field names with some diff.
194          */
fieldNamesWithDiff()195         public final Set<String> fieldNamesWithDiff() {
196             return mFields.keySet();
197         }
198     }
199 
200     /**
201      * Diff class representing a diff between two ZenModeConfigs.
202      */
203     public static class ConfigDiff extends BaseDiff {
204         // Rules. Automatic rule map is keyed by the rule name.
205         private final ArrayMap<String, RuleDiff> mAutomaticRulesDiff = new ArrayMap<>();
206         private RuleDiff mManualRuleDiff;
207 
208         // Field name constants
209         public static final String FIELD_USER = "user";
210         public static final String FIELD_ALLOW_ALARMS = "allowAlarms";
211         public static final String FIELD_ALLOW_MEDIA = "allowMedia";
212         public static final String FIELD_ALLOW_SYSTEM = "allowSystem";
213         public static final String FIELD_ALLOW_CALLS = "allowCalls";
214         public static final String FIELD_ALLOW_REMINDERS = "allowReminders";
215         public static final String FIELD_ALLOW_EVENTS = "allowEvents";
216         public static final String FIELD_ALLOW_REPEAT_CALLERS = "allowRepeatCallers";
217         public static final String FIELD_ALLOW_MESSAGES = "allowMessages";
218         public static final String FIELD_ALLOW_CONVERSATIONS = "allowConversations";
219         public static final String FIELD_ALLOW_CALLS_FROM = "allowCallsFrom";
220         public static final String FIELD_ALLOW_MESSAGES_FROM = "allowMessagesFrom";
221         public static final String FIELD_ALLOW_CONVERSATIONS_FROM = "allowConversationsFrom";
222         public static final String FIELD_SUPPRESSED_VISUAL_EFFECTS = "suppressedVisualEffects";
223         public static final String FIELD_ARE_CHANNELS_BYPASSING_DND = "areChannelsBypassingDnd";
224         private static final Set<String> PEOPLE_TYPE_FIELDS =
225                 Set.of(FIELD_ALLOW_CALLS_FROM, FIELD_ALLOW_MESSAGES_FROM);
226 
227         /**
228          * Create a diff that contains diffs between the "from" and "to" ZenModeConfigs.
229          *
230          * @param from previous ZenModeConfig
231          * @param to   new ZenModeConfig
232          */
ConfigDiff(ZenModeConfig from, ZenModeConfig to)233         public ConfigDiff(ZenModeConfig from, ZenModeConfig to) {
234             super(from, to);
235             // If both are null skip
236             if (from == null && to == null) {
237                 return;
238             }
239             if (hasExistenceChange()) {
240                 // either added or removed; return here. otherwise (they're not both null) there's
241                 // field diffs.
242                 return;
243             }
244 
245             // Now we compare all the fields, knowing there's a diff and that neither is null
246             if (from.user != to.user) {
247                 addField(FIELD_USER, new FieldDiff<>(from.user, to.user));
248             }
249             if (from.allowAlarms != to.allowAlarms) {
250                 addField(FIELD_ALLOW_ALARMS, new FieldDiff<>(from.allowAlarms, to.allowAlarms));
251             }
252             if (from.allowMedia != to.allowMedia) {
253                 addField(FIELD_ALLOW_MEDIA, new FieldDiff<>(from.allowMedia, to.allowMedia));
254             }
255             if (from.allowSystem != to.allowSystem) {
256                 addField(FIELD_ALLOW_SYSTEM, new FieldDiff<>(from.allowSystem, to.allowSystem));
257             }
258             if (from.allowCalls != to.allowCalls) {
259                 addField(FIELD_ALLOW_CALLS, new FieldDiff<>(from.allowCalls, to.allowCalls));
260             }
261             if (from.allowReminders != to.allowReminders) {
262                 addField(FIELD_ALLOW_REMINDERS,
263                         new FieldDiff<>(from.allowReminders, to.allowReminders));
264             }
265             if (from.allowEvents != to.allowEvents) {
266                 addField(FIELD_ALLOW_EVENTS, new FieldDiff<>(from.allowEvents, to.allowEvents));
267             }
268             if (from.allowRepeatCallers != to.allowRepeatCallers) {
269                 addField(FIELD_ALLOW_REPEAT_CALLERS,
270                         new FieldDiff<>(from.allowRepeatCallers, to.allowRepeatCallers));
271             }
272             if (from.allowMessages != to.allowMessages) {
273                 addField(FIELD_ALLOW_MESSAGES,
274                         new FieldDiff<>(from.allowMessages, to.allowMessages));
275             }
276             if (from.allowConversations != to.allowConversations) {
277                 addField(FIELD_ALLOW_CONVERSATIONS,
278                         new FieldDiff<>(from.allowConversations, to.allowConversations));
279             }
280             if (from.allowCallsFrom != to.allowCallsFrom) {
281                 addField(FIELD_ALLOW_CALLS_FROM,
282                         new FieldDiff<>(from.allowCallsFrom, to.allowCallsFrom));
283             }
284             if (from.allowMessagesFrom != to.allowMessagesFrom) {
285                 addField(FIELD_ALLOW_MESSAGES_FROM,
286                         new FieldDiff<>(from.allowMessagesFrom, to.allowMessagesFrom));
287             }
288             if (from.allowConversationsFrom != to.allowConversationsFrom) {
289                 addField(FIELD_ALLOW_CONVERSATIONS_FROM,
290                         new FieldDiff<>(from.allowConversationsFrom, to.allowConversationsFrom));
291             }
292             if (from.suppressedVisualEffects != to.suppressedVisualEffects) {
293                 addField(FIELD_SUPPRESSED_VISUAL_EFFECTS,
294                         new FieldDiff<>(from.suppressedVisualEffects, to.suppressedVisualEffects));
295             }
296             if (from.areChannelsBypassingDnd != to.areChannelsBypassingDnd) {
297                 addField(FIELD_ARE_CHANNELS_BYPASSING_DND,
298                         new FieldDiff<>(from.areChannelsBypassingDnd, to.areChannelsBypassingDnd));
299             }
300 
301             // Compare automatic and manual rules
302             final ArraySet<String> allRules = new ArraySet<>();
303             addKeys(allRules, from.automaticRules);
304             addKeys(allRules, to.automaticRules);
305             final int num = allRules.size();
306             for (int i = 0; i < num; i++) {
307                 final String rule = allRules.valueAt(i);
308                 final ZenModeConfig.ZenRule
309                         fromRule = from.automaticRules != null ? from.automaticRules.get(rule)
310                         : null;
311                 final ZenModeConfig.ZenRule
312                         toRule = to.automaticRules != null ? to.automaticRules.get(rule) : null;
313                 RuleDiff ruleDiff = new RuleDiff(fromRule, toRule);
314                 if (ruleDiff.hasDiff()) {
315                     mAutomaticRulesDiff.put(rule, ruleDiff);
316                 }
317             }
318             // If there's no diff this may turn out to be null, but that's also fine
319             RuleDiff manualRuleDiff = new RuleDiff(from.manualRule, to.manualRule);
320             if (manualRuleDiff.hasDiff()) {
321                 mManualRuleDiff = manualRuleDiff;
322             }
323         }
324 
addKeys(ArraySet<T> set, ArrayMap<T, ?> map)325         private static <T> void addKeys(ArraySet<T> set, ArrayMap<T, ?> map) {
326             if (map != null) {
327                 for (int i = 0; i < map.size(); i++) {
328                     set.add(map.keyAt(i));
329                 }
330             }
331         }
332 
333         /**
334          * Returns whether this diff object contains any diffs in any field.
335          */
336         @Override
hasDiff()337         public boolean hasDiff() {
338             return hasExistenceChange()
339                     || hasFieldDiffs()
340                     || mManualRuleDiff != null
341                     || mAutomaticRulesDiff.size() > 0;
342         }
343 
344         @Override
toString()345         public String toString() {
346             final StringBuilder sb = new StringBuilder("Diff[");
347             if (!hasDiff()) {
348                 sb.append("no changes");
349             }
350 
351             // If added or deleted, then that's just the end of it
352             if (hasExistenceChange()) {
353                 if (wasAdded()) {
354                     sb.append("added");
355                 } else if (wasRemoved()) {
356                     sb.append("removed");
357                 }
358             }
359 
360             // Handle top-level field change
361             boolean first = true;
362             for (String key : fieldNamesWithDiff()) {
363                 FieldDiff diff = getDiffForField(key);
364                 if (diff == null) {
365                     // this shouldn't happen, but
366                     continue;
367                 }
368                 if (first) {
369                     first = false;
370                 } else {
371                     sb.append(",\n");
372                 }
373 
374                 // Some special handling for people- and conversation-type fields for readability
375                 if (PEOPLE_TYPE_FIELDS.contains(key)) {
376                     sb.append(key);
377                     sb.append(":");
378                     sb.append(ZenModeConfig.sourceToString((int) diff.from()));
379                     sb.append("->");
380                     sb.append(ZenModeConfig.sourceToString((int) diff.to()));
381                 } else if (key.equals(FIELD_ALLOW_CONVERSATIONS_FROM)) {
382                     sb.append(key);
383                     sb.append(":");
384                     sb.append(ZenPolicy.conversationTypeToString((int) diff.from()));
385                     sb.append("->");
386                     sb.append(ZenPolicy.conversationTypeToString((int) diff.to()));
387                 } else {
388                     sb.append(key);
389                     sb.append(":");
390                     sb.append(diff);
391                 }
392             }
393 
394             // manual rule
395             if (mManualRuleDiff != null && mManualRuleDiff.hasDiff()) {
396                 if (first) {
397                     first = false;
398                 } else {
399                     sb.append(",\n");
400                 }
401                 sb.append("manualRule:");
402                 sb.append(mManualRuleDiff);
403             }
404 
405             // automatic rules
406             for (String rule : mAutomaticRulesDiff.keySet()) {
407                 RuleDiff diff = mAutomaticRulesDiff.get(rule);
408                 if (diff != null && diff.hasDiff()) {
409                     if (first) {
410                         first = false;
411                     } else {
412                         sb.append(",\n");
413                     }
414                     sb.append("automaticRule[");
415                     sb.append(rule);
416                     sb.append("]:");
417                     sb.append(diff);
418                 }
419             }
420 
421             return sb.append(']').toString();
422         }
423 
424         /**
425          * Get the diff in manual rule, if it exists.
426          */
getManualRuleDiff()427         public RuleDiff getManualRuleDiff() {
428             return mManualRuleDiff;
429         }
430 
431         /**
432          * Get the full map of automatic rule diffs, or null if there are no diffs.
433          */
getAllAutomaticRuleDiffs()434         public ArrayMap<String, RuleDiff> getAllAutomaticRuleDiffs() {
435             return (mAutomaticRulesDiff.size() > 0) ? mAutomaticRulesDiff : null;
436         }
437     }
438 
439     /**
440      * Diff class representing a change between two ZenRules.
441      */
442     public static class RuleDiff extends BaseDiff {
443         public static final String FIELD_ENABLED = "enabled";
444         public static final String FIELD_SNOOZING = "snoozing";
445         public static final String FIELD_NAME = "name";
446         public static final String FIELD_ZEN_MODE = "zenMode";
447         public static final String FIELD_CONDITION_ID = "conditionId";
448         public static final String FIELD_CONDITION = "condition";
449         public static final String FIELD_COMPONENT = "component";
450         public static final String FIELD_CONFIGURATION_ACTIVITY = "configurationActivity";
451         public static final String FIELD_ID = "id";
452         public static final String FIELD_CREATION_TIME = "creationTime";
453         public static final String FIELD_ENABLER = "enabler";
454         public static final String FIELD_ZEN_POLICY = "zenPolicy";
455         public static final String FIELD_MODIFIED = "modified";
456         public static final String FIELD_PKG = "pkg";
457 
458         // Special field to track whether this rule became active or inactive
459         FieldDiff<Boolean> mActiveDiff;
460 
461         /**
462          * Create a RuleDiff representing the difference between two ZenRule objects.
463          * @param from previous ZenRule
464          * @param to new ZenRule
465          * @return The diff between the two given ZenRules
466          */
RuleDiff(ZenModeConfig.ZenRule from, ZenModeConfig.ZenRule to)467         public RuleDiff(ZenModeConfig.ZenRule from, ZenModeConfig.ZenRule to) {
468             super(from, to);
469             // Short-circuit the both-null case
470             if (from == null && to == null) {
471                 return;
472             }
473 
474             // Even if added or removed, there may be a change in whether or not it was active.
475             // This only applies to automatic rules.
476             boolean fromActive = from != null ? from.isAutomaticActive() : false;
477             boolean toActive = to != null ? to.isAutomaticActive() : false;
478             if (fromActive != toActive) {
479                 mActiveDiff = new FieldDiff<>(fromActive, toActive);
480             }
481 
482             // Return if the diff was added or removed
483             if (hasExistenceChange()) {
484                 return;
485             }
486 
487             if (from.enabled != to.enabled) {
488                 addField(FIELD_ENABLED, new FieldDiff<>(from.enabled, to.enabled));
489             }
490             if (from.snoozing != to.snoozing) {
491                 addField(FIELD_SNOOZING, new FieldDiff<>(from.snoozing, to.snoozing));
492             }
493             if (!Objects.equals(from.name, to.name)) {
494                 addField(FIELD_NAME, new FieldDiff<>(from.name, to.name));
495             }
496             if (from.zenMode != to.zenMode) {
497                 addField(FIELD_ZEN_MODE, new FieldDiff<>(from.zenMode, to.zenMode));
498             }
499             if (!Objects.equals(from.conditionId, to.conditionId)) {
500                 addField(FIELD_CONDITION_ID, new FieldDiff<>(from.conditionId,
501                         to.conditionId));
502             }
503             if (!Objects.equals(from.condition, to.condition)) {
504                 addField(FIELD_CONDITION, new FieldDiff<>(from.condition, to.condition));
505             }
506             if (!Objects.equals(from.component, to.component)) {
507                 addField(FIELD_COMPONENT, new FieldDiff<>(from.component, to.component));
508             }
509             if (!Objects.equals(from.configurationActivity, to.configurationActivity)) {
510                 addField(FIELD_CONFIGURATION_ACTIVITY, new FieldDiff<>(
511                         from.configurationActivity, to.configurationActivity));
512             }
513             if (!Objects.equals(from.id, to.id)) {
514                 addField(FIELD_ID, new FieldDiff<>(from.id, to.id));
515             }
516             if (from.creationTime != to.creationTime) {
517                 addField(FIELD_CREATION_TIME,
518                         new FieldDiff<>(from.creationTime, to.creationTime));
519             }
520             if (!Objects.equals(from.enabler, to.enabler)) {
521                 addField(FIELD_ENABLER, new FieldDiff<>(from.enabler, to.enabler));
522             }
523             if (!Objects.equals(from.zenPolicy, to.zenPolicy)) {
524                 addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy));
525             }
526             if (from.modified != to.modified) {
527                 addField(FIELD_MODIFIED, new FieldDiff<>(from.modified, to.modified));
528             }
529             if (!Objects.equals(from.pkg, to.pkg)) {
530                 addField(FIELD_PKG, new FieldDiff<>(from.pkg, to.pkg));
531             }
532         }
533 
534         /**
535          * Returns whether this object represents an actual diff.
536          */
537         @Override
hasDiff()538         public boolean hasDiff() {
539             return hasExistenceChange() || hasFieldDiffs();
540         }
541 
542         @Override
toString()543         public String toString() {
544             final StringBuilder sb = new StringBuilder("ZenRuleDiff{");
545             // If there's no diff, probably we haven't actually let this object continue existing
546             // but might as well handle this case.
547             if (!hasDiff()) {
548                 sb.append("no changes");
549             }
550 
551             // If added or deleted, then that's just the end of it
552             if (hasExistenceChange()) {
553                 if (wasAdded()) {
554                     sb.append("added");
555                 } else if (wasRemoved()) {
556                     sb.append("removed");
557                 }
558             }
559 
560             // Go through all of the individual fields
561             boolean first = true;
562             for (String key : fieldNamesWithDiff()) {
563                 FieldDiff diff = getDiffForField(key);
564                 if (diff == null) {
565                     // this shouldn't happen, but
566                     continue;
567                 }
568                 if (first) {
569                     first = false;
570                 } else {
571                     sb.append(", ");
572                 }
573 
574                 sb.append(key);
575                 sb.append(":");
576                 sb.append(diff);
577             }
578 
579             if (becameActive()) {
580                 if (!first) {
581                     sb.append(", ");
582                 }
583                 sb.append("(->active)");
584             } else if (becameInactive()) {
585                 if (!first) {
586                     sb.append(", ");
587                 }
588                 sb.append("(->inactive)");
589             }
590 
591             return sb.append("}").toString();
592         }
593 
594         /**
595          * Returns whether this diff indicates that this (automatic) rule became active.
596          */
becameActive()597         public boolean becameActive() {
598             // if the "to" side is true, then it became active
599             return mActiveDiff != null && mActiveDiff.to();
600         }
601 
602         /**
603          * Returns whether this diff indicates that this (automatic) rule became inactive.
604          */
becameInactive()605         public boolean becameInactive() {
606             // if the "to" side is false, then it became inactive
607             return mActiveDiff != null && !mActiveDiff.to();
608         }
609     }
610 }
611