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