From b219c62302f6c06cc6a30679ef22ef76580bd220 Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Fri, 15 Jan 2021 10:03:19 -0500 Subject: [PATCH] Add retention policy to delete channels - On initial upgrade, all currently deleted channels will be cleaned up - Moving forward, deleted channels will be retained for 30 days before being permanently removed Test: atest PreferencesHelperTest, NotificationChannelTest Bug: 177246841 Change-Id: Ibfd33d38e9c7cef51e00ac0b63f295c8734009a6 --- core/api/test-current.txt | 1 + .../java/android/app/NotificationChannel.java | 33 ++++++- .../notification/PreferencesHelper.java | 33 +++++-- .../notification/PreferencesHelperTest.java | 90 +++++++++++++++++++ 4 files changed, 149 insertions(+), 8 deletions(-) diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 015ae64a9257a..6a06f4e181018 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -254,6 +254,7 @@ package android.app { method public boolean isImportanceLockedByOEM(); method public void lockFields(int); method public void setDeleted(boolean); + method public void setDeletedTimeMs(long); method public void setDemoted(boolean); method public void setFgServiceShown(boolean); method public void setImportanceLockedByCriticalDeviceFunction(boolean); diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java index b1a8f9b0ba336..323af8211d76a 100644 --- a/core/java/android/app/NotificationChannel.java +++ b/core/java/android/app/NotificationChannel.java @@ -32,6 +32,7 @@ import android.os.Parcelable; import android.provider.Settings; import android.service.notification.NotificationListenerService; import android.text.TextUtils; +import android.util.Slog; import android.util.TypedXmlPullParser; import android.util.TypedXmlSerializer; import android.util.proto.ProtoOutputStream; @@ -111,6 +112,7 @@ public final class NotificationChannel implements Parcelable { private static final String ATT_CONVERSATION_ID = "conv_id"; private static final String ATT_IMP_CONVERSATION = "imp_conv"; private static final String ATT_DEMOTE = "dem"; + private static final String ATT_DELETED_TIME_MS = "del_time"; private static final String DELIMITER = ","; /** @@ -183,6 +185,7 @@ public final class NotificationChannel implements Parcelable { NotificationManager.IMPORTANCE_UNSPECIFIED; private static final boolean DEFAULT_DELETED = false; private static final boolean DEFAULT_SHOW_BADGE = true; + private static final long DEFAULT_DELETION_TIME_MS = -1; @UnsupportedAppUsage private String mId; @@ -214,6 +217,7 @@ public final class NotificationChannel implements Parcelable { private String mConversationId = null; private boolean mDemoted = false; private boolean mImportantConvo = false; + private long mDeletedTime = DEFAULT_DELETION_TIME_MS; /** * Creates a notification channel. @@ -282,6 +286,7 @@ public final class NotificationChannel implements Parcelable { mConversationId = in.readString(); mDemoted = in.readBoolean(); mImportantConvo = in.readBoolean(); + mDeletedTime = in.readLong(); } @Override @@ -341,6 +346,7 @@ public final class NotificationChannel implements Parcelable { dest.writeString(mConversationId); dest.writeBoolean(mDemoted); dest.writeBoolean(mImportantConvo); + dest.writeLong(mDeletedTime); } /** @@ -374,6 +380,14 @@ public final class NotificationChannel implements Parcelable { mDeleted = deleted; } + /** + * @hide + */ + @TestApi + public void setDeletedTimeMs(long time) { + mDeletedTime = time; + } + /** * @hide */ @@ -763,6 +777,13 @@ public final class NotificationChannel implements Parcelable { return mDeleted; } + /** + * @hide + */ + public long getDeletedTimeMs() { + return mDeletedTime; + } + /** * @hide */ @@ -906,6 +927,8 @@ public final class NotificationChannel implements Parcelable { enableVibration(safeBool(parser, ATT_VIBRATION_ENABLED, false)); setShowBadge(safeBool(parser, ATT_SHOW_BADGE, false)); setDeleted(safeBool(parser, ATT_DELETED, false)); + setDeletedTimeMs(XmlUtils.readLongAttribute( + parser, ATT_DELETED_TIME_MS, DEFAULT_DELETION_TIME_MS)); setGroup(parser.getAttributeValue(null, ATT_GROUP)); lockFields(safeInt(parser, ATT_USER_LOCKED, 0)); setFgServiceShown(safeBool(parser, ATT_FG_SERVICE_SHOWN, false)); @@ -1024,6 +1047,9 @@ public final class NotificationChannel implements Parcelable { if (isDeleted()) { out.attributeBoolean(null, ATT_DELETED, isDeleted()); } + if (getDeletedTimeMs() >= 0) { + out.attributeLong(null, ATT_DELETED_TIME_MS, getDeletedTimeMs()); + } if (getGroup() != null) { out.attribute(null, ATT_GROUP, getGroup()); } @@ -1091,6 +1117,7 @@ public final class NotificationChannel implements Parcelable { record.put(ATT_VIBRATION, longArrayToString(getVibrationPattern())); record.put(ATT_SHOW_BADGE, Boolean.toString(canShowBadge())); record.put(ATT_DELETED, Boolean.toString(isDeleted())); + record.put(ATT_DELETED_TIME_MS, Long.toString(getDeletedTimeMs())); record.put(ATT_GROUP, getGroup()); record.put(ATT_BLOCKABLE_SYSTEM, isBlockable()); record.put(ATT_ALLOW_BUBBLE, getAllowBubbles()); @@ -1182,6 +1209,7 @@ public final class NotificationChannel implements Parcelable { && mVibrationEnabled == that.mVibrationEnabled && mShowBadge == that.mShowBadge && isDeleted() == that.isDeleted() + && getDeletedTimeMs() == that.getDeletedTimeMs() && isBlockable() == that.isBlockable() && mAllowBubbles == that.mAllowBubbles && Objects.equals(getId(), that.getId()) @@ -1205,8 +1233,8 @@ public final class NotificationChannel implements Parcelable { int result = Objects.hash(getId(), getName(), mDesc, getImportance(), mBypassDnd, getLockscreenVisibility(), getSound(), mLights, getLightColor(), getUserLockedFields(), - isFgServiceShown(), mVibrationEnabled, mShowBadge, isDeleted(), getGroup(), - getAudioAttributes(), isBlockable(), mAllowBubbles, + isFgServiceShown(), mVibrationEnabled, mShowBadge, isDeleted(), getDeletedTimeMs(), + getGroup(), getAudioAttributes(), isBlockable(), mAllowBubbles, mImportanceLockedByOEM, mImportanceLockedDefaultApp, mOriginalImportance, mParentId, mConversationId, mDemoted, mImportantConvo); result = 31 * result + Arrays.hashCode(mVibration); @@ -1247,6 +1275,7 @@ public final class NotificationChannel implements Parcelable { + ", mVibrationEnabled=" + mVibrationEnabled + ", mShowBadge=" + mShowBadge + ", mDeleted=" + mDeleted + + ", mDeletedTimeMs=" + mDeletedTime + ", mGroup='" + mGroup + '\'' + ", mAudioAttributes=" + mAudioAttributes + ", mBlockableSystem=" + mBlockableSystem diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index cbd973aeaf6a6..61c8b178b4c0d 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -50,6 +50,7 @@ import android.service.notification.ConversationChannelWrapper; import android.service.notification.NotificationListenerService; import android.service.notification.RankingHelperProto; import android.text.TextUtils; +import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.IntArray; @@ -101,6 +102,7 @@ public class PreferencesHelper implements RankingConfig { private static final int NOTIFICATION_PREFERENCES_PULL_LIMIT = 1000; private static final int NOTIFICATION_CHANNEL_PULL_LIMIT = 2000; private static final int NOTIFICATION_CHANNEL_GROUP_PULL_LIMIT = 1000; + private static final int NOTIFICATION_CHANNEL_DELETION_RETENTION_DAYS = 30; @VisibleForTesting static final String TAG_RANKING = "ranking"; @@ -324,12 +326,8 @@ public class PreferencesHelper implements RankingConfig { channel.setImportanceLockedByOEM(true); } } - boolean isInvalidShortcutChannel = - channel.getConversationId() != null && - channel.getConversationId().contains( - PLACEHOLDER_CONVERSATION_ID); - if (mAllowInvalidShortcuts || (!mAllowInvalidShortcuts - && !isInvalidShortcutChannel)) { + + if (isShortcutOk(channel) && isDeletionOk(channel)) { r.channels.put(id, channel); } } @@ -369,6 +367,26 @@ public class PreferencesHelper implements RankingConfig { throw new IllegalStateException("Failed to reach END_DOCUMENT"); } + private boolean isShortcutOk(NotificationChannel channel) { + boolean isInvalidShortcutChannel = + channel.getConversationId() != null && + channel.getConversationId().contains( + PLACEHOLDER_CONVERSATION_ID); + return mAllowInvalidShortcuts || (!mAllowInvalidShortcuts && !isInvalidShortcutChannel); + } + + private boolean isDeletionOk(NotificationChannel nc) { + if (!nc.isDeleted()) { + return true; + } + long boundary = System.currentTimeMillis() - ( + DateUtils.DAY_IN_MILLIS * NOTIFICATION_CHANNEL_DELETION_RETENTION_DAYS); + if (nc.getDeletedTimeMs() <= boundary) { + return false; + } + return true; + } + private PackagePreferences getPackagePreferencesLocked(String pkg, int uid) { final String key = packagePreferencesKey(pkg, uid); return mPackagePreferences.get(key); @@ -828,6 +846,7 @@ public class PreferencesHelper implements RankingConfig { if (existing.isDeleted()) { // The existing channel was deleted - undelete it. existing.setDeleted(false); + existing.setDeletedTimeMs(-1); needsPolicyFileChange = true; wasUndeleted = true; @@ -1119,6 +1138,7 @@ public class PreferencesHelper implements RankingConfig { private void deleteNotificationChannelLocked(NotificationChannel channel, String pkg, int uid) { if (!channel.isDeleted()) { channel.setDeleted(true); + channel.setDeletedTimeMs(System.currentTimeMillis()); LogMaker lm = getChannelLog(channel, pkg); lm.setType(com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_CLOSE); MetricsLogger.action(lm); @@ -1479,6 +1499,7 @@ public class PreferencesHelper implements RankingConfig { if (nc.getConversationId() != null && conversationIds.contains(nc.getConversationId())) { nc.setDeleted(true); + nc.setDeletedTimeMs(System.currentTimeMillis()); LogMaker lm = getChannelLog(nc, pkg); lm.setType( com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_CLOSE); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 4d2a4784b5d9d..8c744c94249b3 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -95,6 +95,7 @@ import android.provider.Settings.Secure; import android.service.notification.ConversationChannelWrapper; import android.test.suitebuilder.annotation.SmallTest; import android.testing.TestableContentResolver; +import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.IntArray; @@ -3292,6 +3293,95 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertNotNull(mHelper.getNotificationChannel(PKG_O, UID_O, "id", true)); } + @Test + public void testDeleted_noTime() throws Exception { + mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mLogger, + mAppOpsManager, mStatsEventBuilderFactory); + + final String xml = "\n" + + "\n" + + "" + + "" + + ""; + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream(new ByteArrayInputStream(xml.getBytes())), + null); + parser.nextTag(); + mHelper.readXml(parser, false, UserHandle.USER_ALL); + + assertNull(mHelper.getNotificationChannel(PKG_O, UID_O, "id", true)); + } + + @Test + public void testDeleted_recentTime() throws Exception { + mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mLogger, + mAppOpsManager, mStatsEventBuilderFactory); + + mHelper.createNotificationChannel( + PKG_P, UID_P, new NotificationChannel("id", "id", 2), true, false); + mHelper.deleteNotificationChannel(PKG_P, UID_P, "id"); + NotificationChannel nc1 = mHelper.getNotificationChannel(PKG_P, UID_P, "id", true); + assertTrue(DateUtils.isToday(nc1.getDeletedTimeMs())); + assertTrue(nc1.isDeleted()); + + ByteArrayOutputStream baos = writeXmlAndPurge(PKG_P, UID_P, false, + UserHandle.USER_SYSTEM, "id", NotificationChannel.DEFAULT_CHANNEL_ID); + + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())), + null); + parser.nextTag(); + mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mLogger, + mAppOpsManager, mStatsEventBuilderFactory); + mHelper.readXml(parser, true, UserHandle.USER_SYSTEM); + + NotificationChannel nc = mHelper.getNotificationChannel(PKG_P, UID_P, "id", true); + assertTrue(DateUtils.isToday(nc.getDeletedTimeMs())); + assertTrue(nc.isDeleted()); + } + + @Test + public void testUnDelete_time() throws Exception { + mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mLogger, + mAppOpsManager, mStatsEventBuilderFactory); + + mHelper.createNotificationChannel( + PKG_P, UID_P, new NotificationChannel("id", "id", 2), true, false); + mHelper.deleteNotificationChannel(PKG_P, UID_P, "id"); + NotificationChannel nc1 = mHelper.getNotificationChannel(PKG_P, UID_P, "id", true); + assertTrue(DateUtils.isToday(nc1.getDeletedTimeMs())); + assertTrue(nc1.isDeleted()); + + mHelper.createNotificationChannel( + PKG_P, UID_P, new NotificationChannel("id", "id", 2), true, false); + nc1 = mHelper.getNotificationChannel(PKG_P, UID_P, "id", true); + assertEquals(-1, nc1.getDeletedTimeMs()); + assertFalse(nc1.isDeleted()); + } + + @Test + public void testDeleted_longTime() throws Exception { + mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mLogger, + mAppOpsManager, mStatsEventBuilderFactory); + + long time = System.currentTimeMillis() - (DateUtils.DAY_IN_MILLIS * 30); + + final String xml = "\n" + + "\n" + + "" + + "" + + ""; + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream(new ByteArrayInputStream(xml.getBytes())), + null); + parser.nextTag(); + mHelper.readXml(parser, false, UserHandle.USER_ALL); + + NotificationChannel nc = mHelper.getNotificationChannel(PKG_O, UID_O, "id", true); + assertNull(nc); + } + @Test public void testGetConversations_all() { String convoId = "convo";