1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.server.notification; 17 18 import static com.google.common.truth.Truth.assertThat; 19 20 import static org.mockito.ArgumentMatchers.any; 21 import static org.mockito.ArgumentMatchers.anyInt; 22 import static org.mockito.ArgumentMatchers.anyLong; 23 import static org.mockito.Mockito.mock; 24 import static org.mockito.Mockito.never; 25 import static org.mockito.Mockito.times; 26 import static org.mockito.Mockito.verify; 27 import static org.mockito.Mockito.when; 28 29 import android.app.AlarmManager; 30 import android.app.NotificationHistory; 31 import android.app.NotificationHistory.HistoricalNotification; 32 import android.content.Context; 33 import android.graphics.drawable.Icon; 34 import android.os.Handler; 35 import android.util.AtomicFile; 36 37 import androidx.test.InstrumentationRegistry; 38 import androidx.test.runner.AndroidJUnit4; 39 40 import com.android.server.UiServiceTestCase; 41 42 import org.junit.Before; 43 import org.junit.Test; 44 import org.junit.runner.RunWith; 45 import org.mockito.Mock; 46 import org.mockito.MockitoAnnotations; 47 48 import java.io.File; 49 import java.util.ArrayList; 50 import java.util.Calendar; 51 import java.util.GregorianCalendar; 52 import java.util.List; 53 import java.util.Set; 54 55 @RunWith(AndroidJUnit4.class) 56 public class NotificationHistoryDatabaseTest extends UiServiceTestCase { 57 58 File mRootDir; 59 @Mock 60 Handler mFileWriteHandler; 61 @Mock 62 Context mContext; 63 @Mock 64 AlarmManager mAlarmManager; 65 66 NotificationHistoryDatabase mDataBase; 67 getHistoricalNotification(int index)68 private HistoricalNotification getHistoricalNotification(int index) { 69 return getHistoricalNotification("package" + index, index); 70 } 71 getHistoricalNotification(String packageName, int index)72 private HistoricalNotification getHistoricalNotification(String packageName, int index) { 73 String expectedChannelName = "channelName" + index; 74 String expectedChannelId = "channelId" + index; 75 int expectedUid = 1123456 + index; 76 int expectedUserId = 11 + index; 77 long expectedPostTime = 987654321 + index; 78 String expectedTitle = "title" + index; 79 String expectedText = "text" + index; 80 Icon expectedIcon = Icon.createWithResource(InstrumentationRegistry.getContext(), 81 index); 82 83 return new HistoricalNotification.Builder() 84 .setPackage(packageName) 85 .setChannelName(expectedChannelName) 86 .setChannelId(expectedChannelId) 87 .setUid(expectedUid) 88 .setUserId(expectedUserId) 89 .setPostedTimeMs(expectedPostTime) 90 .setTitle(expectedTitle) 91 .setText(expectedText) 92 .setIcon(expectedIcon) 93 .build(); 94 } 95 96 @Before setUp()97 public void setUp() { 98 MockitoAnnotations.initMocks(this); 99 when(mContext.getSystemService(AlarmManager.class)).thenReturn(mAlarmManager); 100 when(mContext.getUser()).thenReturn(getContext().getUser()); 101 when(mContext.getPackageName()).thenReturn(getContext().getPackageName()); 102 103 mRootDir = new File(mContext.getFilesDir(), "NotificationHistoryDatabaseTest"); 104 105 mDataBase = new NotificationHistoryDatabase(mContext, mFileWriteHandler, mRootDir); 106 mDataBase.init(); 107 } 108 109 @Test testDeletionReceiver()110 public void testDeletionReceiver() { 111 verify(mContext, times(1)).registerReceiver(any(), any()); 112 } 113 114 @Test testPrune()115 public void testPrune() throws Exception { 116 GregorianCalendar cal = new GregorianCalendar(); 117 cal.setTimeInMillis(10); 118 int retainDays = 1; 119 120 List<AtomicFile> expectedFiles = new ArrayList<>(); 121 122 // add 5 files with a creation date of "today" 123 for (long i = cal.getTimeInMillis(); i >= 5; i--) { 124 File file = mock(File.class); 125 when(file.getName()).thenReturn(String.valueOf(i)); 126 when(file.getAbsolutePath()).thenReturn(String.valueOf(i)); 127 AtomicFile af = new AtomicFile(file); 128 expectedFiles.add(af); 129 mDataBase.mHistoryFiles.addLast(af); 130 } 131 132 cal.add(Calendar.DATE, -1 * retainDays); 133 // Add 5 more files more than retainDays old 134 for (int i = 5; i >= 0; i--) { 135 File file = mock(File.class); 136 when(file.getName()).thenReturn(String.valueOf(cal.getTimeInMillis() - i)); 137 when(file.getAbsolutePath()).thenReturn(String.valueOf(cal.getTimeInMillis() - i)); 138 AtomicFile af = new AtomicFile(file); 139 mDataBase.mHistoryFiles.addLast(af); 140 } 141 142 // back to today; trim everything a day + old 143 cal.add(Calendar.DATE, 1 * retainDays); 144 mDataBase.prune(retainDays, cal.getTimeInMillis()); 145 146 assertThat(mDataBase.mHistoryFiles).containsExactlyElementsIn(expectedFiles); 147 148 verify(mAlarmManager, times(6)).setExactAndAllowWhileIdle(anyInt(), anyLong(), any()); 149 } 150 151 @Test testPrune_badFileName_noCrash()152 public void testPrune_badFileName_noCrash() { 153 GregorianCalendar cal = new GregorianCalendar(); 154 cal.setTimeInMillis(10); 155 int retainDays = 1; 156 157 List<AtomicFile> expectedFiles = new ArrayList<>(); 158 159 // add 5 files with a creation date of "today", but the file names are bad 160 for (long i = cal.getTimeInMillis(); i >= 5; i--) { 161 File file = mock(File.class); 162 when(file.getName()).thenReturn(i + ".bak"); 163 when(file.getAbsolutePath()).thenReturn(i + ".bak"); 164 AtomicFile af = new AtomicFile(file); 165 mDataBase.mHistoryFiles.addLast(af); 166 } 167 168 // trim everything a day+ old 169 cal.add(Calendar.DATE, 1 * retainDays); 170 mDataBase.prune(retainDays, cal.getTimeInMillis()); 171 172 assertThat(mDataBase.mHistoryFiles).containsExactlyElementsIn(expectedFiles); 173 } 174 175 @Test testOnPackageRemove_posts()176 public void testOnPackageRemove_posts() { 177 mDataBase.onPackageRemoved("test"); 178 verify(mFileWriteHandler, times(1)).post(any()); 179 } 180 181 @Test testForceWriteToDisk()182 public void testForceWriteToDisk() { 183 mDataBase.forceWriteToDisk(); 184 verify(mFileWriteHandler, times(1)).post(any()); 185 } 186 187 @Test testForceWriteToDisk_bypassesExistingWrites()188 public void testForceWriteToDisk_bypassesExistingWrites() { 189 when(mFileWriteHandler.hasCallbacks(any())).thenReturn(true); 190 mDataBase.forceWriteToDisk(); 191 verify(mFileWriteHandler, times(1)).post(any()); 192 } 193 194 @Test testAddNotification()195 public void testAddNotification() { 196 HistoricalNotification n = getHistoricalNotification(1); 197 HistoricalNotification n2 = getHistoricalNotification(2); 198 199 mDataBase.addNotification(n); 200 assertThat(mDataBase.mBuffer.getNotificationsToWrite()).contains(n); 201 verify(mFileWriteHandler, times(1)).postDelayed(any(), anyLong()); 202 203 // second add should not trigger another write 204 mDataBase.addNotification(n2); 205 assertThat(mDataBase.mBuffer.getNotificationsToWrite()).contains(n2); 206 verify(mFileWriteHandler, times(1)).postDelayed(any(), anyLong()); 207 } 208 209 @Test testAddNotification_newestFirst()210 public void testAddNotification_newestFirst() { 211 HistoricalNotification n = getHistoricalNotification(1); 212 HistoricalNotification n2 = getHistoricalNotification(2); 213 214 mDataBase.addNotification(n); 215 216 // second add should not trigger another write 217 mDataBase.addNotification(n2); 218 219 assertThat(mDataBase.mBuffer.getNotificationsToWrite().get(0)).isEqualTo(n2); 220 assertThat(mDataBase.mBuffer.getNotificationsToWrite().get(1)).isEqualTo(n); 221 } 222 223 @Test testReadNotificationHistory_readsAllFiles()224 public void testReadNotificationHistory_readsAllFiles() throws Exception { 225 for (long i = 10; i >= 5; i--) { 226 AtomicFile af = mock(AtomicFile.class); 227 mDataBase.mHistoryFiles.addLast(af); 228 } 229 230 mDataBase.readNotificationHistory(); 231 232 for (AtomicFile file : mDataBase.mHistoryFiles) { 233 verify(file, times(1)).openRead(); 234 } 235 } 236 237 @Test testReadNotificationHistory_readsBuffer()238 public void testReadNotificationHistory_readsBuffer() throws Exception { 239 HistoricalNotification hn = getHistoricalNotification(1); 240 mDataBase.addNotification(hn); 241 242 NotificationHistory nh = mDataBase.readNotificationHistory(); 243 244 assertThat(nh.getNotificationsToWrite()).contains(hn); 245 } 246 247 @Test testReadNotificationHistory_withNumFilterDoesNotReadExtraFiles()248 public void testReadNotificationHistory_withNumFilterDoesNotReadExtraFiles() throws Exception { 249 AtomicFile af = mock(AtomicFile.class); 250 when(af.getBaseFile()).thenReturn(new File(mRootDir, "af")); 251 mDataBase.mHistoryFiles.addLast(af); 252 253 AtomicFile af2 = mock(AtomicFile.class); 254 when(af2.getBaseFile()).thenReturn(new File(mRootDir, "af2")); 255 mDataBase.mHistoryFiles.addLast(af2); 256 257 mDataBase.readNotificationHistory(null, null, 0); 258 259 verify(af, times(1)).openRead(); 260 verify(af2, never()).openRead(); 261 } 262 263 @Test testRemoveNotificationRunnable()264 public void testRemoveNotificationRunnable() throws Exception { 265 NotificationHistory nh = mock(NotificationHistory.class); 266 NotificationHistoryDatabase.RemoveNotificationRunnable rnr = 267 mDataBase.new RemoveNotificationRunnable("pkg", 123); 268 rnr.setNotificationHistory(nh); 269 270 AtomicFile af = mock(AtomicFile.class); 271 when(af.getBaseFile()).thenReturn(new File(mRootDir, "af")); 272 mDataBase.mHistoryFiles.addLast(af); 273 274 when(nh.removeNotificationFromWrite("pkg", 123)).thenReturn(true); 275 276 mDataBase.mBuffer = mock(NotificationHistory.class); 277 278 rnr.run(); 279 280 verify(mDataBase.mBuffer).removeNotificationFromWrite("pkg", 123); 281 verify(af).openRead(); 282 verify(nh).removeNotificationFromWrite("pkg", 123); 283 verify(af).startWrite(); 284 } 285 286 @Test testRemoveNotificationRunnable_noChanges()287 public void testRemoveNotificationRunnable_noChanges() throws Exception { 288 NotificationHistory nh = mock(NotificationHistory.class); 289 NotificationHistoryDatabase.RemoveNotificationRunnable rnr = 290 mDataBase.new RemoveNotificationRunnable("pkg", 123); 291 rnr.setNotificationHistory(nh); 292 293 AtomicFile af = mock(AtomicFile.class); 294 when(af.getBaseFile()).thenReturn(new File(mRootDir, "af")); 295 mDataBase.mHistoryFiles.addLast(af); 296 297 when(nh.removeNotificationFromWrite("pkg", 123)).thenReturn(false); 298 299 mDataBase.mBuffer = mock(NotificationHistory.class); 300 301 rnr.run(); 302 303 verify(mDataBase.mBuffer).removeNotificationFromWrite("pkg", 123); 304 verify(af).openRead(); 305 verify(nh).removeNotificationFromWrite("pkg", 123); 306 verify(af, never()).startWrite(); 307 } 308 309 @Test testRemoveConversationRunnable()310 public void testRemoveConversationRunnable() throws Exception { 311 NotificationHistory nh = mock(NotificationHistory.class); 312 NotificationHistoryDatabase.RemoveConversationRunnable rcr = 313 mDataBase.new RemoveConversationRunnable("pkg", Set.of("convo", "another")); 314 rcr.setNotificationHistory(nh); 315 316 AtomicFile af = mock(AtomicFile.class); 317 when(af.getBaseFile()).thenReturn(new File(mRootDir, "af")); 318 mDataBase.mHistoryFiles.addLast(af); 319 320 when(nh.removeConversationsFromWrite("pkg", Set.of("convo", "another"))).thenReturn(true); 321 322 mDataBase.mBuffer = mock(NotificationHistory.class); 323 324 rcr.run(); 325 326 verify(mDataBase.mBuffer).removeConversationsFromWrite("pkg",Set.of("convo", "another")); 327 verify(af).openRead(); 328 verify(nh).removeConversationsFromWrite("pkg",Set.of("convo", "another")); 329 verify(af).startWrite(); 330 } 331 332 @Test testRemoveConversationRunnable_noChanges()333 public void testRemoveConversationRunnable_noChanges() throws Exception { 334 NotificationHistory nh = mock(NotificationHistory.class); 335 NotificationHistoryDatabase.RemoveConversationRunnable rcr = 336 mDataBase.new RemoveConversationRunnable("pkg", Set.of("convo")); 337 rcr.setNotificationHistory(nh); 338 339 AtomicFile af = mock(AtomicFile.class); 340 when(af.getBaseFile()).thenReturn(new File(mRootDir, "af")); 341 mDataBase.mHistoryFiles.addLast(af); 342 343 when(nh.removeConversationsFromWrite("pkg", Set.of("convo"))).thenReturn(false); 344 345 mDataBase.mBuffer = mock(NotificationHistory.class); 346 347 rcr.run(); 348 349 verify(mDataBase.mBuffer).removeConversationsFromWrite("pkg", Set.of("convo")); 350 verify(af).openRead(); 351 verify(nh).removeConversationsFromWrite("pkg", Set.of("convo")); 352 verify(af, never()).startWrite(); 353 } 354 355 @Test testRemoveChannelRunnable()356 public void testRemoveChannelRunnable() throws Exception { 357 NotificationHistory nh = mock(NotificationHistory.class); 358 NotificationHistoryDatabase.RemoveChannelRunnable rcr = 359 mDataBase.new RemoveChannelRunnable("pkg", "channel"); 360 rcr.setNotificationHistory(nh); 361 362 AtomicFile af = mock(AtomicFile.class); 363 when(af.getBaseFile()).thenReturn(new File(mRootDir, "af")); 364 mDataBase.mHistoryFiles.addLast(af); 365 366 when(nh.removeChannelFromWrite("pkg", "channel")).thenReturn(true); 367 368 mDataBase.mBuffer = mock(NotificationHistory.class); 369 370 rcr.run(); 371 372 verify(mDataBase.mBuffer).removeChannelFromWrite("pkg", "channel"); 373 verify(af).openRead(); 374 verify(nh).removeChannelFromWrite("pkg", "channel"); 375 verify(af).startWrite(); 376 } 377 378 @Test testRemoveChannelRunnable_noChanges()379 public void testRemoveChannelRunnable_noChanges() throws Exception { 380 NotificationHistory nh = mock(NotificationHistory.class); 381 NotificationHistoryDatabase.RemoveChannelRunnable rcr = 382 mDataBase.new RemoveChannelRunnable("pkg", "channel"); 383 rcr.setNotificationHistory(nh); 384 385 AtomicFile af = mock(AtomicFile.class); 386 when(af.getBaseFile()).thenReturn(new File(mRootDir, "af")); 387 mDataBase.mHistoryFiles.addLast(af); 388 389 when(nh.removeChannelFromWrite("pkg", "channel")).thenReturn(false); 390 391 mDataBase.mBuffer = mock(NotificationHistory.class); 392 393 rcr.run(); 394 395 verify(mDataBase.mBuffer).removeChannelFromWrite("pkg", "channel"); 396 verify(af).openRead(); 397 verify(nh).removeChannelFromWrite("pkg", "channel"); 398 verify(af, never()).startWrite(); 399 } 400 401 @Test testWriteBufferRunnable()402 public void testWriteBufferRunnable() throws Exception { 403 NotificationHistory nh = mock(NotificationHistory.class); 404 when(nh.getPooledStringsToWrite()).thenReturn(new String[]{}); 405 when(nh.getNotificationsToWrite()).thenReturn(new ArrayList<>()); 406 NotificationHistoryDatabase.WriteBufferRunnable wbr = 407 mDataBase.new WriteBufferRunnable(); 408 409 mDataBase.mBuffer = nh; 410 AtomicFile af = mock(AtomicFile.class); 411 File file = mock(File.class); 412 when(file.getName()).thenReturn("5"); 413 when(af.getBaseFile()).thenReturn(file); 414 415 wbr.run(5, af); 416 417 assertThat(mDataBase.mHistoryFiles.size()).isEqualTo(1); 418 assertThat(mDataBase.mBuffer).isNotEqualTo(nh); 419 verify(mAlarmManager, times(1)).setExactAndAllowWhileIdle(anyInt(), anyLong(), any()); 420 } 421 422 @Test testRemoveFilePathFromHistory_hasMatch()423 public void testRemoveFilePathFromHistory_hasMatch() throws Exception { 424 for (int i = 0; i < 5; i++) { 425 AtomicFile af = mock(AtomicFile.class); 426 when(af.getBaseFile()).thenReturn(new File(mRootDir, "af" + i)); 427 mDataBase.mHistoryFiles.addLast(af); 428 } 429 // Baseline size of history files 430 assertThat(mDataBase.mHistoryFiles.size()).isEqualTo(5); 431 432 // Remove only file number 3 433 String filePathToRemove = new File(mRootDir, "af3").getAbsolutePath(); 434 mDataBase.removeFilePathFromHistory(filePathToRemove); 435 assertThat(mDataBase.mHistoryFiles.size()).isEqualTo(4); 436 } 437 438 @Test testRemoveFilePathFromHistory_noMatch()439 public void testRemoveFilePathFromHistory_noMatch() throws Exception { 440 for (int i = 0; i < 5; i++) { 441 AtomicFile af = mock(AtomicFile.class); 442 when(af.getBaseFile()).thenReturn(new File(mRootDir, "af" + i)); 443 mDataBase.mHistoryFiles.addLast(af); 444 } 445 // Baseline size of history files 446 assertThat(mDataBase.mHistoryFiles.size()).isEqualTo(5); 447 448 // Attempt to remove a filename that doesn't exist, expect nothing to break or change 449 String filePathToRemove = new File(mRootDir, "af.thisfileisfake").getAbsolutePath(); 450 mDataBase.removeFilePathFromHistory(filePathToRemove); 451 assertThat(mDataBase.mHistoryFiles.size()).isEqualTo(5); 452 } 453 } 454