diff options
| author | 2025-02-13 12:45:48 +0100 | |
|---|---|---|
| committer | 2025-02-14 13:28:10 +0100 | |
| commit | dfce623b20a061b9ff6426ae918d4ab4622ce041 (patch) | |
| tree | 749a700b21b6b1f2fe711f71111da04412632c13 | |
| parent | 6dd098aca7921ad73629365c4046e73d37947949 (diff) | |
Remove long unused (and non-user-modified) implicit ZenRules
We now track the last time each ZenRule was activated. On ZenModeConfig load (e.g. reboot, user switch) if 30 days have passed since the last activation of an implicit rule, it is deleted. For rules created before we started tracking activation, we also set lastActivation=now -- in essence, so that we wait 30 days before deciding whether they are actually used or not.
As a side-change, rename ZenRule.canBeUpdatedByApp() to ZenRule.isUserModified(), inverting is logic. The latter is the condition that almost all callsites are interested in.
Bug: 394087495
Test: atest ZenModeConfigTest ZenModeHelperTest
Flag: android.app.modes_cleanup_implicit
Change-Id: If9f7916e59b47871f14bd4b1b4efaf6cd2d41af7
6 files changed, 455 insertions, 85 deletions
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index bb4f556532f7..8e6b88c66408 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -83,6 +83,13 @@ flag { } flag { + name: "modes_cleanup_implicit" + namespace: "systemui" + description: "Deletes implicit modes if never customized and not used for some time. Depends on MODES_UI" + bug: "394087495" +} + +flag { name: "api_tvextender" is_exported: true namespace: "systemui" diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 4011574da879..6f94c1b2d274 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -309,6 +309,7 @@ public class ZenModeConfig implements Parcelable { private static final String RULE_ATT_DISABLED_ORIGIN = "disabledOrigin"; private static final String RULE_ATT_LEGACY_SUPPRESSED_EFFECTS = "legacySuppressedEffects"; private static final String RULE_ATT_CONDITION_OVERRIDE = "conditionOverride"; + private static final String RULE_ATT_LAST_ACTIVATION = "lastActivation"; private static final String DEVICE_EFFECT_DISPLAY_GRAYSCALE = "zdeDisplayGrayscale"; private static final String DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY = @@ -1187,11 +1188,7 @@ public class ZenModeConfig implements Parcelable { rt.zenPolicyUserModifiedFields = safeInt(parser, POLICY_USER_MODIFIED_FIELDS, 0); rt.zenDeviceEffectsUserModifiedFields = safeInt(parser, DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0); - Long deletionInstant = tryParseLong( - parser.getAttributeValue(null, RULE_ATT_DELETION_INSTANT), null); - if (deletionInstant != null) { - rt.deletionInstant = Instant.ofEpochMilli(deletionInstant); - } + rt.deletionInstant = safeInstant(parser, RULE_ATT_DELETION_INSTANT, null); if (Flags.modesUi()) { rt.disabledOrigin = safeInt(parser, RULE_ATT_DISABLED_ORIGIN, ORIGIN_UNKNOWN); @@ -1199,6 +1196,9 @@ public class ZenModeConfig implements Parcelable { RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, 0); rt.conditionOverride = safeInt(parser, RULE_ATT_CONDITION_OVERRIDE, ZenRule.OVERRIDE_NONE); + if (Flags.modesCleanupImplicit()) { + rt.lastActivation = safeInstant(parser, RULE_ATT_LAST_ACTIVATION, null); + } } return rt; @@ -1249,10 +1249,7 @@ public class ZenModeConfig implements Parcelable { out.attributeInt(null, POLICY_USER_MODIFIED_FIELDS, rule.zenPolicyUserModifiedFields); out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS, rule.zenDeviceEffectsUserModifiedFields); - if (rule.deletionInstant != null) { - out.attributeLong(null, RULE_ATT_DELETION_INSTANT, - rule.deletionInstant.toEpochMilli()); - } + writeXmlAttributeInstant(out, RULE_ATT_DELETION_INSTANT, rule.deletionInstant); if (Flags.modesUi()) { out.attributeInt(null, RULE_ATT_DISABLED_ORIGIN, rule.disabledOrigin); out.attributeInt(null, RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, @@ -1260,6 +1257,16 @@ public class ZenModeConfig implements Parcelable { if (rule.conditionOverride == ZenRule.OVERRIDE_ACTIVATE && !forBackup) { out.attributeInt(null, RULE_ATT_CONDITION_OVERRIDE, rule.conditionOverride); } + if (Flags.modesCleanupImplicit()) { + writeXmlAttributeInstant(out, RULE_ATT_LAST_ACTIVATION, rule.lastActivation); + } + } + } + + private static void writeXmlAttributeInstant(TypedXmlSerializer out, String att, + @Nullable Instant instant) throws IOException { + if (instant != null) { + out.attributeLong(null, att, instant.toEpochMilli()); } } @@ -1600,6 +1607,19 @@ public class ZenModeConfig implements Parcelable { return values; } + @Nullable + private static Instant safeInstant(TypedXmlPullParser parser, String att, + @Nullable Instant defValue) { + final String strValue = parser.getAttributeValue(null, att); + if (!TextUtils.isEmpty(strValue)) { + Long longValue = tryParseLong(strValue, null); + if (longValue != null) { + return Instant.ofEpochMilli(longValue); + } + } + return defValue; + } + @Override public int describeContents() { return 0; @@ -2598,6 +2618,18 @@ public class ZenModeConfig implements Parcelable { @ConditionOverride int conditionOverride = OVERRIDE_NONE; + /** + * Last time at which the rule was activated (for any reason, including overrides). + * If {@code null}, the rule has never been activated since its creation. + * + * <p>Note that this was previously untracked, so it will also be {@code null} for rules + * created before we started tracking and never activated since -- make sure to account for + * it, for example by falling back to {@link #creationTime} in logic involving this field. + */ + @Nullable + @FlaggedApi(Flags.FLAG_MODES_CLEANUP_IMPLICIT) + public Instant lastActivation; + public ZenRule() { } public ZenRule(Parcel source) { @@ -2635,24 +2667,29 @@ public class ZenModeConfig implements Parcelable { disabledOrigin = source.readInt(); legacySuppressedEffects = source.readInt(); conditionOverride = source.readInt(); + if (Flags.modesCleanupImplicit()) { + if (source.readInt() == 1) { + lastActivation = Instant.ofEpochMilli(source.readLong()); + } + } } } /** - * Whether this ZenRule can be updated by an app. In general, rules that have been - * customized by the user cannot be further updated by an app, with some exceptions: + * Whether this ZenRule has been customized by the user in any way. + + * <p>In general, rules that have been customized by the user cannot be further updated by + * an app, with some exceptions: * <ul> * <li>Non user-configurable fields, like type, icon, configurationActivity, etc. * <li>Name, if the name was not specifically modified by the user (to support language * switches). * </ul> */ - public boolean canBeUpdatedByApp() { - // The rule is considered updateable if its bitmask has no user modifications, and - // the bitmasks of the policy and device effects have no modification. - return userModifiedFields == 0 - && zenPolicyUserModifiedFields == 0 - && zenDeviceEffectsUserModifiedFields == 0; + public boolean isUserModified() { + return userModifiedFields != 0 + || zenPolicyUserModifiedFields != 0 + || zenDeviceEffectsUserModifiedFields != 0; } @Override @@ -2708,6 +2745,14 @@ public class ZenModeConfig implements Parcelable { dest.writeInt(disabledOrigin); dest.writeInt(legacySuppressedEffects); dest.writeInt(conditionOverride); + if (Flags.modesCleanupImplicit()) { + if (lastActivation != null) { + dest.writeInt(1); + dest.writeLong(lastActivation.toEpochMilli()); + } else { + dest.writeInt(0); + } + } } } @@ -2760,6 +2805,9 @@ public class ZenModeConfig implements Parcelable { if (Flags.modesUi()) { sb.append(",disabledOrigin=").append(disabledOrigin); sb.append(",legacySuppressedEffects=").append(legacySuppressedEffects); + if (Flags.modesCleanupImplicit()) { + sb.append(",lastActivation=").append(lastActivation); + } } return sb.append(']').toString(); @@ -2838,6 +2886,10 @@ public class ZenModeConfig implements Parcelable { && other.disabledOrigin == disabledOrigin && other.legacySuppressedEffects == legacySuppressedEffects && other.conditionOverride == conditionOverride; + if (Flags.modesCleanupImplicit()) { + finalEquals = finalEquals + && Objects.equals(other.lastActivation, lastActivation); + } } return finalEquals; @@ -2846,13 +2898,23 @@ public class ZenModeConfig implements Parcelable { @Override public int hashCode() { if (Flags.modesUi()) { - return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, - component, configurationActivity, pkg, id, enabler, zenPolicy, - zenDeviceEffects, allowManualInvocation, iconResName, - triggerDescription, type, userModifiedFields, - zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields, - deletionInstant, disabledOrigin, legacySuppressedEffects, - conditionOverride); + if (Flags.modesCleanupImplicit()) { + return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, + component, configurationActivity, pkg, id, enabler, zenPolicy, + zenDeviceEffects, allowManualInvocation, iconResName, + triggerDescription, type, userModifiedFields, + zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields, + deletionInstant, disabledOrigin, legacySuppressedEffects, + conditionOverride, lastActivation); + } else { + return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, + component, configurationActivity, pkg, id, enabler, zenPolicy, + zenDeviceEffects, allowManualInvocation, iconResName, + triggerDescription, type, userModifiedFields, + zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields, + deletionInstant, disabledOrigin, legacySuppressedEffects, + conditionOverride); + } } else { return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, component, configurationActivity, pkg, id, enabler, zenPolicy, diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index f7a4d3d9132c..889df512dd60 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -157,6 +157,12 @@ public class ZenModeHelper { static final int RULE_LIMIT_PER_PACKAGE = 100; private static final Duration DELETED_RULE_KEPT_FOR = Duration.ofDays(30); + /** + * Amount of time since last activation after which implicit rules that have never been + * customized by the user are automatically cleaned up. + */ + private static final Duration IMPLICIT_RULE_KEPT_FOR = Duration.ofDays(30); + private static final int MAX_ICON_RESOURCE_NAME_LENGTH = 1000; /** @@ -534,7 +540,7 @@ public class ZenModeHelper { ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); if (sleepingRule != null && !sleepingRule.enabled - && sleepingRule.canBeUpdatedByApp() /* meaning it's not user-customized */) { + && !sleepingRule.isUserModified()) { config.automaticRules.remove(ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); } } @@ -864,7 +870,7 @@ public class ZenModeHelper { // 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 == ORIGIN_APP && !ruleToRemove.canBeUpdatedByApp() + if (origin == ORIGIN_APP && ruleToRemove.isUserModified() && !PACKAGE_ANDROID.equals(ruleToRemove.pkg)) { String deletedKey = ZenModeConfig.deletedRuleKey(ruleToRemove); if (deletedKey != null) { @@ -1282,7 +1288,7 @@ public class ZenModeHelper { // * the request comes from an origin that can always update values, like the user, or // * the rule has not yet been user modified, and thus can be updated by the app. boolean updateValues = isNew || doesOriginAlwaysUpdateValues(origin) - || rule.canBeUpdatedByApp(); + || !rule.isUserModified(); // For all other values, if updates are not allowed, we discard the update. if (!updateValues) { @@ -1914,6 +1920,7 @@ public class ZenModeHelper { * <ul> * <li>Rule instances whose owner is not installed. * <li>Deleted rules that were deleted more than 30 days ago. + * <li>Implicit rules that haven't been used in 30 days (and have not been customized). * </ul> */ private void cleanUpZenRules() { @@ -1932,6 +1939,10 @@ public class ZenModeHelper { } } + if (Flags.modesUi() && Flags.modesCleanupImplicit()) { + deleteUnusedImplicitRules(newConfig.automaticRules); + } + if (!newConfig.equals(mConfig)) { setConfigLocked(newConfig, null, ORIGIN_SYSTEM, "cleanUpZenRules", Process.SYSTEM_UID); @@ -1957,6 +1968,29 @@ public class ZenModeHelper { } } + private void deleteUnusedImplicitRules(ArrayMap<String, ZenRule> ruleList) { + if (ruleList == null) { + return; + } + Instant deleteIfUnusedSince = mClock.instant().minus(IMPLICIT_RULE_KEPT_FOR); + + for (int i = ruleList.size() - 1; i >= 0; i--) { + ZenRule rule = ruleList.valueAt(i); + if (isImplicitRuleId(rule.id) && !rule.isUserModified()) { + if (rule.lastActivation == null) { + // This rule existed before we started tracking activation time. It *might* be + // in use. Set lastActivation=now so it has some time (IMPLICIT_RULE_KEPT_FOR) + // before being removed if truly unused. + rule.lastActivation = mClock.instant(); + } + + if (rule.lastActivation.isBefore(deleteIfUnusedSince)) { + ruleList.removeAt(i); + } + } + } + } + /** * @return a copy of the zen mode configuration */ @@ -2091,6 +2125,20 @@ public class ZenModeHelper { } } + // Update last activation for rules that are being activated. + if (Flags.modesUi() && Flags.modesCleanupImplicit()) { + Instant now = mClock.instant(); + if (!mConfig.isManualActive() && config.isManualActive()) { + config.manualRule.lastActivation = now; + } + for (ZenRule rule : config.automaticRules.values()) { + ZenRule previousRule = mConfig.automaticRules.get(rule.id); + if (rule.isActive() && (previousRule == null || !previousRule.isActive())) { + rule.lastActivation = now; + } + } + } + mConfig = config; dispatchOnConfigChanged(); updateAndApplyConsolidatedPolicyAndDeviceEffects(origin, reason); 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 4c1544f14667..67efb9e76692 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -488,33 +488,33 @@ public class ZenModeConfigTest extends UiServiceTestCase { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.zenPolicy = null; rule.zenDeviceEffects = null; - assertThat(rule.canBeUpdatedByApp()).isTrue(); + assertThat(rule.isUserModified()).isFalse(); rule.userModifiedFields = 1; - assertThat(rule.canBeUpdatedByApp()).isFalse(); + assertThat(rule.isUserModified()).isTrue(); } @Test public void testCanBeUpdatedByApp_policyModified() throws Exception { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.zenPolicy = new ZenPolicy(); - assertThat(rule.canBeUpdatedByApp()).isTrue(); + assertThat(rule.isUserModified()).isFalse(); rule.zenPolicyUserModifiedFields = 1; - assertThat(rule.canBeUpdatedByApp()).isFalse(); + assertThat(rule.isUserModified()).isTrue(); } @Test public void testCanBeUpdatedByApp_deviceEffectsModified() throws Exception { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.zenDeviceEffects = new ZenDeviceEffects.Builder().build(); - assertThat(rule.canBeUpdatedByApp()).isTrue(); + assertThat(rule.isUserModified()).isFalse(); rule.zenDeviceEffectsUserModifiedFields = 1; - assertThat(rule.canBeUpdatedByApp()).isFalse(); + assertThat(rule.isUserModified()).isTrue(); } @Test @@ -563,6 +563,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); if (Flags.modesUi()) { rule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; + if (Flags.modesCleanupImplicit()) { + rule.lastActivation = Instant.ofEpochMilli(456); + } } config.automaticRules.put(rule.id, rule); @@ -600,6 +603,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.deletionInstant, ruleActual.deletionInstant); if (Flags.modesUi()) { assertEquals(rule.disabledOrigin, ruleActual.disabledOrigin); + if (Flags.modesCleanupImplicit()) { + assertEquals(rule.lastActivation, ruleActual.lastActivation); + } } if (Flags.backupRestoreLogging()) { verify(logger).logItemsBackedUp(DATA_TYPE_ZEN_RULES, 2); @@ -633,6 +639,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); if (Flags.modesUi()) { rule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; + if (Flags.modesCleanupImplicit()) { + rule.lastActivation = Instant.ofEpochMilli(789); + } } Parcel parcel = Parcel.obtain(); @@ -664,6 +673,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.deletionInstant, parceled.deletionInstant); if (Flags.modesUi()) { assertEquals(rule.disabledOrigin, parceled.disabledOrigin); + if (Flags.modesCleanupImplicit()) { + assertEquals(rule.lastActivation, parceled.lastActivation); + } } assertEquals(rule, parceled); @@ -746,6 +758,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); if (Flags.modesUi()) { rule.disabledOrigin = ZenModeConfig.ORIGIN_APP; + if (Flags.modesCleanupImplicit()) { + rule.lastActivation = Instant.ofEpochMilli(123); + } } ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -781,6 +796,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.deletionInstant, fromXml.deletionInstant); if (Flags.modesUi()) { assertEquals(rule.disabledOrigin, fromXml.disabledOrigin); + if (Flags.modesCleanupImplicit()) { + assertEquals(rule.lastActivation, fromXml.lastActivation); + } } } @@ -908,7 +926,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME; assertThat(rule.userModifiedFields).isEqualTo(1); - assertThat(rule.canBeUpdatedByApp()).isFalse(); + assertThat(rule.isUserModified()).isTrue(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); writeRuleXml(rule, baos); @@ -916,7 +934,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { ZenModeConfig.ZenRule fromXml = readRuleXml(bais); assertThat(fromXml.userModifiedFields).isEqualTo(rule.userModifiedFields); - assertThat(fromXml.canBeUpdatedByApp()).isFalse(); + assertThat(fromXml.isUserModified()).isTrue(); } @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 8a5f80cb3e49..6d0bf8b322fd 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java @@ -475,7 +475,8 @@ public class ZenModeDiffTest extends UiServiceTestCase { // "Metadata" fields are never compared. Set<String> exemptFields = new LinkedHashSet<>( Set.of("userModifiedFields", "zenPolicyUserModifiedFields", - "zenDeviceEffectsUserModifiedFields", "deletionInstant", "disabledOrigin")); + "zenDeviceEffectsUserModifiedFields", "deletionInstant", "disabledOrigin", + "lastActivation")); // Flagged fields are only compared if their flag is on. if (Flags.modesUi()) { exemptFields.add(RuleDiff.FIELD_SNOOZING); // Obsolete. 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 4d2f105e27b3..0ab11e0cbe3d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -23,6 +23,7 @@ import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME; import static android.app.AutomaticZenRule.TYPE_THEATER; import static android.app.AutomaticZenRule.TYPE_UNKNOWN; import static android.app.Flags.FLAG_BACKUP_RESTORE_LOGGING; +import static android.app.Flags.FLAG_MODES_CLEANUP_IMPLICIT; import static android.app.Flags.FLAG_MODES_MULTIUSER; import static android.app.Flags.FLAG_MODES_UI; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ACTIVATED; @@ -124,7 +125,10 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; +import static java.time.temporal.ChronoUnit.DAYS; + import android.Manifest; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.AlarmManager; @@ -219,7 +223,6 @@ import java.io.Reader; import java.io.StringWriter; import java.time.Instant; import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Calendar; import java.util.LinkedList; @@ -2233,8 +2236,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConfig.automaticRules.put(implicitRuleBeforeModesUi.id, implicitRuleBeforeModesUi); // Plus one other normal rule. - ZenRule anotherRule = newZenRule("other_pkg", Instant.now(), null); - anotherRule.id = "other_rule"; + ZenRule anotherRule = newZenRule("other_rule", "other_pkg", Instant.now()); anotherRule.iconResName = "other_icon"; anotherRule.type = TYPE_IMMERSIVE; mZenModeHelper.mConfig.automaticRules.put(anotherRule.id, anotherRule); @@ -2271,8 +2273,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { implicitRuleWithModesUi); // Plus one other normal rule. - ZenRule anotherRule = newZenRule("other_pkg", Instant.now(), null); - anotherRule.id = "other_rule"; + ZenRule anotherRule = newZenRule("other_rule", "other_pkg", Instant.now()); anotherRule.iconResName = "other_icon"; anotherRule.type = TYPE_IMMERSIVE; mZenModeHelper.mConfig.automaticRules.put(anotherRule.id, anotherRule); @@ -4611,7 +4612,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); - assertThat(storedRule.canBeUpdatedByApp()).isTrue(); + assertThat(storedRule.isUserModified()).isFalse(); } @Test @@ -4719,7 +4720,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { STATE_DISALLOW); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); - assertThat(storedRule.canBeUpdatedByApp()).isFalse(); + assertThat(storedRule.isUserModified()).isTrue(); assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo( ZenPolicy.FIELD_ALLOW_CHANNELS | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS @@ -4761,7 +4762,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); - assertThat(storedRule.canBeUpdatedByApp()).isFalse(); + assertThat(storedRule.isUserModified()).isTrue(); assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo( ZenDeviceEffects.FIELD_GRAYSCALE); } @@ -5713,8 +5714,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // 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); + ZenRule pkg1Rule = newDeletedZenRule("1", "pkg1", now.minus(1, DAYS), now); + ZenRule pkg2Rule = newDeletedZenRule("2", "pkg2", now.minus(2, DAYS), now); mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg1Rule), pkg1Rule); mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg2Rule), pkg2Rule); @@ -5832,9 +5833,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testRuleCleanup() throws Exception { 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); + Instant yesterday = now.minus(1, DAYS); + Instant aWeekAgo = now.minus(7, DAYS); + Instant twoMonthsAgo = now.minus(60, DAYS); mTestClock.setNowMillis(now.toEpochMilli()); when(mPackageManager.getPackageInfo(eq("good_pkg"), anyInt())) @@ -5847,24 +5848,28 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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)); + config.automaticRules.put("ar1", newZenRule("ar1", "good_pkg", now)); + config.automaticRules.put("ar2", newZenRule("ar2", "good_pkg", yesterday)); + config.automaticRules.put("ar3", newZenRule("ar3", "good_pkg", twoMonthsAgo)); // newish rules for a missing package - config.automaticRules.put("ar4", newZenRule("bad_pkg", yesterday, null)); + config.automaticRules.put("ar4", newZenRule("ar4", "bad_pkg", yesterday)); // oldish rules belonging to a missing package - config.automaticRules.put("ar5", newZenRule("bad_pkg", aWeekAgo, null)); + config.automaticRules.put("ar5", newZenRule("ar5", "bad_pkg", aWeekAgo)); // rules deleted recently - config.deletedRules.put("del1", newZenRule("good_pkg", twoMonthsAgo, yesterday)); - config.deletedRules.put("del2", newZenRule("good_pkg", twoMonthsAgo, aWeekAgo)); + config.deletedRules.put("del1", + newDeletedZenRule("del1", "good_pkg", twoMonthsAgo, yesterday)); + config.deletedRules.put("del2", + newDeletedZenRule("del2", "good_pkg", twoMonthsAgo, aWeekAgo)); // rules deleted a long time ago - config.deletedRules.put("del3", newZenRule("good_pkg", twoMonthsAgo, twoMonthsAgo)); + config.deletedRules.put("del3", + newDeletedZenRule("del3", "good_pkg", twoMonthsAgo, twoMonthsAgo)); // rules for a missing package, created recently and deleted recently - config.deletedRules.put("del4", newZenRule("bad_pkg", yesterday, now)); + config.deletedRules.put("del4", newDeletedZenRule("del4", "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)); + config.deletedRules.put("del5", newDeletedZenRule("del5", "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)); + config.deletedRules.put("del6", + newDeletedZenRule("del6", "bad_pkg", twoMonthsAgo, twoMonthsAgo)); mZenModeHelper.onUserSwitched(42); // copies config and cleans it up. @@ -5874,14 +5879,115 @@ public class ZenModeHelperTest extends UiServiceTestCase { .containsExactly("del1", "del2", "del4"); } - private static ZenRule newZenRule(String pkg, Instant createdAt, @Nullable Instant deletedAt) { + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void testRuleCleanup_removesNotRecentlyUsedNotModifiedImplicitRules() throws Exception { + Instant now = Instant.ofEpochMilli(1701796461000L); + Instant yesterday = now.minus(1, DAYS); + Instant aWeekAgo = now.minus(7, DAYS); + Instant twoMonthsAgo = now.minus(60, DAYS); + Instant aYearAgo = now.minus(365, DAYS); + mTestClock.setNowMillis(now.toEpochMilli()); + when(mPackageManager.getPackageInfo(anyString(), anyInt())).thenReturn(new PackageInfo()); + + // Set up a config to be loaded, containing a bunch of implicit rules + ZenModeConfig config = new ZenModeConfig(); + config.user = 42; + mZenModeHelper.mConfigs.put(42, config); + // used recently + ZenRule usedRecently1 = newImplicitZenRule("pkg1", aYearAgo, yesterday); + ZenRule usedRecently2 = newImplicitZenRule("pkg2", aYearAgo, aWeekAgo); + config.automaticRules.put(usedRecently1.id, usedRecently1); + config.automaticRules.put(usedRecently2.id, usedRecently2); + // not used in a long time + ZenRule longUnused = newImplicitZenRule("pkg3", aYearAgo, twoMonthsAgo); + config.automaticRules.put(longUnused.id, longUnused); + // created a long time ago, before lastActivation tracking + ZenRule oldAndLastUsageUnknown = newImplicitZenRule("pkg4", twoMonthsAgo, null); + config.automaticRules.put(oldAndLastUsageUnknown.id, oldAndLastUsageUnknown); + // created a short time ago, before lastActivation tracking + ZenRule newAndLastUsageUnknown = newImplicitZenRule("pkg5", aWeekAgo, null); + config.automaticRules.put(newAndLastUsageUnknown.id, newAndLastUsageUnknown); + // not used in a long time, but was customized by user + ZenRule longUnusedButCustomized = newImplicitZenRule("pkg6", aYearAgo, twoMonthsAgo); + longUnusedButCustomized.zenPolicyUserModifiedFields = ZenPolicy.FIELD_CONVERSATIONS; + config.automaticRules.put(longUnusedButCustomized.id, longUnusedButCustomized); + // created a long time ago, before lastActivation tracking, and was customized by user + ZenRule oldAndLastUsageUnknownAndCustomized = newImplicitZenRule("pkg7", twoMonthsAgo, + null); + oldAndLastUsageUnknownAndCustomized.userModifiedFields = AutomaticZenRule.FIELD_ICON; + config.automaticRules.put(oldAndLastUsageUnknownAndCustomized.id, + oldAndLastUsageUnknownAndCustomized); + + mZenModeHelper.onUserSwitched(42); // copies config and cleans it up. + + // The recently used OR modified OR last-used-unknown rules stay. + assertThat(mZenModeHelper.mConfig.automaticRules.values()) + .comparingElementsUsing(IGNORE_METADATA) + .containsExactly(usedRecently1, usedRecently2, oldAndLastUsageUnknown, + newAndLastUsageUnknown, longUnusedButCustomized, + oldAndLastUsageUnknownAndCustomized); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void testRuleCleanup_assignsLastActivationToImplicitRules() throws Exception { + Instant now = Instant.ofEpochMilli(1701796461000L); + Instant aWeekAgo = now.minus(7, DAYS); + Instant aYearAgo = now.minus(365, DAYS); + mTestClock.setNowMillis(now.toEpochMilli()); + when(mPackageManager.getPackageInfo(anyString(), anyInt())).thenReturn(new PackageInfo()); + + // Set up a config to be loaded, containing implicit rules. + ZenModeConfig config = new ZenModeConfig(); + config.user = 42; + mZenModeHelper.mConfigs.put(42, config); + // with last activation known + ZenRule usedRecently = newImplicitZenRule("pkg1", aYearAgo, aWeekAgo); + config.automaticRules.put(usedRecently.id, usedRecently); + // created a long time ago, with last activation unknown + ZenRule oldAndLastUsageUnknown = newImplicitZenRule("pkg4", aYearAgo, null); + config.automaticRules.put(oldAndLastUsageUnknown.id, oldAndLastUsageUnknown); + // created a short time ago, with last activation unknown + ZenRule newAndLastUsageUnknown = newImplicitZenRule("pkg5", aWeekAgo, null); + config.automaticRules.put(newAndLastUsageUnknown.id, newAndLastUsageUnknown); + + mZenModeHelper.onUserSwitched(42); // copies config and cleans it up. + + // All rules stayed. + usedRecently = getZenRule(usedRecently.id); + oldAndLastUsageUnknown = getZenRule(oldAndLastUsageUnknown.id); + newAndLastUsageUnknown = getZenRule(newAndLastUsageUnknown.id); + + // The rules with an unknown last usage have been assigned a placeholder one. + assertThat(usedRecently.lastActivation).isEqualTo(aWeekAgo); + assertThat(oldAndLastUsageUnknown.lastActivation).isEqualTo(now); + assertThat(newAndLastUsageUnknown.lastActivation).isEqualTo(now); + } + + private static ZenRule newDeletedZenRule(String id, String pkg, Instant createdAt, + @NonNull Instant deletedAt) { + ZenRule rule = newZenRule(id, pkg, createdAt); + rule.deletionInstant = deletedAt; + return rule; + } + + private static ZenRule newImplicitZenRule(String pkg, @NonNull Instant createdAt, + @Nullable Instant lastActivatedAt) { + ZenRule implicitRule = newZenRule(implicitRuleId(pkg), pkg, createdAt); + implicitRule.lastActivation = lastActivatedAt; + return implicitRule; + } + + private static ZenRule newZenRule(String id, String pkg, Instant createdAt) { ZenRule rule = new ZenRule(); + rule.id = id; rule.pkg = pkg; rule.creationTime = createdAt.toEpochMilli(); rule.enabled = true; - rule.deletionInstant = deletedAt; + rule.deletionInstant = null; // Plus stuff so that isValidAutomaticRule() passes - rule.name = "A rule from " + pkg + " created on " + createdAt; + rule.name = "Rule " + id; rule.conditionId = Uri.parse(rule.name); return rule; } @@ -5919,11 +6025,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void getAutomaticZenRuleState_notOwnedRule_returnsStateUnknown() { // Assume existence of a system-owned rule that is currently ACTIVE. - ZenRule systemRule = newZenRule("android", Instant.now(), null); + ZenRule systemRule = newZenRule("systemRule", "android", Instant.now()); systemRule.zenMode = ZEN_MODE_ALARMS; systemRule.condition = new Condition(systemRule.conditionId, "on", Condition.STATE_TRUE); ZenModeConfig config = mZenModeHelper.mConfig.copy(); - config.automaticRules.put("systemRule", systemRule); + config.automaticRules.put(systemRule.id, systemRule); mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); @@ -5935,11 +6041,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void setAutomaticZenRuleState_idForNotOwnedRule_ignored() { // Assume existence of an other-package-owned rule that is currently ACTIVE. assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); - ZenRule otherRule = newZenRule("another.package", Instant.now(), null); + ZenRule otherRule = newZenRule("otherRule", "another.package", Instant.now()); otherRule.zenMode = ZEN_MODE_ALARMS; otherRule.condition = new Condition(otherRule.conditionId, "on", Condition.STATE_TRUE); ZenModeConfig config = mZenModeHelper.mConfig.copy(); - config.automaticRules.put("otherRule", otherRule); + config.automaticRules.put(otherRule.id, otherRule); mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); @@ -5955,11 +6061,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void setAutomaticZenRuleStateFromConditionProvider_conditionForNotOwnedRule_ignored() { // Assume existence of an other-package-owned rule that is currently ACTIVE. assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); - ZenRule otherRule = newZenRule("another.package", Instant.now(), null); + ZenRule otherRule = newZenRule("otherRule", "another.package", Instant.now()); otherRule.zenMode = ZEN_MODE_ALARMS; otherRule.condition = new Condition(otherRule.conditionId, "on", Condition.STATE_TRUE); ZenModeConfig config = mZenModeHelper.mConfig.copy(); - config.automaticRules.put("otherRule", otherRule); + config.automaticRules.put(otherRule.id, otherRule); mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); @@ -7255,6 +7361,125 @@ public class ZenModeHelperTest extends UiServiceTestCase { "config: setAzrStateFromCps: cond/cond (ORIGIN_APP) from uid " + CUSTOM_PKG_UID); } + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void setAutomaticZenRuleState_updatesLastActivation() { + String ruleOne = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, + new AutomaticZenRule.Builder("rule", CONDITION_ID) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(), + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + String ruleTwo = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, + new AutomaticZenRule.Builder("unrelated", Uri.parse("other.condition")) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(), + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleOne).lastActivation).isNull(); + assertThat(getZenRule(ruleTwo).lastActivation).isNull(); + + Instant firstActivation = Instant.ofEpochMilli(100); + mTestClock.setNow(firstActivation); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(firstActivation); + assertThat(getZenRule(ruleTwo).lastActivation).isNull(); + + mTestClock.setNow(Instant.ofEpochMilli(300)); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_FALSE, + ORIGIN_APP, CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(firstActivation); + assertThat(getZenRule(ruleTwo).lastActivation).isNull(); + + Instant secondActivation = Instant.ofEpochMilli(500); + mTestClock.setNow(secondActivation); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(secondActivation); + assertThat(getZenRule(ruleTwo).lastActivation).isNull(); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void setManualZenMode_updatesLastActivation() { + assertThat(mZenModeHelper.mConfig.manualRule.lastActivation).isNull(); + Instant instant = Instant.ofEpochMilli(100); + mTestClock.setNow(instant); + + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_ALARMS, null, + ORIGIN_USER_IN_SYSTEMUI, "reason", "systemui", SYSTEM_UID); + + assertThat(mZenModeHelper.mConfig.manualRule.lastActivation).isEqualTo(instant); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void applyGlobalZenModeAsImplicitZenRule_updatesLastActivation() { + Instant instant = Instant.ofEpochMilli(100); + mTestClock.setNow(instant); + + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, ZEN_MODE_ALARMS); + + ZenRule implicitRule = getZenRule(implicitRuleId(CUSTOM_PKG_NAME)); + assertThat(implicitRule.lastActivation).isEqualTo(instant); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void setAutomaticZenRuleState_notChangingActiveState_doesNotUpdateLastActivation() { + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, + new AutomaticZenRule.Builder("rule", CONDITION_ID) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(), + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleId).lastActivation).isNull(); + + // Manual activation comes first + Instant firstActivation = Instant.ofEpochMilli(100); + mTestClock.setNow(firstActivation); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); + + assertThat(getZenRule(ruleId).lastActivation).isEqualTo(firstActivation); + + // Now the app says the rule should be active (assume it's on a schedule, and the app + // doesn't listen to broadcasts so it doesn't know an override was present). This doesn't + // change the activation state. + mTestClock.setNow(Instant.ofEpochMilli(300)); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleId).lastActivation).isEqualTo(firstActivation); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void addOrUpdateRule_doesNotUpdateLastActivation() { + AutomaticZenRule azr = new AutomaticZenRule.Builder("rule", CONDITION_ID) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(); + + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, azr, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleId).lastActivation).isNull(); + + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, + new AutomaticZenRule.Builder(azr).setName("New name").build(), ORIGIN_APP, "reason", + CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleId).lastActivation).isNull(); + } + private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode, @Nullable ZenPolicy zenPolicy) { ZenRule rule = new ZenRule(); @@ -7272,22 +7497,27 @@ public class ZenModeHelperTest extends UiServiceTestCase { } private static final Correspondence<ZenRule, ZenRule> IGNORE_METADATA = - Correspondence.transforming(zr -> { - Parcel p = Parcel.obtain(); - try { - zr.writeToParcel(p, 0); - p.setDataPosition(0); - ZenRule copy = new ZenRule(p); - copy.creationTime = 0; - copy.userModifiedFields = 0; - copy.zenPolicyUserModifiedFields = 0; - copy.zenDeviceEffectsUserModifiedFields = 0; - return copy; - } finally { - p.recycle(); - } - }, - "Ignoring timestamp and userModifiedFields"); + Correspondence.transforming( + ZenModeHelperTest::cloneWithoutMetadata, + ZenModeHelperTest::cloneWithoutMetadata, + "Ignoring timestamps and userModifiedFields"); + + private static ZenRule cloneWithoutMetadata(ZenRule rule) { + Parcel p = Parcel.obtain(); + try { + rule.writeToParcel(p, 0); + p.setDataPosition(0); + ZenRule copy = new ZenRule(p); + copy.creationTime = 0; + copy.userModifiedFields = 0; + copy.zenPolicyUserModifiedFields = 0; + copy.zenDeviceEffectsUserModifiedFields = 0; + copy.lastActivation = null; + return copy; + } finally { + p.recycle(); + } + } private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy, @Nullable Boolean conditionActive) { @@ -7693,6 +7923,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { return mNowMillis; } + private void setNow(Instant instant) { + mNowMillis = instant.toEpochMilli(); + } + private void setNowMillis(long millis) { mNowMillis = millis; } |