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