Remove notification channels from history when they're deleted.

To make this possible, this change adds functionality for notification channel removal to NotificationHistory, NotificationHistoryDatabase, and NotificationHistoryManager and corresponding tests.

This change does not remove the notifications from deleted channels from the "recently dismissed" list (stored in mArchive); will follow up with that later.

Test: atest NotificationHistoryTest, NotificationHistoryDatabaseTest, NotificationManagerServiceTest; manual via creating channels, deleting them and verifying
Bug: 169349809

Change-Id: I7b1c137e516d14703665750f06554ac7ca44c90e
This commit is contained in:
Yuri Lin
2021-03-23 17:24:55 -04:00
parent 8a49089585
commit 048571dd57
7 changed files with 190 additions and 1 deletions

View File

@@ -403,6 +403,26 @@ public final class NotificationHistory implements Parcelable {
return removed;
}
/**
* Removes all notifications from a channel and regenerates the string pool
*/
public boolean removeChannelFromWrite(String packageName, String channelId) {
boolean removed = false;
for (int i = mNotificationsToWrite.size() - 1; i >= 0; i--) {
HistoricalNotification hn = mNotificationsToWrite.get(i);
if (packageName.equals(hn.getPackage())
&& Objects.equals(channelId, hn.getChannelId())) {
removed = true;
mNotificationsToWrite.remove(i);
}
}
if (removed) {
poolStringsFromNotifications();
}
return removed;
}
/**
* Gets pooled strings in order to write them to disk
*/

View File

@@ -29,6 +29,8 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -326,6 +328,48 @@ public class NotificationHistoryTest {
.containsExactlyElementsIn(postRemoveExpectedEntries);
}
@Test
public void testRemoveChannelFromWrite() {
NotificationHistory history = new NotificationHistory();
List<HistoricalNotification> postRemoveExpectedEntries = new ArrayList<>();
Set<String> postRemoveExpectedStrings = new HashSet<>();
for (int i = 1; i <= 10; i++) {
HistoricalNotification n = getHistoricalNotification("pkg", i);
// Remove channel numbers 5 and 6
if (i != 5 && i != 6) {
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 channel id that will also be removed
history.addNotificationToWrite(getHistoricalNotification("pkg", 6));
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.removeChannelFromWrite("pkg", "channelId5");
history.removeChannelFromWrite("pkg", "channelId6");
// 1 package names and 8 * 2 unique channel names and ids and 4 conversation ids
assertThat(history.getPooledStringsToWrite().length).isEqualTo(21);
assertThat(Arrays.asList(history.getPooledStringsToWrite()))
.containsExactlyElementsIn(postRemoveExpectedStrings);
assertThat(history.getNotificationsToWrite())
.containsExactlyElementsIn(postRemoveExpectedEntries);
}
@Test
public void testParceling() {
NotificationHistory history = new NotificationHistory();

View File

@@ -175,6 +175,11 @@ public class NotificationHistoryDatabase {
mFileWriteHandler.post(rcr);
}
public void deleteNotificationChannel(String pkg, String channelId) {
RemoveChannelRunnable rcr = new RemoveChannelRunnable(pkg, channelId);
mFileWriteHandler.post(rcr);
}
public void addNotification(final HistoricalNotification notification) {
synchronized (mLock) {
mBuffer.addNewNotificationToWrite(notification);
@@ -505,4 +510,47 @@ public class NotificationHistoryDatabase {
}
}
}
final class RemoveChannelRunnable implements Runnable {
private String mPkg;
private String mChannelId;
private NotificationHistory mNotificationHistory;
RemoveChannelRunnable(String pkg, String channelId) {
mPkg = pkg;
mChannelId = channelId;
}
@VisibleForTesting
void setNotificationHistory(NotificationHistory nh) {
mNotificationHistory = nh;
}
@Override
public void run() {
if (DEBUG) Slog.d(TAG, "RemoveChannelRunnable");
synchronized (mLock) {
// Remove from pending history
mBuffer.removeChannelFromWrite(mPkg, mChannelId);
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.removeChannelFromWrite(mPkg, mChannelId)) {
writeLocked(af, notificationHistory);
}
} catch (Exception e) {
Slog.e(TAG, "Cannot clean up file on channel removal "
+ af.getBaseFile().getName(), e);
}
}
}
}
}
}

View File

@@ -183,6 +183,22 @@ public class NotificationHistoryManager {
}
}
public void deleteNotificationChannel(String pkg, int uid, String channelId) {
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 channel for locked/gone/disabled user "
+ userId);
return;
}
userHistory.deleteNotificationChannel(pkg, channelId);
}
}
public void triggerWriteToDisk() {
synchronized (mLock) {
final int userCount = mUserState.size();

View File

@@ -3623,6 +3623,7 @@ public class NotificationManagerService extends SystemService {
cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channelId, 0, 0, true,
callingUser, REASON_CHANNEL_REMOVED, null);
mPreferencesHelper.deleteNotificationChannel(pkg, callingUid, channelId);
mHistoryManager.deleteNotificationChannel(pkg, callingUid, channelId);
mListeners.notifyNotificationChannelChanged(pkg,
UserHandle.getUserHandleForUid(callingUid),
mPreferencesHelper.getNotificationChannel(pkg, callingUid, channelId, true),

View File

@@ -349,6 +349,52 @@ public class NotificationHistoryDatabaseTest extends UiServiceTestCase {
verify(af, never()).startWrite();
}
@Test
public void testRemoveChannelRunnable() throws Exception {
NotificationHistory nh = mock(NotificationHistory.class);
NotificationHistoryDatabase.RemoveChannelRunnable rcr =
mDataBase.new RemoveChannelRunnable("pkg", "channel");
rcr.setNotificationHistory(nh);
AtomicFile af = mock(AtomicFile.class);
when(af.getBaseFile()).thenReturn(new File(mRootDir, "af"));
mDataBase.mHistoryFiles.addLast(af);
when(nh.removeChannelFromWrite("pkg", "channel")).thenReturn(true);
mDataBase.mBuffer = mock(NotificationHistory.class);
rcr.run();
verify(mDataBase.mBuffer).removeChannelFromWrite("pkg", "channel");
verify(af).openRead();
verify(nh).removeChannelFromWrite("pkg", "channel");
verify(af).startWrite();
}
@Test
public void testRemoveChannelRunnable_noChanges() throws Exception {
NotificationHistory nh = mock(NotificationHistory.class);
NotificationHistoryDatabase.RemoveChannelRunnable rcr =
mDataBase.new RemoveChannelRunnable("pkg", "channel");
rcr.setNotificationHistory(nh);
AtomicFile af = mock(AtomicFile.class);
when(af.getBaseFile()).thenReturn(new File(mRootDir, "af"));
mDataBase.mHistoryFiles.addLast(af);
when(nh.removeChannelFromWrite("pkg", "channel")).thenReturn(false);
mDataBase.mBuffer = mock(NotificationHistory.class);
rcr.run();
verify(mDataBase.mBuffer).removeChannelFromWrite("pkg", "channel");
verify(af).openRead();
verify(nh).removeChannelFromWrite("pkg", "channel");
verify(af, never()).startWrite();
}
@Test
public void testWriteBufferRunnable() throws Exception {
NotificationHistory nh = mock(NotificationHistory.class);

View File

@@ -366,7 +366,7 @@ public class NotificationHistoryManagerTest extends UiServiceTestCase {
@Test
public void testDeleteConversation_userUnlocked() {
String pkg = "pkg";
Set<String> convos = Set.of("convo", "another");
Set<String> convos = Set.of("convo", "another");
NotificationHistoryDatabase userHistory = mock(NotificationHistoryDatabase.class);
mHistoryManager.onUserUnlocked(USER_SYSTEM);
@@ -377,6 +377,20 @@ public class NotificationHistoryManagerTest extends UiServiceTestCase {
verify(userHistory, times(1)).deleteConversations(pkg, convos);
}
@Test
public void testDeleteNotificationChannel_userUnlocked() {
String pkg = "pkg";
String channelId = "channelId";
NotificationHistoryDatabase userHistory = mock(NotificationHistoryDatabase.class);
mHistoryManager.onUserUnlocked(USER_SYSTEM);
mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistory);
mHistoryManager.deleteNotificationChannel(pkg, 1, channelId);
verify(userHistory, times(1)).deleteNotificationChannel(pkg, channelId);
}
@Test
public void testTriggerWriteToDisk() {
NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class);