diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index e885fd38c2ffb..9e83136b978d5 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";