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