diff options
| author | 2023-12-05 15:35:36 +0100 | |
|---|---|---|
| committer | 2024-01-11 14:21:59 +0100 | |
| commit | 6480b8b78a8343274f53356d6edce2437a4f58da (patch) | |
| tree | 5631e483d392517faf30fbd4c46185fc2fd83baf | |
| parent | 65403b120556c2d3617565ac46fc42332c703633 (diff) | |
Preserve user customization when an app deletes and recreates an AutomaticZenRule
Test: atest ZenModeHelperTest
Bug: 308672001
Change-Id: Iccbef610c50a062f627fdedca54881b8fd0264ed
7 files changed, 614 insertions, 76 deletions
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 45a0c205a09b..54248be74e04 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -64,6 +64,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.time.Instant; import java.util.Arrays; import java.util.Calendar; import java.util.Date; @@ -233,6 +234,7 @@ public class ZenModeConfig implements Parcelable { private static final String MANUAL_TAG = "manual"; private static final String AUTOMATIC_TAG = "automatic"; + private static final String AUTOMATIC_DELETED_TAG = "deleted"; private static final String RULE_ATT_ID = "ruleId"; private static final String RULE_ATT_ENABLED = "enabled"; @@ -251,6 +253,7 @@ public class ZenModeConfig implements Parcelable { private static final String RULE_ATT_USER_MODIFIED_FIELDS = "userModifiedFields"; private static final String RULE_ATT_ICON = "rule_icon"; private static final String RULE_ATT_TRIGGER_DESC = "triggerDesc"; + private static final String RULE_ATT_DELETION_INSTANT = "deletionInstant"; private static final String DEVICE_EFFECT_DISPLAY_GRAYSCALE = "zdeDisplayGrayscale"; private static final String DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY = @@ -292,6 +295,10 @@ public class ZenModeConfig implements Parcelable { @UnsupportedAppUsage public ArrayMap<String, ZenRule> automaticRules = new ArrayMap<>(); + // Note: Map is *pkg|conditionId* (see deletedRuleKey()) -> ZenRule, + // unlike automaticRules (which is id -> rule). + public final ArrayMap<String, ZenRule> deletedRules = new ArrayMap<>(); + @UnsupportedAppUsage public ZenModeConfig() { } @@ -306,15 +313,9 @@ public class ZenModeConfig implements Parcelable { allowMessagesFrom = source.readInt(); user = source.readInt(); manualRule = source.readParcelable(null, ZenRule.class); - final int len = source.readInt(); - if (len > 0) { - final String[] ids = new String[len]; - final ZenRule[] rules = new ZenRule[len]; - source.readStringArray(ids); - source.readTypedArray(rules, ZenRule.CREATOR); - for (int i = 0; i < len; i++) { - automaticRules.put(ids[i], rules[i]); - } + readRulesFromParcel(automaticRules, source); + if (Flags.modesApi()) { + readRulesFromParcel(deletedRules, source); } allowAlarms = source.readInt() == 1; allowMedia = source.readInt() == 1; @@ -328,6 +329,19 @@ public class ZenModeConfig implements Parcelable { } } + private static void readRulesFromParcel(ArrayMap<String, ZenRule> ruleMap, Parcel source) { + final int len = source.readInt(); + if (len > 0) { + final String[] ids = new String[len]; + final ZenRule[] rules = new ZenRule[len]; + source.readStringArray(ids); + source.readTypedArray(rules, ZenRule.CREATOR); + for (int i = 0; i < len; i++) { + ruleMap.put(ids[i], rules[i]); + } + } + } + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(allowCalls ? 1 : 0); @@ -339,19 +353,9 @@ public class ZenModeConfig implements Parcelable { dest.writeInt(allowMessagesFrom); dest.writeInt(user); dest.writeParcelable(manualRule, 0); - if (!automaticRules.isEmpty()) { - final int len = automaticRules.size(); - final String[] ids = new String[len]; - final ZenRule[] rules = new ZenRule[len]; - for (int i = 0; i < len; i++) { - ids[i] = automaticRules.keyAt(i); - rules[i] = automaticRules.valueAt(i); - } - dest.writeInt(len); - dest.writeStringArray(ids); - dest.writeTypedArray(rules, 0); - } else { - dest.writeInt(0); + writeRulesToParcel(automaticRules, dest); + if (Flags.modesApi()) { + writeRulesToParcel(deletedRules, dest); } dest.writeInt(allowAlarms ? 1 : 0); dest.writeInt(allowMedia ? 1 : 0); @@ -365,6 +369,23 @@ public class ZenModeConfig implements Parcelable { } } + private static void writeRulesToParcel(ArrayMap<String, ZenRule> ruleMap, Parcel dest) { + if (!ruleMap.isEmpty()) { + final int len = ruleMap.size(); + final String[] ids = new String[len]; + final ZenRule[] rules = new ZenRule[len]; + for (int i = 0; i < len; i++) { + ids[i] = ruleMap.keyAt(i); + rules[i] = ruleMap.valueAt(i); + } + dest.writeInt(len); + dest.writeStringArray(ids); + dest.writeTypedArray(rules, 0); + } else { + dest.writeInt(0); + } + } + @Override public String toString() { StringBuilder sb = new StringBuilder(ZenModeConfig.class.getSimpleName()).append('[') @@ -389,23 +410,26 @@ public class ZenModeConfig implements Parcelable { } else { sb.append(",areChannelsBypassingDnd=").append(areChannelsBypassingDnd); } - return sb.append(",\nautomaticRules=").append(rulesToString()) - .append(",\nmanualRule=").append(manualRule) - .append(']').toString(); + sb.append(",\nautomaticRules=").append(rulesToString(automaticRules)) + .append(",\nmanualRule=").append(manualRule); + if (Flags.modesApi()) { + sb.append(",\ndeletedRules=").append(rulesToString(deletedRules)); + } + return sb.append(']').toString(); } - private String rulesToString() { - if (automaticRules.isEmpty()) { + private static String rulesToString(ArrayMap<String, ZenRule> ruleList) { + if (ruleList.isEmpty()) { return "{}"; } - StringBuilder buffer = new StringBuilder(automaticRules.size() * 28); + StringBuilder buffer = new StringBuilder(ruleList.size() * 28); buffer.append("{\n"); - for (int i = 0; i < automaticRules.size(); i++) { + for (int i = 0; i < ruleList.size(); i++) { if (i > 0) { buffer.append(",\n"); } - Object value = automaticRules.valueAt(i); + Object value = ruleList.valueAt(i); buffer.append(value); } buffer.append('}'); @@ -487,7 +511,9 @@ public class ZenModeConfig implements Parcelable { && other.allowConversations == allowConversations && other.allowConversationsFrom == allowConversationsFrom; if (Flags.modesApi()) { - return eq && other.allowPriorityChannels == allowPriorityChannels; + return eq + && Objects.equals(other.deletedRules, deletedRules) + && other.allowPriorityChannels == allowPriorityChannels; } return eq; } @@ -644,12 +670,20 @@ public class ZenModeConfig implements Parcelable { DEFAULT_SUPPRESSED_VISUAL_EFFECTS); } else if (MANUAL_TAG.equals(tag)) { rt.manualRule = readRuleXml(parser); - } else if (AUTOMATIC_TAG.equals(tag)) { + } else if (AUTOMATIC_TAG.equals(tag) + || (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag))) { final String id = parser.getAttributeValue(null, RULE_ATT_ID); final ZenRule automaticRule = readRuleXml(parser); if (id != null && automaticRule != null) { automaticRule.id = id; - rt.automaticRules.put(id, automaticRule); + if (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag)) { + String deletedRuleKey = deletedRuleKey(automaticRule); + if (deletedRuleKey != null) { + rt.deletedRules.put(deletedRuleKey, automaticRule); + } + } else if (AUTOMATIC_TAG.equals(tag)) { + rt.automaticRules.put(id, automaticRule); + } } } else if (STATE_TAG.equals(tag)) { rt.areChannelsBypassingDnd = safeBoolean(parser, @@ -660,13 +694,24 @@ public class ZenModeConfig implements Parcelable { throw new IllegalStateException("Failed to reach END_DOCUMENT"); } + /** Generates the map key used for a {@link ZenRule} in {@link #deletedRules}. */ + @Nullable + public static String deletedRuleKey(ZenRule rule) { + if (rule.pkg != null && rule.conditionId != null) { + return rule.pkg + "|" + rule.conditionId.toString(); + } else { + return null; + } + } + /** * Writes XML of current ZenModeConfig * @param out serializer * @param version uses XML_VERSION if version is null * @throws IOException */ - public void writeXml(TypedXmlSerializer out, Integer version) throws IOException { + public void writeXml(TypedXmlSerializer out, Integer version, boolean forBackup) + throws IOException { out.startTag(null, ZEN_TAG); out.attribute(null, ZEN_ATT_VERSION, version == null ? Integer.toString(XML_VERSION) : Integer.toString(version)); @@ -707,6 +752,15 @@ public class ZenModeConfig implements Parcelable { writeRuleXml(automaticRule, out); out.endTag(null, AUTOMATIC_TAG); } + if (Flags.modesApi() && !forBackup) { + for (int i = 0; i < deletedRules.size(); i++) { + final ZenRule deletedRule = deletedRules.valueAt(i); + out.startTag(null, AUTOMATIC_DELETED_TAG); + out.attribute(null, RULE_ATT_ID, deletedRule.id); + writeRuleXml(deletedRule, out); + out.endTag(null, AUTOMATIC_DELETED_TAG); + } + } out.startTag(null, STATE_TAG); out.attributeBoolean(null, STATE_ATT_CHANNELS_BYPASSING_DND, areChannelsBypassingDnd); @@ -752,6 +806,11 @@ public class ZenModeConfig implements Parcelable { rt.triggerDescription = parser.getAttributeValue(null, RULE_ATT_TRIGGER_DESC); rt.type = safeInt(parser, RULE_ATT_TYPE, AutomaticZenRule.TYPE_UNKNOWN); rt.userModifiedFields = safeInt(parser, RULE_ATT_USER_MODIFIED_FIELDS, 0); + Long deletionInstant = tryParseLong( + parser.getAttributeValue(null, RULE_ATT_DELETION_INSTANT), null); + if (deletionInstant != null) { + rt.deletionInstant = Instant.ofEpochMilli(deletionInstant); + } } return rt; } @@ -799,6 +858,10 @@ public class ZenModeConfig implements Parcelable { } out.attributeInt(null, RULE_ATT_TYPE, rule.type); out.attributeInt(null, RULE_ATT_USER_MODIFIED_FIELDS, rule.userModifiedFields); + if (rule.deletionInstant != null) { + out.attributeLong(null, RULE_ATT_DELETION_INSTANT, + rule.deletionInstant.toEpochMilli()); + } } } @@ -1998,6 +2061,7 @@ public class ZenModeConfig implements Parcelable { public String iconResName; public boolean allowManualInvocation; public int userModifiedFields; + @Nullable public Instant deletionInstant; // Only set on deleted rules. public ZenRule() { } @@ -2031,6 +2095,9 @@ public class ZenModeConfig implements Parcelable { triggerDescription = source.readString(); type = source.readInt(); userModifiedFields = source.readInt(); + if (source.readInt() == 1) { + deletionInstant = Instant.ofEpochMilli(source.readLong()); + } } } @@ -2091,6 +2158,12 @@ public class ZenModeConfig implements Parcelable { dest.writeString(triggerDescription); dest.writeInt(type); dest.writeInt(userModifiedFields); + if (deletionInstant != null) { + dest.writeInt(1); + dest.writeLong(deletionInstant.toEpochMilli()); + } else { + dest.writeInt(0); + } } } @@ -2121,6 +2194,9 @@ public class ZenModeConfig implements Parcelable { .append(",triggerDescription=").append(triggerDescription) .append(",type=").append(type) .append(",userModifiedFields=").append(userModifiedFields); + if (deletionInstant != null) { + sb.append(",deletionInstant=").append(deletionInstant); + } } return sb.append(']').toString(); @@ -2180,7 +2256,8 @@ public class ZenModeConfig implements Parcelable { && Objects.equals(other.iconResName, iconResName) && Objects.equals(other.triggerDescription, triggerDescription) && other.type == type - && other.userModifiedFields == userModifiedFields; + && other.userModifiedFields == userModifiedFields + && Objects.equals(other.deletionInstant, deletionInstant); } return finalEquals; @@ -2192,7 +2269,7 @@ public class ZenModeConfig implements Parcelable { return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, component, configurationActivity, pkg, id, enabler, zenPolicy, zenDeviceEffects, modified, allowManualInvocation, iconResName, - triggerDescription, type, userModifiedFields); + triggerDescription, type, userModifiedFields, deletionInstant); } return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, component, configurationActivity, pkg, id, enabler, zenPolicy, modified); diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java index 8902368072bf..91ef11cf1d2d 100644 --- a/core/java/android/service/notification/ZenModeDiff.java +++ b/core/java/android/service/notification/ZenModeDiff.java @@ -30,6 +30,11 @@ import java.util.Set; /** * ZenModeDiff is a utility class meant to encapsulate the diff between ZenModeConfigs and their * subcomponents (automatic and manual ZenRules). + * + * <p>Note that this class is intended to detect <em>meaningful</em> differences, so objects that + * are not identical (as per their {@code equals()} implementation) can still produce an empty diff + * if only "metadata" fields are updated. + * * @hide */ public class ZenModeDiff { @@ -467,7 +472,6 @@ public class ZenModeDiff { public static final String FIELD_ICON_RES = "iconResName"; public static final String FIELD_TRIGGER_DESCRIPTION = "triggerDescription"; public static final String FIELD_TYPE = "type"; - public static final String FIELD_USER_MODIFIED_FIELDS = "userModifiedFields"; // NOTE: new field strings must match the variable names in ZenModeConfig.ZenRule // Special field to track whether this rule became active or inactive @@ -563,10 +567,6 @@ public class ZenModeDiff { if (!Objects.equals(from.iconResName, to.iconResName)) { addField(FIELD_ICON_RES, new FieldDiff<>(from.iconResName, to.iconResName)); } - if (from.userModifiedFields != to.userModifiedFields) { - addField(FIELD_USER_MODIFIED_FIELDS, - new FieldDiff<>(from.userModifiedFields, to.userModifiedFields)); - } } } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 7aa7b7e1bfc1..9ddc362769f6 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -215,7 +215,6 @@ import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.LauncherApps; -import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManagerInternal; @@ -373,6 +372,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; +import java.time.Clock; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -2466,8 +2466,8 @@ public class NotificationManagerService extends SystemService { mMetricsLogger = new MetricsLogger(); mRankingHandler = rankingHandler; mConditionProviders = conditionProviders; - mZenModeHelper = new ZenModeHelper(getContext(), mHandler.getLooper(), mConditionProviders, - flagResolver, new ZenModeEventLogger(mPackageManagerClient)); + mZenModeHelper = new ZenModeHelper(getContext(), mHandler.getLooper(), Clock.systemUTC(), + mConditionProviders, flagResolver, new ZenModeEventLogger(mPackageManagerClient)); mZenModeHelper.addCallback(new ZenModeHelper.Callback() { @Override public void onConfigChanged() { diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 911643b1a634..afbf08d9b77d 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -117,6 +117,9 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.io.PrintWriter; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -130,9 +133,12 @@ public class ZenModeHelper { static final String TAG = "ZenModeHelper"; static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final String PACKAGE_ANDROID = "android"; + // The amount of time rules instances can exist without their owning app being installed. private static final int RULE_INSTANCE_GRACE_PERIOD = 1000 * 60 * 60 * 72; static final int RULE_LIMIT_PER_PACKAGE = 100; + private static final Duration DELETED_RULE_KEPT_FOR = Duration.ofDays(30); private static final String IMPLICIT_RULE_ID_PREFIX = "implicit_"; // + pkg_name @@ -148,6 +154,7 @@ public class ZenModeHelper { private final Context mContext; private final H mHandler; + private final Clock mClock; private final SettingsObserver mSettingsObserver; private final AppOpsManager mAppOps; private final NotificationManager mNotificationManager; @@ -189,11 +196,13 @@ public class ZenModeHelper { private String[] mPriorityOnlyDndExemptPackages; - public ZenModeHelper(Context context, Looper looper, ConditionProviders conditionProviders, + public ZenModeHelper(Context context, Looper looper, Clock clock, + ConditionProviders conditionProviders, SystemUiSystemPropertiesFlags.FlagResolver flagResolver, ZenModeEventLogger zenModeEventLogger) { mContext = context; mHandler = new H(looper); + mClock = clock; addCallback(mMetrics); mAppOps = context.getSystemService(AppOpsManager.class); mNotificationManager = context.getSystemService(NotificationManager.class); @@ -452,6 +461,7 @@ public class ZenModeHelper { newConfig = mConfig.copy(); ZenRule rule = new ZenRule(); populateZenRule(pkg, automaticZenRule, rule, origin, /* isNew= */ true); + rule = maybeRestoreRemovedRule(newConfig, rule, automaticZenRule, origin); newConfig.automaticRules.put(rule.id, rule); maybeReplaceDefaultRule(newConfig, automaticZenRule); @@ -463,6 +473,37 @@ public class ZenModeHelper { } } + private ZenRule maybeRestoreRemovedRule(ZenModeConfig config, ZenRule ruleToAdd, + AutomaticZenRule azrToAdd, @ConfigChangeOrigin int origin) { + if (!Flags.modesApi()) { + return ruleToAdd; + } + String deletedKey = ZenModeConfig.deletedRuleKey(ruleToAdd); + if (deletedKey == null) { + // Couldn't calculate the deletedRuleKey (condition or pkg null?). This should + // never happen for an app-provided rule because NMS validates both. + return ruleToAdd; + } + ZenRule ruleToRestore = config.deletedRules.get(deletedKey); + if (ruleToRestore == null) { + return ruleToAdd; // Cannot restore. + } + + // We have found a previous rule to maybe restore. Whether we do that or not, we don't need + // to keep it around (if not restored now, it won't be in future calls either). + config.deletedRules.remove(deletedKey); + ruleToRestore.deletionInstant = null; + + if (origin != UPDATE_ORIGIN_APP) { + return ruleToAdd; // Okay to create anew. + } + + // "Preserve" the previous rule by considering the azrToAdd an update instead. + // Only app-modifiable fields will actually be modified. + populateZenRule(ruleToRestore.pkg, azrToAdd, ruleToRestore, origin, /* isNew= */ false); + return ruleToRestore; + } + private static void maybeReplaceDefaultRule(ZenModeConfig config, AutomaticZenRule addedRule) { if (!Flags.modesApi()) { return; @@ -644,7 +685,7 @@ public class ZenModeHelper { ZenRule rule = new ZenRule(); rule.id = implicitRuleId(pkg); rule.pkg = pkg; - rule.creationTime = System.currentTimeMillis(); + rule.creationTime = mClock.millis(); Binder.withCleanCallingIdentity(() -> { try { @@ -664,7 +705,7 @@ public class ZenModeHelper { rule.condition = null; rule.conditionId = new Uri.Builder() .scheme(Condition.SCHEME) - .authority("android") + .authority(PACKAGE_ANDROID) .appendPath("implicit") .appendPath(pkg) .build(); @@ -693,7 +734,9 @@ public class ZenModeHelper { if (ruleToRemove == null) return false; if (canManageAutomaticZenRule(ruleToRemove)) { newConfig.automaticRules.remove(id); - if (ruleToRemove.getPkg() != null && !"android".equals(ruleToRemove.getPkg())) { + maybePreserveRemovedRule(newConfig, ruleToRemove, origin); + if (ruleToRemove.getPkg() != null + && !PACKAGE_ANDROID.equals(ruleToRemove.getPkg())) { for (ZenRule currRule : newConfig.automaticRules.values()) { if (currRule.getPkg() != null && currRule.getPkg().equals(ruleToRemove.getPkg())) { @@ -723,12 +766,44 @@ public class ZenModeHelper { ZenRule rule = newConfig.automaticRules.get(newConfig.automaticRules.keyAt(i)); if (Objects.equals(rule.getPkg(), packageName) && canManageAutomaticZenRule(rule)) { newConfig.automaticRules.removeAt(i); + maybePreserveRemovedRule(newConfig, rule, origin); + } + } + // If the system is clearing all rules this means DND access is revoked or the package + // was uninstalled, so also clear the preserved-deleted rules. + if (origin == UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI) { + for (int i = newConfig.deletedRules.size() - 1; i >= 0; i--) { + ZenRule rule = newConfig.deletedRules.get(newConfig.deletedRules.keyAt(i)); + if (Objects.equals(rule.getPkg(), packageName)) { + newConfig.deletedRules.removeAt(i); + } } } return setConfigLocked(newConfig, origin, reason, null, true, callingUid); } } + private void maybePreserveRemovedRule(ZenModeConfig config, ZenRule ruleToRemove, + @ConfigChangeOrigin int origin) { + if (!Flags.modesApi()) { + return; + } + // If an app deletes a previously customized rule, keep it around to preserve + // the user's customization when/if it's recreated later. + // We don't try to preserve system-owned rules because their conditionIds (used as + // deletedRuleKey) are not stable. This is almost moot anyway because an app cannot + // delete a system-owned rule. + if (origin == UPDATE_ORIGIN_APP && !ruleToRemove.canBeUpdatedByApp() + && !PACKAGE_ANDROID.equals(ruleToRemove.pkg)) { + String deletedKey = ZenModeConfig.deletedRuleKey(ruleToRemove); + if (deletedKey != null) { + ruleToRemove.deletionInstant = Instant.now(mClock); + // Overwrites a previously-deleted rule with the same conditionId, but that's okay. + config.deletedRules.put(deletedKey, ruleToRemove); + } + } + } + void setAutomaticZenRuleState(String id, Condition condition, @ConfigChangeOrigin int origin, int callingUid) { ZenModeConfig newConfig; @@ -919,7 +994,7 @@ public class ZenModeHelper { // These values can always be edited by the app, so we apply changes immediately. if (isNew) { rule.id = ZenModeConfig.newRuleId(); - rule.creationTime = System.currentTimeMillis(); + rule.creationTime = mClock.millis(); rule.component = automaticZenRule.getOwner(); rule.pkg = pkg; } @@ -1379,7 +1454,7 @@ public class ZenModeHelper { boolean hasDefaultRules = config.automaticRules.containsAll( ZenModeConfig.DEFAULT_RULE_IDS); - long time = System.currentTimeMillis(); + long time = Flags.modesApi() ? mClock.millis() : System.currentTimeMillis(); if (config.automaticRules != null && config.automaticRules.size() > 0) { for (ZenRule automaticRule : config.automaticRules.values()) { if (forRestore) { @@ -1419,6 +1494,12 @@ public class ZenModeHelper { Settings.Secure.putIntForUser(mContext.getContentResolver(), Settings.Secure.ZEN_SETTINGS_UPDATED, 1, userId); } + + if (Flags.modesApi() && forRestore) { + // Note: forBackup doesn't write deletedRules, but just in case. + config.deletedRules.clear(); + } + if (DEBUG) Log.d(TAG, reason); synchronized (mConfigLock) { setConfigLocked(config, null, @@ -1436,7 +1517,7 @@ public class ZenModeHelper { if (forBackup && mConfigs.keyAt(i) != userId) { continue; } - mConfigs.valueAt(i).writeXml(out, version); + mConfigs.valueAt(i).writeXml(out, version, forBackup); } } } @@ -1468,28 +1549,51 @@ public class ZenModeHelper { } /** - * Removes old rule instances whose owner is not installed. + * Cleans up obsolete rules: + * <ul> + * <li>Rule instances whose owner is not installed. + * <li>Deleted rules that were deleted more than 30 days ago. + * </ul> */ private void cleanUpZenRules() { - long currentTime = System.currentTimeMillis(); + Instant keptRuleThreshold = mClock.instant().minus(DELETED_RULE_KEPT_FOR); synchronized (mConfigLock) { final ZenModeConfig newConfig = mConfig.copy(); - if (newConfig.automaticRules != null) { - for (int i = newConfig.automaticRules.size() - 1; i >= 0; i--) { - ZenRule rule = newConfig.automaticRules.get(newConfig.automaticRules.keyAt(i)); - if (RULE_INSTANCE_GRACE_PERIOD < (currentTime - rule.creationTime)) { - try { - if (rule.getPkg() != null) { - mPm.getPackageInfo(rule.getPkg(), PackageManager.MATCH_ANY_USER); - } - } catch (PackageManager.NameNotFoundException e) { - newConfig.automaticRules.removeAt(i); + + deleteRulesWithoutOwner(newConfig.automaticRules); + if (Flags.modesApi()) { + deleteRulesWithoutOwner(newConfig.deletedRules); + for (int i = newConfig.deletedRules.size() - 1; i >= 0; i--) { + ZenRule deletedRule = newConfig.deletedRules.valueAt(i); + if (deletedRule.deletionInstant == null + || deletedRule.deletionInstant.isBefore(keptRuleThreshold)) { + newConfig.deletedRules.removeAt(i); + } + } + } + + if (!newConfig.equals(mConfig)) { + setConfigLocked(newConfig, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, + "cleanUpZenRules", Process.SYSTEM_UID); + } + } + } + + private void deleteRulesWithoutOwner(ArrayMap<String, ZenRule> ruleList) { + long currentTime = Flags.modesApi() ? mClock.millis() : System.currentTimeMillis(); + if (ruleList != null) { + for (int i = ruleList.size() - 1; i >= 0; i--) { + ZenRule rule = ruleList.valueAt(i); + if (RULE_INSTANCE_GRACE_PERIOD < (currentTime - rule.creationTime)) { + try { + if (rule.getPkg() != null) { + mPm.getPackageInfo(rule.getPkg(), PackageManager.MATCH_ANY_USER); } + } catch (PackageManager.NameNotFoundException e) { + ruleList.removeAt(i); } } } - setConfigLocked(newConfig, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "cleanUpZenRules", - Process.SYSTEM_UID); } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java index 177d64555899..dd252f3ffd20 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -60,6 +60,7 @@ import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.time.Instant; @SmallTest @RunWith(AndroidJUnit4.class) @@ -407,6 +408,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.userModifiedFields = 16; rule.iconResName = ICON_RES_NAME; rule.triggerDescription = TRIGGER_DESC; + rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); Parcel parcel = Parcel.obtain(); rule.writeToParcel(parcel, 0); @@ -432,9 +434,10 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.userModifiedFields, parceled.userModifiedFields); assertEquals(rule.triggerDescription, parceled.triggerDescription); assertEquals(rule.zenPolicy, parceled.zenPolicy); + assertEquals(rule.deletionInstant, parceled.deletionInstant); + assertEquals(rule, parceled); assertEquals(rule.hashCode(), parceled.hashCode()); - } @Test @@ -510,6 +513,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.userModifiedFields = 4; rule.iconResName = ICON_RES_NAME; rule.triggerDescription = TRIGGER_DESC; + rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); ByteArrayOutputStream baos = new ByteArrayOutputStream(); writeRuleXml(rule, baos); @@ -539,6 +543,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.userModifiedFields, fromXml.userModifiedFields); assertEquals(rule.triggerDescription, fromXml.triggerDescription); assertEquals(rule.iconResName, fromXml.iconResName); + assertEquals(rule.deletionInstant, fromXml.deletionInstant); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java index 7e92e427b9a4..9d7cf53e62db 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java @@ -65,18 +65,22 @@ import java.util.Set; @TestableLooper.RunWithLooper public class ZenModeDiffTest extends UiServiceTestCase { // Base set of exempt fields independent of fields that are enabled/disabled via flags. - // version is not included in the diff; manual & automatic rules have special handling + // version is not included in the diff; manual & automatic rules have special handling; + // deleted rules are not included in the diff. public static final Set<String> ZEN_MODE_CONFIG_EXEMPT_FIELDS = - Set.of("version", "manualRule", "automaticRules"); + android.app.Flags.modesApi() + ? Set.of("version", "manualRule", "automaticRules", "deletedRules") + : Set.of("version", "manualRule", "automaticRules"); // Differences for flagged fields are only generated if the flag is enabled. - // TODO: b/310620812 - Remove this exempt list when flag is inlined. + // "Metadata" fields (userModifiedFields, deletionInstant) are not compared. private static final Set<String> ZEN_RULE_EXEMPT_FIELDS = android.app.Flags.modesApi() - ? Set.of() + ? Set.of("userModifiedFields", "deletionInstant") : Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION, RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL, - RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, RuleDiff.FIELD_USER_MODIFIED_FIELDS); + RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, "userModifiedFields", + "deletionInstant"); // allowPriorityChannels is flagged by android.app.modes_api public static final Set<String> ZEN_MODE_CONFIG_FLAGGED_FIELDS = diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 0224ff35219b..9e3e336fa12f 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -43,6 +43,7 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.provider.Settings.Global.ZEN_MODE_ALARMS; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; import static android.provider.Settings.Global.ZEN_MODE_OFF; @@ -92,6 +93,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; +import android.Manifest; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.AppGlobals; @@ -104,6 +106,7 @@ import android.content.ComponentName; import android.content.ContentResolver; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; @@ -116,6 +119,7 @@ import android.media.VolumePolicy; import android.net.Uri; import android.os.Parcel; import android.os.Process; +import android.os.SimpleClock; import android.os.UserHandle; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; @@ -172,12 +176,16 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.Reader; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; @SmallTest @SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service. @@ -232,6 +240,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Mock PackageManager mPackageManager; private Resources mResources; private TestableLooper mTestableLooper; + private final TestClock mTestClock = new TestClock(); private ZenModeHelper mZenModeHelper; private ContentResolver mContentResolver; @Mock DeviceEffectsApplier mDeviceEffectsApplier; @@ -269,7 +278,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mConditionProviders.addSystemProvider(new CountdownConditionProvider()); mConditionProviders.addSystemProvider(new ScheduleConditionProvider()); mZenModeEventLogger = new ZenModeEventLoggerFake(mPackageManager); - mZenModeHelper = new ZenModeHelper(mContext, mTestableLooper.getLooper(), + mZenModeHelper = new ZenModeHelper(mContext, mTestableLooper.getLooper(), mTestClock, mConditionProviders, mTestFlagResolver, mZenModeEventLogger); ResolveInfo ri = new ResolveInfo(); @@ -1198,7 +1207,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void ruleUidAutomaticZenRuleRemovedUpdatesCache() throws Exception { when(mContext.checkCallingPermission(anyString())) - .thenReturn(PackageManager.PERMISSION_GRANTED); + .thenReturn(PERMISSION_GRANTED); setupZenConfig(); // one enabled automatic rule @@ -1780,7 +1789,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void testDoNotUpdateModifiedDefaultAutoRule() { // mDefaultConfig is set to default config in setup by getDefaultConfigParser when(mContext.checkCallingPermission(anyString())) - .thenReturn(PackageManager.PERMISSION_GRANTED); + .thenReturn(PERMISSION_GRANTED); // shouldn't update rule that's been modified ZenModeConfig.ZenRule updatedDefaultRule = new ZenModeConfig.ZenRule(); @@ -1806,7 +1815,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void testDoNotUpdateEnabledDefaultAutoRule() { // mDefaultConfig is set to default config in setup by getDefaultConfigParser when(mContext.checkCallingPermission(anyString())) - .thenReturn(PackageManager.PERMISSION_GRANTED); + .thenReturn(PERMISSION_GRANTED); // shouldn't update the rule that's enabled ZenModeConfig.ZenRule updatedDefaultRule = new ZenModeConfig.ZenRule(); @@ -1833,7 +1842,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // mDefaultConfig is set to default config in setup by getDefaultConfigParser final String defaultRuleName = "rule name test"; when(mContext.checkCallingPermission(anyString())) - .thenReturn(PackageManager.PERMISSION_GRANTED); + .thenReturn(PERMISSION_GRANTED); // will update rule that is not enabled and modified ZenModeConfig.ZenRule customDefaultRule = new ZenModeConfig.ZenRule(); @@ -4318,6 +4327,324 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + public void removeAndAddAutomaticZenRule_wasCustomized_isRestored() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + + // Start with a rule. + mZenModeHelper.mConfig.automaticRules.clear(); + mTestClock.setNowMillis(1000); + AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build()) + .build(); + String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, + UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); + assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000); + assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).canUpdate()).isTrue(); + + // User customizes it. + AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule) + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build()) + .build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate", + Process.SYSTEM_UID); + + // App deletes it. + mTestClock.advanceByMillis(1000); + mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it", + CUSTOM_PKG_UID); + assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); + assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1); + + // App adds it again. + mTestClock.advanceByMillis(1000); + String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, + UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID); + + // Verify that the rule was restored: + // - id and creation time is the same as the original one. + // - ZenPolicy is the one that the user had set. + // - rule still has the user-modified fields. + AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId); + assertThat(finalRule.getCreationTime()).isEqualTo(1000); // And not 3000. + assertThat(newRuleId).isEqualTo(ruleId); + assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS); + assertThat(finalRule.getZenPolicy().getPriorityCategoryRepeatCallers()).isEqualTo( + ZenPolicy.STATE_ALLOW); + assertThat(finalRule.getUserModifiedFields()).isEqualTo( + AutomaticZenRule.FIELD_INTERRUPTION_FILTER); + assertThat(finalRule.getZenPolicy().getUserModifiedFields()).isEqualTo( + ZenPolicy.FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS); + + // Also, we discarded the "deleted rule" since we already used it for restoration. + assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0); + } + + @Test + public void removeAndAddAutomaticZenRule_wasNotCustomized_isNotRestored() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + + // Start with a single rule. + mZenModeHelper.mConfig.automaticRules.clear(); + mTestClock.setNowMillis(1000); + AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build()) + .build(); + String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, + UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); + assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000); + + // App deletes it. + mTestClock.advanceByMillis(1000); + mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it", + CUSTOM_PKG_UID); + assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); + assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0); + + // App adds it again. + mTestClock.advanceByMillis(1000); + String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, + UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID); + + // Verify that the rule was recreated. This means id and creation time are new. + AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId); + assertThat(finalRule.getCreationTime()).isEqualTo(3000); + assertThat(newRuleId).isNotEqualTo(ruleId); + } + + @Test + public void removeAndAddAutomaticZenRule_recreatedButNotByApp_isNotRestored() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + + // Start with a single rule. + mZenModeHelper.mConfig.automaticRules.clear(); + mTestClock.setNowMillis(1000); + AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build()) + .build(); + String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, + UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); + assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000); + + // User customizes it. + mTestClock.advanceByMillis(1000); + AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule) + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build()) + .build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate", + Process.SYSTEM_UID); + + // App deletes it. + mTestClock.advanceByMillis(1000); + mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it", + CUSTOM_PKG_UID); + assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); + assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1); + + // User creates it again (unusual case, but ok). + mTestClock.advanceByMillis(1000); + String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, + UPDATE_ORIGIN_USER, "add it anew", CUSTOM_PKG_UID); + + // Verify that the rule was recreated. This means id and creation time are new, and the rule + // matches the latest data supplied to addAZR. + AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId); + assertThat(finalRule.getCreationTime()).isEqualTo(4000); + assertThat(newRuleId).isNotEqualTo(ruleId); + assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY); + assertThat(finalRule.getZenPolicy().getPriorityCategoryRepeatCallers()).isEqualTo( + ZenPolicy.STATE_DISALLOW); + + // Also, we discarded the "deleted rule" since we're not interested in recreating it. + assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0); + } + + @Test + public void removeAndAddAutomaticZenRule_removedByUser_isNotRestored() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + + // Start with a single rule. + mZenModeHelper.mConfig.automaticRules.clear(); + mTestClock.setNowMillis(1000); + AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build()) + .build(); + String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, + UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); + assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000); + + // User customizes it. + mTestClock.advanceByMillis(1000); + AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule) + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build()) + .build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate", + Process.SYSTEM_UID); + + // User deletes it. + mTestClock.advanceByMillis(1000); + mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_USER, "delete it", + CUSTOM_PKG_UID); + assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); + assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0); + + // App creates it again. + mTestClock.advanceByMillis(1000); + String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, + UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID); + + // Verify that the rule was recreated. This means id and creation time are new. + AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId); + assertThat(finalRule.getCreationTime()).isEqualTo(4000); + assertThat(newRuleId).isNotEqualTo(ruleId); + } + + @Test + public void removeAutomaticZenRule_preservedForRestoringByPackageAndConditionId() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mContext.getTestablePermissions().setPermission(Manifest.permission.MANAGE_NOTIFICATIONS, + PERMISSION_GRANTED); // So that canManageAZR passes although packages don't match. + mZenModeHelper.mConfig.automaticRules.clear(); + + // Start with a bunch of customized rules where conditionUris are not unique. + String id1 = mZenModeHelper.addAutomaticZenRule("pkg1", + new AutomaticZenRule.Builder("Test1", Uri.parse("uri1")).build(), UPDATE_ORIGIN_APP, + "add it", CUSTOM_PKG_UID); + String id2 = mZenModeHelper.addAutomaticZenRule("pkg1", + new AutomaticZenRule.Builder("Test2", Uri.parse("uri2")).build(), UPDATE_ORIGIN_APP, + "add it", CUSTOM_PKG_UID); + String id3 = mZenModeHelper.addAutomaticZenRule("pkg1", + new AutomaticZenRule.Builder("Test3", Uri.parse("uri2")).build(), UPDATE_ORIGIN_APP, + "add it", CUSTOM_PKG_UID); + String id4 = mZenModeHelper.addAutomaticZenRule("pkg2", + new AutomaticZenRule.Builder("Test4", Uri.parse("uri1")).build(), UPDATE_ORIGIN_APP, + "add it", CUSTOM_PKG_UID); + String id5 = mZenModeHelper.addAutomaticZenRule("pkg2", + new AutomaticZenRule.Builder("Test5", Uri.parse("uri1")).build(), UPDATE_ORIGIN_APP, + "add it", CUSTOM_PKG_UID); + for (ZenRule zenRule : mZenModeHelper.mConfig.automaticRules.values()) { + zenRule.userModifiedFields = AutomaticZenRule.FIELD_INTERRUPTION_FILTER; + } + + mZenModeHelper.removeAutomaticZenRule(id1, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(id2, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(id3, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(id4, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(id5, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID); + + assertThat(mZenModeHelper.mConfig.deletedRules.keySet()) + .containsExactly("pkg1|uri1", "pkg1|uri2", "pkg2|uri1"); + assertThat(mZenModeHelper.mConfig.deletedRules.values().stream().map(zr -> zr.name) + .collect(Collectors.toList())) + .containsExactly("Test1", "Test3", "Test5"); + } + + @Test + public void removeAllZenRules_preservedForRestoring() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mZenModeHelper.mConfig.automaticRules.clear(); + + mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + new AutomaticZenRule.Builder("Test1", Uri.parse("uri1")).build(), UPDATE_ORIGIN_APP, + "add it", CUSTOM_PKG_UID); + mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + new AutomaticZenRule.Builder("Test2", Uri.parse("uri2")).build(), UPDATE_ORIGIN_APP, + "add it", CUSTOM_PKG_UID); + + for (ZenRule zenRule : mZenModeHelper.mConfig.automaticRules.values()) { + zenRule.userModifiedFields = AutomaticZenRule.FIELD_INTERRUPTION_FILTER; + } + + mZenModeHelper.removeAutomaticZenRules(mContext.getPackageName(), UPDATE_ORIGIN_APP, + "begone", CUSTOM_PKG_UID); + + assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(2); + } + + @Test + public void removeAllZenRules_fromSystem_deletesPreservedRulesToo() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mZenModeHelper.mConfig.automaticRules.clear(); + + // Start with deleted rules from 2 different packages. + Instant now = Instant.ofEpochMilli(1701796461000L); + ZenRule pkg1Rule = newZenRule("pkg1", now.minus(1, ChronoUnit.DAYS), now); + ZenRule pkg2Rule = newZenRule("pkg2", now.minus(2, ChronoUnit.DAYS), now); + mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg1Rule), pkg1Rule); + mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg2Rule), pkg2Rule); + + mZenModeHelper.removeAutomaticZenRules("pkg1", + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "goodbye pkg1", Process.SYSTEM_UID); + + // Preserved rules from pkg1 are gone; those from pkg2 are still there. + assertThat(mZenModeHelper.mConfig.deletedRules.values().stream().map(r -> r.pkg) + .collect(Collectors.toSet())).containsExactly("pkg2"); + } + + @Test + public void testRuleCleanup() throws Exception { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + Instant now = Instant.ofEpochMilli(1701796461000L); + Instant yesterday = now.minus(1, ChronoUnit.DAYS); + Instant aWeekAgo = now.minus(7, ChronoUnit.DAYS); + Instant twoMonthsAgo = now.minus(60, ChronoUnit.DAYS); + mTestClock.setNowMillis(now.toEpochMilli()); + + when(mPackageManager.getPackageInfo(eq("good_pkg"), anyInt())) + .thenReturn(new PackageInfo()); + when(mPackageManager.getPackageInfo(eq("bad_pkg"), anyInt())) + .thenThrow(new PackageManager.NameNotFoundException("bad_pkg is not here")); + + // Set up a config for another user containing: + ZenModeConfig config = new ZenModeConfig(); + config.user = 42; + mZenModeHelper.mConfigs.put(42, config); + // okay rules (not deleted, package exists, with a range of creation dates). + config.automaticRules.put("ar1", newZenRule("good_pkg", now, null)); + config.automaticRules.put("ar2", newZenRule("good_pkg", yesterday, null)); + config.automaticRules.put("ar3", newZenRule("good_pkg", twoMonthsAgo, null)); + // newish rules for a missing package + config.automaticRules.put("ar4", newZenRule("bad_pkg", yesterday, null)); + // oldish rules belonging to a missing package + config.automaticRules.put("ar5", newZenRule("bad_pkg", aWeekAgo, null)); + // rules deleted recently + config.deletedRules.put("del1", newZenRule("good_pkg", twoMonthsAgo, yesterday)); + config.deletedRules.put("del2", newZenRule("good_pkg", twoMonthsAgo, aWeekAgo)); + // rules deleted a long time ago + config.deletedRules.put("del3", newZenRule("good_pkg", twoMonthsAgo, twoMonthsAgo)); + // rules for a missing package, created recently and deleted recently + config.deletedRules.put("del4", newZenRule("bad_pkg", yesterday, now)); + // rules for a missing package, created a long time ago and deleted recently + config.deletedRules.put("del5", newZenRule("bad_pkg", twoMonthsAgo, now)); + // rules for a missing package, created a long time ago and deleted a long time ago + config.deletedRules.put("del6", newZenRule("bad_pkg", twoMonthsAgo, twoMonthsAgo)); + + mZenModeHelper.onUserUnlocked(42); // copies config and cleans it up. + + assertThat(mZenModeHelper.mConfig.automaticRules.keySet()) + .containsExactly("ar1", "ar2", "ar3", "ar4"); + assertThat(mZenModeHelper.mConfig.deletedRules.keySet()) + .containsExactly("del1", "del2", "del4"); + } + + private static ZenRule newZenRule(String pkg, Instant createdAt, @Nullable Instant deletedAt) { + ZenRule rule = new ZenRule(); + rule.pkg = pkg; + rule.creationTime = createdAt.toEpochMilli(); + rule.deletionInstant = deletedAt; + // Plus stuff so that isValidAutomaticRule() passes + rule.name = "A rule from " + pkg + " created on " + createdAt; + rule.conditionId = Uri.parse(rule.name); + return rule; + } + + @Test public void applyGlobalZenModeAsImplicitZenRule_createsImplicitRuleAndActivatesIt() { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); mZenModeHelper.mConfig.automaticRules.clear(); @@ -4919,4 +5246,25 @@ public class ZenModeHelperTest extends UiServiceTestCase { return parser.nextTag(); } } + + private static class TestClock extends SimpleClock { + private long mNowMillis = 441644400000L; + + private TestClock() { + super(ZoneOffset.UTC); + } + + @Override + public long millis() { + return mNowMillis; + } + + private void setNowMillis(long millis) { + mNowMillis = millis; + } + + private void advanceByMillis(long millis) { + mNowMillis += millis; + } + } } |