Clean up settings and history for deleted convos

Test: atest
Fixes: 149556404
Change-Id: I998ecf39b794b4b3b3daca44875f9754797e0187
diff --git a/core/java/android/app/NotificationHistory.java b/core/java/android/app/NotificationHistory.java
index d16120d..8c2cc94 100644
--- a/core/java/android/app/NotificationHistory.java
+++ b/core/java/android/app/NotificationHistory.java
@@ -384,6 +384,26 @@
     }
 
     /**
+     * Removes all notifications from a conversation and regenerates the string pool
+     */
+    public boolean removeConversationFromWrite(String packageName, String conversationId) {
+        boolean removed = false;
+        for (int i = mNotificationsToWrite.size() - 1; i >= 0; i--) {
+            HistoricalNotification hn = mNotificationsToWrite.get(i);
+            if (packageName.equals(hn.getPackage())
+                    && conversationId.equals(hn.getConversationId())) {
+                removed = true;
+                mNotificationsToWrite.remove(i);
+            }
+        }
+        if (removed) {
+            poolStringsFromNotifications();
+        }
+
+        return removed;
+    }
+
+    /**
      * Gets pooled strings in order to write them to disk
      */
     public @NonNull String[] getPooledStringsToWrite() {
diff --git a/core/tests/coretests/src/android/app/NotificationHistoryTest.java b/core/tests/coretests/src/android/app/NotificationHistoryTest.java
index 8d8acb7..0443d3a 100644
--- a/core/tests/coretests/src/android/app/NotificationHistoryTest.java
+++ b/core/tests/coretests/src/android/app/NotificationHistoryTest.java
@@ -289,6 +289,44 @@
     }
 
     @Test
+    public void testRemoveConversationNotificationFromWrite() {
+        NotificationHistory history = new NotificationHistory();
+
+        List<HistoricalNotification> postRemoveExpectedEntries = new ArrayList<>();
+        List<String> postRemoveExpectedStrings = new ArrayList<>();
+        for (int i = 1; i <= 10; i++) {
+            HistoricalNotification n = getHistoricalNotification("pkg", i);
+
+            if (i != 2) {
+                postRemoveExpectedStrings.add(n.getPackage());
+                postRemoveExpectedStrings.add(n.getChannelName());
+                postRemoveExpectedStrings.add(n.getChannelId());
+                if (n.getConversationId() != null) {
+                    postRemoveExpectedStrings.add(n.getConversationId());
+                }
+                postRemoveExpectedEntries.add(n);
+            }
+
+            history.addNotificationToWrite(n);
+        }
+        // add second notification with the same conversation id that will be removed
+        history.addNotificationToWrite(getHistoricalNotification("pkg", 2));
+
+        history.poolStringsFromNotifications();
+
+        assertThat(history.getNotificationsToWrite().size()).isEqualTo(11);
+        // 1 package name and 20 unique channel names and ids and 5 conversation ids
+        assertThat(history.getPooledStringsToWrite().length).isEqualTo(26);
+
+        history.removeConversationFromWrite("pkg", "convo2");
+
+        // 1 package names and 9 * 2 unique channel names and ids and 4 conversation ids
+        assertThat(history.getPooledStringsToWrite().length).isEqualTo(23);
+        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 dc61fb0..dbaf824 100644
--- a/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java
+++ b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java
@@ -175,6 +175,11 @@
         mFileWriteHandler.post(rnr);
     }
 
+    public void deleteConversation(String pkg, String conversationId) {
+        RemoveConversationRunnable rcr = new RemoveConversationRunnable(pkg, conversationId);
+        mFileWriteHandler.post(rcr);
+    }
+
     public void addNotification(final HistoricalNotification notification) {
         synchronized (mLock) {
             mBuffer.addNewNotificationToWrite(notification);
@@ -396,7 +401,7 @@
 
         @Override
         public void run() {
-            if (DEBUG) Slog.d(TAG, "RemovePackageRunnable");
+            if (DEBUG) Slog.d(TAG, "RemoveNotificationRunnable");
             synchronized (mLock) {
                 // Remove from pending history
                 mBuffer.removeNotificationFromWrite(mPkg, mPostedTime);
@@ -422,6 +427,49 @@
         }
     }
 
+    final class RemoveConversationRunnable implements Runnable {
+        private String mPkg;
+        private String mConversationId;
+        private NotificationHistory mNotificationHistory;
+
+        public RemoveConversationRunnable(String pkg, String conversationId) {
+            mPkg = pkg;
+            mConversationId = conversationId;
+        }
+
+        @VisibleForTesting
+        void setNotificationHistory(NotificationHistory nh) {
+            mNotificationHistory = nh;
+        }
+
+        @Override
+        public void run() {
+            if (DEBUG) Slog.d(TAG, "RemoveConversationRunnable");
+            synchronized (mLock) {
+                // Remove from pending history
+                mBuffer.removeConversationFromWrite(mPkg, mConversationId);
+
+                Iterator<AtomicFile> 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.removeConversationFromWrite(mPkg, mConversationId)) {
+                            writeLocked(af, notificationHistory);
+                        }
+                    } catch (Exception e) {
+                        Slog.e(TAG, "Cannot clean up file on conversation 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 9aab0fd..88e0dc6 100644
--- a/services/core/java/com/android/server/notification/NotificationHistoryManager.java
+++ b/services/core/java/com/android/server/notification/NotificationHistoryManager.java
@@ -166,6 +166,22 @@
         }
     }
 
+    public void deleteConversation(String pkg, int uid, String conversationId) {
+        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 conversation for locked/gone/disabled user "
+                        + userId);
+                return;
+            }
+            userHistory.deleteConversation(pkg, conversationId);
+        }
+    }
+
     // 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 2e4a977..ad5bea5 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -5655,6 +5655,22 @@
         mHandler.post(new EnqueueNotificationRunnable(userId, r, isAppForeground));
     }
 
+    public void onConversationRemoved(String pkg, int uid, String conversationId) {
+        checkCallerIsSystem();
+        Preconditions.checkStringNotEmpty(pkg);
+        Preconditions.checkStringNotEmpty(conversationId);
+
+        mHistoryManager.deleteConversation(pkg, uid, conversationId);
+        List<String> deletedChannelIds =
+                mPreferencesHelper.deleteConversation(pkg, uid, conversationId);
+        for (String channelId : deletedChannelIds) {
+            cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channelId, 0, 0, true,
+                    UserHandle.getUserId(uid), REASON_CHANNEL_BANNED,
+                    null);
+        }
+        handleSavePolicyFile();
+    }
+
     @VisibleForTesting
     protected void fixNotification(Notification notification, String pkg, String tag, int id,
             int userId) throws NameNotFoundException {
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 9f8362c..20c8625 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -1223,6 +1223,33 @@
         }
     }
 
+    public @NonNull List<String> deleteConversation(String pkg, int uid, String conversationId) {
+        synchronized (mPackagePreferences) {
+            List<String> deletedChannelIds = new ArrayList<>();
+            PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
+            if (r == null) {
+                return deletedChannelIds;
+            }
+            int N = r.channels.size();
+            for (int i = 0; i < N; i++) {
+                final NotificationChannel nc = r.channels.valueAt(i);
+                if (conversationId.equals(nc.getConversationId())) {
+                    nc.setDeleted(true);
+                    LogMaker lm = getChannelLog(nc, pkg);
+                    lm.setType(
+                            com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_CLOSE);
+                    MetricsLogger.action(lm);
+
+                    deletedChannelIds.add(nc.getId());
+                }
+            }
+            if (!deletedChannelIds.isEmpty() && mAreChannelsBypassingDnd) {
+                updateChannelsBypassingDnd(mContext.getUserId());
+            }
+            return deletedChannelIds;
+        }
+    }
+
     @Override
     public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid,
             boolean includeDeleted) {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java
index cbb760a..99b4fd9 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java
@@ -286,6 +286,52 @@
         verify(af, never()).startWrite();
     }
 
+    @Test
+    public void testRemoveConversationRunnable() throws Exception {
+        NotificationHistory nh = mock(NotificationHistory.class);
+        NotificationHistoryDatabase.RemoveConversationRunnable rcr =
+                mDataBase.new RemoveConversationRunnable("pkg", "convo");
+        rcr.setNotificationHistory(nh);
+
+        AtomicFile af = mock(AtomicFile.class);
+        when(af.getBaseFile()).thenReturn(new File(mRootDir, "af"));
+        mDataBase.mHistoryFiles.addLast(af);
+
+        when(nh.removeConversationFromWrite("pkg", "convo")).thenReturn(true);
+
+        mDataBase.mBuffer = mock(NotificationHistory.class);
+
+        rcr.run();
+
+        verify(mDataBase.mBuffer).removeConversationFromWrite("pkg", "convo");
+        verify(af).openRead();
+        verify(nh).removeConversationFromWrite("pkg", "convo");
+        verify(af).startWrite();
+    }
+
+    @Test
+    public void testRemoveConversationRunnable_noChanges() throws Exception {
+        NotificationHistory nh = mock(NotificationHistory.class);
+        NotificationHistoryDatabase.RemoveConversationRunnable rcr =
+                mDataBase.new RemoveConversationRunnable("pkg", "convo");
+        rcr.setNotificationHistory(nh);
+
+        AtomicFile af = mock(AtomicFile.class);
+        when(af.getBaseFile()).thenReturn(new File(mRootDir, "af"));
+        mDataBase.mHistoryFiles.addLast(af);
+
+        when(nh.removeConversationFromWrite("pkg", "convo")).thenReturn(false);
+
+        mDataBase.mBuffer = mock(NotificationHistory.class);
+
+        rcr.run();
+
+        verify(mDataBase.mBuffer).removeConversationFromWrite("pkg", "convo");
+        verify(af).openRead();
+        verify(nh).removeConversationFromWrite("pkg", "convo");
+        verify(af, never()).startWrite();
+    }
+
     private class TestFileAttrProvider implements NotificationHistoryDatabase.FileAttrProvider {
         public Map<File, Long> 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 2c548be..f7c2609 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java
@@ -303,6 +303,20 @@
     }
 
     @Test
+    public void testDeleteConversation_userUnlocked() {
+        String pkg = "pkg";
+        String convo  = "convo";
+        NotificationHistoryDatabase userHistory = mock(NotificationHistoryDatabase.class);
+
+        mHistoryManager.onUserUnlocked(USER_SYSTEM);
+        mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistory);
+
+        mHistoryManager.deleteConversation(pkg, 1, convo);
+
+        verify(userHistory, times(1)).deleteConversation(pkg, convo);
+    }
+
+    @Test
     public void testTriggerWriteToDisk() {
         NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class);
         NotificationHistoryDatabase userHistoryAll = mock(NotificationHistoryDatabase.class);
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 3f690b1..0adf15c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -3009,7 +3009,7 @@
 
         NotificationChannel channel2 =
                 new NotificationChannel("B person", "B fabulous person", IMPORTANCE_DEFAULT);
-        channel2.setConversationId(calls.getId(), channel.getName().toString());
+        channel2.setConversationId(calls.getId(), channel2.getName().toString());
         mHelper.createNotificationChannel(PKG_O, UID_O, channel2, true, false);
 
         Map<String, NotificationChannel> expected = new HashMap<>();
@@ -3036,4 +3036,46 @@
                     .isEqualTo(expectedGroup.get(convo.getNotificationChannel().getId()));
         }
     }
+
+    @Test
+    public void testDeleteConversation() {
+        String convoId = "convo";
+        NotificationChannel messages =
+                new NotificationChannel("messages", "Messages", IMPORTANCE_DEFAULT);
+        mHelper.createNotificationChannel(PKG_O, UID_O, messages, true, false);
+        NotificationChannel calls =
+                new NotificationChannel("calls", "Calls", IMPORTANCE_DEFAULT);
+        mHelper.createNotificationChannel(PKG_O, UID_O, calls, true, false);
+
+        NotificationChannel channel =
+                new NotificationChannel("A person msgs", "messages from A", IMPORTANCE_DEFAULT);
+        channel.setConversationId(messages.getId(), convoId);
+        mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false);
+
+        NotificationChannel noMatch =
+                new NotificationChannel("B person msgs", "messages from B", IMPORTANCE_DEFAULT);
+        noMatch.setConversationId(messages.getId(), "different convo");
+        mHelper.createNotificationChannel(PKG_O, UID_O, noMatch, true, false);
+
+        NotificationChannel channel2 =
+                new NotificationChannel("A person calls", "calls from A", IMPORTANCE_DEFAULT);
+        channel2.setConversationId(calls.getId(), convoId);
+        mHelper.createNotificationChannel(PKG_O, UID_O, channel2, true, false);
+
+        assertEquals(channel, mHelper.getNotificationChannel(PKG_O, UID_O, channel.getId(), false));
+        assertEquals(channel2,
+                mHelper.getNotificationChannel(PKG_O, UID_O, channel2.getId(), false));
+        assertEquals(2, mHelper.deleteConversation(PKG_O, UID_O, convoId).size());
+
+        assertEquals(messages,
+                mHelper.getNotificationChannel(PKG_O, UID_O, messages.getId(), false));
+        assertEquals(noMatch,
+                mHelper.getNotificationChannel(PKG_O, UID_O, noMatch.getId(), false));
+
+        assertNull(mHelper.getNotificationChannel(PKG_O, UID_O, channel.getId(), false));
+        assertNull(mHelper.getNotificationChannel(PKG_O, UID_O, channel2.getId(), false));
+        assertEquals(channel, mHelper.getNotificationChannel(PKG_O, UID_O, channel.getId(), true));
+        assertEquals(channel2,
+                mHelper.getNotificationChannel(PKG_O, UID_O, channel2.getId(), true));
+    }
 }