diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 16c0910f12730..19397ed3ebaae 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -113,6 +113,7 @@ interface INotificationManager int getAppsBypassingDndCount(int uid); ParceledListSlice getNotificationChannelsBypassingDnd(String pkg, int userId); boolean isPackagePaused(String pkg); + void deleteNotificationHistoryItem(String pkg, int uid, long postedTime); void silenceNotificationSound(); diff --git a/core/java/android/app/NotificationHistory.java b/core/java/android/app/NotificationHistory.java index 909a476f3c941..f26e628743477 100644 --- a/core/java/android/app/NotificationHistory.java +++ b/core/java/android/app/NotificationHistory.java @@ -345,6 +345,26 @@ public final class NotificationHistory implements Parcelable { poolStringsFromNotifications(); } + /** + * Removes an individual historical notification and regenerates the string pool + */ + public boolean removeNotificationFromWrite(String packageName, long postedTime) { + boolean removed = false; + for (int i = mNotificationsToWrite.size() - 1; i >= 0; i--) { + HistoricalNotification hn = mNotificationsToWrite.get(i); + if (packageName.equals(hn.getPackage()) + && postedTime == hn.getPostedTimeMs()) { + removed = true; + mNotificationsToWrite.remove(i); + } + } + if (removed) { + poolStringsFromNotifications(); + } + + return removed; + } + /** * Gets pooled strings in order to write them to disk */ diff --git a/core/tests/coretests/src/android/app/NotificationHistoryTest.java b/core/tests/coretests/src/android/app/NotificationHistoryTest.java index 0a21875fd77de..d1608d0556043 100644 --- a/core/tests/coretests/src/android/app/NotificationHistoryTest.java +++ b/core/tests/coretests/src/android/app/NotificationHistoryTest.java @@ -233,6 +233,40 @@ public class NotificationHistoryTest { .containsExactlyElementsIn(postRemoveExpectedEntries); } + @Test + public void testRemoveNotificationFromWrite() { + NotificationHistory history = new NotificationHistory(); + + List postRemoveExpectedEntries = new ArrayList<>(); + List postRemoveExpectedStrings = new ArrayList<>(); + for (int i = 1; i <= 10; i++) { + HistoricalNotification n = getHistoricalNotification("pkg", i); + + if (987654323 != n.getPostedTimeMs()) { + postRemoveExpectedStrings.add(n.getPackage()); + postRemoveExpectedStrings.add(n.getChannelName()); + postRemoveExpectedStrings.add(n.getChannelId()); + postRemoveExpectedEntries.add(n); + } + + history.addNotificationToWrite(n); + } + + history.poolStringsFromNotifications(); + + assertThat(history.getNotificationsToWrite().size()).isEqualTo(10); + // 1 package name and 10 unique channel names and ids + assertThat(history.getPooledStringsToWrite().length).isEqualTo(21); + + history.removeNotificationFromWrite("pkg", 987654323); + + + // 1 package names and 9 * 2 unique channel names and ids + assertThat(history.getPooledStringsToWrite().length).isEqualTo(19); + assertThat(history.getNotificationsToWrite()) + .containsExactlyElementsIn(postRemoveExpectedEntries); + } + @Test public void testParceling() { NotificationHistory history = new NotificationHistory(); diff --git a/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java index e8cb1632bdc32..061cbd8a1a402 100644 --- a/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java +++ b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java @@ -170,6 +170,11 @@ public class NotificationHistoryDatabase { mFileWriteHandler.post(rpr); } + public void deleteNotificationHistoryItem(String pkg, long postedTime) { + RemoveNotificationRunnable rnr = new RemoveNotificationRunnable(pkg, postedTime); + mFileWriteHandler.post(rnr); + } + public void addNotification(final HistoricalNotification notification) { synchronized (mLock) { mBuffer.addNewNotificationToWrite(notification); @@ -367,6 +372,49 @@ public class NotificationHistoryDatabase { } } + final class RemoveNotificationRunnable implements Runnable { + private String mPkg; + private long mPostedTime; + private NotificationHistory mNotificationHistory; + + public RemoveNotificationRunnable(String pkg, long postedTime) { + mPkg = pkg; + mPostedTime = postedTime; + } + + @VisibleForTesting + void setNotificationHistory(NotificationHistory nh) { + mNotificationHistory = nh; + } + + @Override + public void run() { + if (DEBUG) Slog.d(TAG, "RemovePackageRunnable"); + synchronized (mLock) { + // Remove from pending history + mBuffer.removeNotificationFromWrite(mPkg, mPostedTime); + + Iterator historyFileItr = mHistoryFiles.iterator(); + while (historyFileItr.hasNext()) { + final AtomicFile af = historyFileItr.next(); + try { + NotificationHistory notificationHistory = mNotificationHistory != null + ? mNotificationHistory + : new NotificationHistory(); + readLocked(af, notificationHistory, + new NotificationHistoryFilter.Builder().build()); + if(notificationHistory.removeNotificationFromWrite(mPkg, mPostedTime)) { + writeLocked(af, notificationHistory); + } + } catch (Exception e) { + Slog.e(TAG, "Cannot clean up file on notification removal " + + af.getBaseFile().getName(), e); + } + } + } + } + } + public static final class NotificationHistoryFileAttrProvider implements NotificationHistoryDatabase.FileAttrProvider { final static String TAG = "NotifHistoryFileDate"; diff --git a/services/core/java/com/android/server/notification/NotificationHistoryManager.java b/services/core/java/com/android/server/notification/NotificationHistoryManager.java index 41bc29f7a82e5..9aab0fd912e84 100644 --- a/services/core/java/com/android/server/notification/NotificationHistoryManager.java +++ b/services/core/java/com/android/server/notification/NotificationHistoryManager.java @@ -130,7 +130,7 @@ public class NotificationHistoryManager { } } - public void onPackageRemoved(int userId, String packageName) { + public void onPackageRemoved(@UserIdInt int userId, String packageName) { synchronized (mLock) { if (!mUserUnlockedStates.get(userId, false)) { if (mHistoryEnabled.get(userId, false)) { @@ -150,6 +150,22 @@ public class NotificationHistoryManager { } } + public void deleteNotificationHistoryItem(String pkg, int uid, long postedTime) { + synchronized (mLock) { + int userId = UserHandle.getUserId(uid); + final NotificationHistoryDatabase userHistory = + getUserHistoryAndInitializeIfNeededLocked(userId); + // TODO: it shouldn't be possible to delete a notification entry while the user is + // locked but we should handle it + if (userHistory == null) { + Slog.w(TAG, "Attempted to remove notif for locked/gone/disabled user " + + userId); + return; + } + userHistory.deleteNotificationHistoryItem(pkg, postedTime); + } + } + // TODO: wire this up to AMS when power button is long pressed public void triggerWriteToDisk() { synchronized (mLock) { diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 46dc21bbeaadf..f8777d6187769 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -2780,7 +2780,7 @@ public class NotificationManagerService extends SystemService { if (!isSystemToast) { int count = 0; final int N = mToastQueue.size(); - for (int i=0; i creationDates = new HashMap<>(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java index b5eb1bf318486..2c548be185c90 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java @@ -288,6 +288,20 @@ public class NotificationHistoryManagerTest extends UiServiceTestCase { verify(userHistoryAll, never()).onPackageRemoved(pkg); } + @Test + public void testDeleteNotificationHistoryItem_userUnlocked() { + String pkg = "pkg"; + long time = 235; + NotificationHistoryDatabase userHistory = mock(NotificationHistoryDatabase.class); + + mHistoryManager.onUserUnlocked(USER_SYSTEM); + mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistory); + + mHistoryManager.deleteNotificationHistoryItem(pkg, 1, time); + + verify(userHistory, times(1)).deleteNotificationHistoryItem(pkg, time); + } + @Test public void testTriggerWriteToDisk() { NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class);