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