• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Copyright (C) 2017 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 
17 package android.ext.services.notification;
18 
19 import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
20 import static android.app.NotificationManager.IMPORTANCE_LOW;
21 import static android.app.NotificationManager.IMPORTANCE_MIN;
22 
23 import static org.mockito.ArgumentMatchers.any;
24 import static org.mockito.ArgumentMatchers.anyInt;
25 import static org.mockito.ArgumentMatchers.anyString;
26 import static org.mockito.Mockito.mock;
27 import static org.mockito.Mockito.never;
28 import static org.mockito.Mockito.times;
29 import static org.mockito.Mockito.verify;
30 import static org.mockito.Mockito.when;
31 
32 import android.app.Application;
33 import android.app.INotificationManager;
34 import android.app.Notification;
35 import android.app.NotificationChannel;
36 import android.content.Intent;
37 import android.content.pm.ApplicationInfo;
38 import android.content.pm.IPackageManager;
39 import android.os.Build;
40 import android.os.UserHandle;
41 import android.service.notification.Adjustment;
42 import android.service.notification.NotificationListenerService;
43 import android.service.notification.NotificationListenerService.Ranking;
44 import android.service.notification.NotificationListenerService.RankingMap;
45 import android.service.notification.NotificationStats;
46 import android.service.notification.StatusBarNotification;
47 import android.test.ServiceTestCase;
48 import android.testing.TestableContext;
49 import android.util.AtomicFile;
50 
51 import androidx.test.InstrumentationRegistry;
52 
53 import com.android.internal.util.FastXmlSerializer;
54 
55 import org.junit.Before;
56 import org.junit.Rule;
57 import org.junit.Test;
58 import org.mockito.ArgumentCaptor;
59 import org.mockito.Mock;
60 import org.mockito.MockitoAnnotations;
61 import org.xmlpull.v1.XmlSerializer;
62 
63 import java.io.BufferedInputStream;
64 import java.io.BufferedOutputStream;
65 import java.io.ByteArrayInputStream;
66 import java.io.ByteArrayOutputStream;
67 import java.io.FileOutputStream;
68 import java.util.ArrayList;
69 
70 public class AssistantTest extends ServiceTestCase<Assistant> {
71 
72     private static final String PKG1 = "pkg1";
73     private static final int UID1 = 1;
74     private static final NotificationChannel P1C1 =
75             new NotificationChannel("one", "", IMPORTANCE_LOW);
76     private static final NotificationChannel P1C2 =
77             new NotificationChannel("p1c2", "", IMPORTANCE_DEFAULT);
78     private static final NotificationChannel P1C3 =
79             new NotificationChannel("p1c3", "", IMPORTANCE_MIN);
80     private static final String PKG2 = "pkg2";
81 
82     private static final int UID2 = 2;
83     private static final NotificationChannel P2C1 =
84             new NotificationChannel("one", "", IMPORTANCE_LOW);
85 
86     @Mock INotificationManager mNoMan;
87     @Mock AtomicFile mFile;
88     @Mock IPackageManager mPackageManager;
89     @Mock SmsHelper mSmsHelper;
90 
91     Assistant mAssistant;
92     Application mApplication;
93 
94     @Rule
95     public final TestableContext mContext =
96             new TestableContext(InstrumentationRegistry.getContext(), null);
97 
AssistantTest()98     public AssistantTest() {
99         super(Assistant.class);
100     }
101 
102     @Before
setUp()103     public void setUp() throws Exception {
104         MockitoAnnotations.initMocks(this);
105 
106         Intent startIntent =
107                 new Intent("android.service.notification.NotificationAssistantService");
108         startIntent.setPackage("android.ext.services");
109 
110         mApplication = (Application) InstrumentationRegistry.getInstrumentation().
111                 getTargetContext().getApplicationContext();
112         // Force the test to use the correct application instead of trying to use a mock application
113         setApplication(mApplication);
114 
115         setupService();
116         mAssistant = getService();
117 
118         // Override the AssistantSettings factory.
119         mAssistant.mSettingsFactory = AssistantSettings::createForTesting;
120 
121         bindService(startIntent);
122 
123         mAssistant.mSettings.mDismissToViewRatioLimit = 0.8f;
124         mAssistant.mSettings.mStreakLimit = 2;
125         mAssistant.mSettings.mNewInterruptionModel = true;
126         mAssistant.setNoMan(mNoMan);
127         mAssistant.setFile(mFile);
128         mAssistant.setPackageManager(mPackageManager);
129 
130         ApplicationInfo info = mock(ApplicationInfo.class);
131         when(mPackageManager.getApplicationInfo(anyString(), anyInt(), anyInt()))
132                 .thenReturn(info);
133         info.targetSdkVersion = Build.VERSION_CODES.P;
134         when(mFile.startWrite()).thenReturn(mock(FileOutputStream.class));
135     }
136 
generateSbn(String pkg, int uid, NotificationChannel channel, String tag, String groupKey)137     private StatusBarNotification generateSbn(String pkg, int uid, NotificationChannel channel,
138             String tag, String groupKey) {
139         Notification n = new Notification.Builder(mContext, channel.getId())
140                 .setContentTitle("foo")
141                 .setGroup(groupKey)
142                 .build();
143 
144         StatusBarNotification sbn = new StatusBarNotification(pkg, pkg, 0, tag, uid, uid, n,
145                 UserHandle.SYSTEM, null, 0);
146 
147         return sbn;
148     }
149 
generateRanking(StatusBarNotification sbn, NotificationChannel channel)150     private Ranking generateRanking(StatusBarNotification sbn, NotificationChannel channel) {
151         Ranking mockRanking = mock(Ranking.class);
152         when(mockRanking.getChannel()).thenReturn(channel);
153         when(mockRanking.getImportance()).thenReturn(channel.getImportance());
154         when(mockRanking.getKey()).thenReturn(sbn.getKey());
155         when(mockRanking.getOverrideGroupKey()).thenReturn(null);
156         return mockRanking;
157     }
158 
almostBlockChannel(String pkg, int uid, NotificationChannel channel)159     private void almostBlockChannel(String pkg, int uid, NotificationChannel channel) {
160         for (int i = 0; i < ChannelImpressions.DEFAULT_STREAK_LIMIT; i++) {
161             dismissBadNotification(pkg, uid, channel, String.valueOf(i));
162         }
163     }
164 
dismissBadNotification(String pkg, int uid, NotificationChannel channel, String tag)165     private void dismissBadNotification(String pkg, int uid, NotificationChannel channel,
166             String tag) {
167         StatusBarNotification sbn = generateSbn(pkg, uid, channel, tag, null);
168         mAssistant.setFakeRanking(generateRanking(sbn, channel));
169         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
170         mAssistant.setFakeRanking(mock(Ranking.class));
171         NotificationStats stats = new NotificationStats();
172         stats.setDismissalSurface(NotificationStats.DISMISSAL_SHADE);
173         stats.setSeen();
174         mAssistant.onNotificationRemoved(
175                 sbn, mock(RankingMap.class), stats, NotificationListenerService.REASON_CANCEL);
176     }
177 
178     @Test
testNoAdjustmentForInitialPost()179     public void testNoAdjustmentForInitialPost() throws Exception {
180         StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C1, null, null);
181 
182         mAssistant.setFakeRanking(generateRanking(sbn, P1C1));
183         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
184 
185         verify(mNoMan, never()).applyEnqueuedAdjustmentFromAssistant(any(), any());
186     }
187 
188     @Test
testTriggerAdjustment()189     public void testTriggerAdjustment() throws Exception {
190         almostBlockChannel(PKG1, UID1, P1C1);
191         dismissBadNotification(PKG1, UID1, P1C1, "trigger!");
192 
193         StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C1, "new one!", null);
194         mAssistant.setFakeRanking(generateRanking(sbn, P1C1));
195         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
196 
197         ArgumentCaptor<Adjustment> captor = ArgumentCaptor.forClass(Adjustment.class);
198         verify(mNoMan, times(1)).applyEnqueuedAdjustmentFromAssistant(any(), captor.capture());
199         assertEquals(sbn.getKey(), captor.getValue().getKey());
200         assertEquals(Ranking.USER_SENTIMENT_NEGATIVE,
201                 captor.getValue().getSignals().getInt(Adjustment.KEY_USER_SENTIMENT));
202     }
203 
204     @Test
testMinCannotTriggerAdjustment()205     public void testMinCannotTriggerAdjustment() throws Exception {
206         almostBlockChannel(PKG1, UID1, P1C3);
207         dismissBadNotification(PKG1, UID1, P1C3, "trigger!");
208 
209         StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C3, "new one!", null);
210         mAssistant.setFakeRanking(generateRanking(sbn, P1C3));
211         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
212 
213         verify(mNoMan, never()).applyEnqueuedAdjustmentFromAssistant(any(), any());
214     }
215 
216     @Test
testGroupChildCanTriggerAdjustment()217     public void testGroupChildCanTriggerAdjustment() throws Exception {
218         almostBlockChannel(PKG1, UID1, P1C1);
219 
220         StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C1, "no", "I HAVE A GROUP");
221         mAssistant.setFakeRanking(generateRanking(sbn, P1C1));
222         NotificationStats stats = new NotificationStats();
223         stats.setDismissalSurface(NotificationStats.DISMISSAL_SHADE);
224         stats.setSeen();
225         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
226         mAssistant.onNotificationRemoved(
227                 sbn, mock(RankingMap.class), stats, NotificationListenerService.REASON_CANCEL);
228 
229         sbn = generateSbn(PKG1, UID1, P1C1, "new one!", "group");
230         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
231 
232         ArgumentCaptor<Adjustment> captor = ArgumentCaptor.forClass(Adjustment.class);
233         verify(mNoMan, times(1)).applyEnqueuedAdjustmentFromAssistant(any(), captor.capture());
234         assertEquals(sbn.getKey(), captor.getValue().getKey());
235         assertEquals(Ranking.USER_SENTIMENT_NEGATIVE,
236                 captor.getValue().getSignals().getInt(Adjustment.KEY_USER_SENTIMENT));
237     }
238 
239     @Test
testGroupSummaryCannotTriggerAdjustment()240     public void testGroupSummaryCannotTriggerAdjustment() throws Exception {
241         almostBlockChannel(PKG1, UID1, P1C1);
242 
243         StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C1, "no", "I HAVE A GROUP");
244         sbn.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY;
245         mAssistant.setFakeRanking(generateRanking(sbn, P1C1));
246         NotificationStats stats = new NotificationStats();
247         stats.setDismissalSurface(NotificationStats.DISMISSAL_SHADE);
248         stats.setSeen();
249         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
250         mAssistant.onNotificationRemoved(
251                 sbn, mock(RankingMap.class), stats, NotificationListenerService.REASON_CANCEL);
252 
253         sbn = generateSbn(PKG1, UID1, P1C1, "new one!", "group");
254         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
255 
256         verify(mNoMan, never()).applyEnqueuedAdjustmentFromAssistant(any(), any());
257     }
258 
259     @Test
testAodCannotTriggerAdjustment()260     public void testAodCannotTriggerAdjustment() throws Exception {
261         almostBlockChannel(PKG1, UID1, P1C1);
262 
263         StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C1, "no", null);
264         mAssistant.setFakeRanking(generateRanking(sbn, P1C1));
265         NotificationStats stats = new NotificationStats();
266         stats.setDismissalSurface(NotificationStats.DISMISSAL_AOD);
267         stats.setSeen();
268         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
269         mAssistant.onNotificationRemoved(
270                 sbn, mock(RankingMap.class), stats, NotificationListenerService.REASON_CANCEL);
271 
272         sbn = generateSbn(PKG1, UID1, P1C1, "new one!", null);
273         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
274 
275         verify(mNoMan, never()).applyEnqueuedAdjustmentFromAssistant(any(), any());
276     }
277 
278     @Test
testInteractedCannotTriggerAdjustment()279     public void testInteractedCannotTriggerAdjustment() throws Exception {
280         almostBlockChannel(PKG1, UID1, P1C1);
281         StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C1, "no", null);
282         mAssistant.setFakeRanking(generateRanking(sbn, P1C1));
283         NotificationStats stats = new NotificationStats();
284         stats.setDismissalSurface(NotificationStats.DISMISSAL_SHADE);
285         stats.setSeen();
286         stats.setExpanded();
287         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
288         mAssistant.onNotificationRemoved(
289                 sbn, mock(RankingMap.class), stats, NotificationListenerService.REASON_CANCEL);
290 
291         sbn = generateSbn(PKG1, UID1, P1C1, "new one!", null);
292         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
293 
294         verify(mNoMan, never()).applyEnqueuedAdjustmentFromAssistant(any(), any());
295     }
296 
297     @Test
testAppDismissedCannotTriggerAdjustment()298     public void testAppDismissedCannotTriggerAdjustment() throws Exception {
299         almostBlockChannel(PKG1, UID1, P1C1);
300 
301         StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C1, "no", null);
302         mAssistant.setFakeRanking(generateRanking(sbn, P1C1));
303         NotificationStats stats = new NotificationStats();
304         stats.setDismissalSurface(NotificationStats.DISMISSAL_SHADE);
305         stats.setSeen();
306         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
307         mAssistant.onNotificationRemoved(
308                 sbn, mock(RankingMap.class), stats, NotificationListenerService.REASON_APP_CANCEL);
309 
310         sbn = generateSbn(PKG1, UID1, P1C1, "new one!", null);
311         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
312 
313         verify(mNoMan, never()).applyEnqueuedAdjustmentFromAssistant(any(), any());
314     }
315 
316     @Test
testAppSeparation()317     public void testAppSeparation() throws Exception {
318         almostBlockChannel(PKG1, UID1, P1C1);
319         dismissBadNotification(PKG1, UID1, P1C1, "trigger!");
320 
321         StatusBarNotification sbn = generateSbn(PKG2, UID2, P2C1, "new app!", null);
322         mAssistant.setFakeRanking(generateRanking(sbn, P2C1));
323         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
324 
325         verify(mNoMan, never()).applyEnqueuedAdjustmentFromAssistant(any(), any());
326     }
327 
328     @Test
testChannelSeparation()329     public void testChannelSeparation() throws Exception {
330         almostBlockChannel(PKG1, UID1, P1C1);
331         dismissBadNotification(PKG1, UID1, P1C1, "trigger!");
332 
333         StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C2, "new app!", null);
334         mAssistant.setFakeRanking(generateRanking(sbn, P1C2));
335         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
336 
337         verify(mNoMan, never()).applyEnqueuedAdjustmentFromAssistant(any(), any());
338     }
339 
340     @Test
testReadXml()341     public void testReadXml() throws Exception {
342         String key1 = mAssistant.getKey("pkg1", 1, "channel1");
343         int streak1 = 2;
344         int views1 = 5;
345         int dismiss1 = 9;
346 
347         int streak1a = 3;
348         int views1a = 10;
349         int dismiss1a = 99;
350         String key1a = mAssistant.getKey("pkg1", 1, "channel1a");
351 
352         int streak2 = 7;
353         int views2 = 77;
354         int dismiss2 = 777;
355         String key2 = mAssistant.getKey("pkg2", 2, "channel2");
356 
357         String xml = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>"
358                 + "<assistant version=\"1\">\n"
359                 + "<impression-set key=\"" + key1 + "\" "
360                 + "dismisses=\"" + dismiss1 + "\" views=\"" + views1
361                 + "\" streak=\"" + streak1 + "\"/>\n"
362                 + "<impression-set key=\"" + key1a + "\" "
363                 + "dismisses=\"" + dismiss1a + "\" views=\"" + views1a
364                 + "\" streak=\"" + streak1a + "\"/>\n"
365                 + "<impression-set key=\"" + key2 + "\" "
366                 + "dismisses=\"" + dismiss2 + "\" views=\"" + views2
367                 + "\" streak=\"" + streak2 + "\"/>\n"
368                 + "</assistant>\n";
369         mAssistant.readXml(new BufferedInputStream(new ByteArrayInputStream(xml.getBytes())));
370 
371         ChannelImpressions c1 = mAssistant.getImpressions(key1);
372         assertEquals(2, c1.getStreak());
373         assertEquals(5, c1.getViews());
374         assertEquals(9, c1.getDismissals());
375 
376         ChannelImpressions c1a = mAssistant.getImpressions(key1a);
377         assertEquals(3, c1a.getStreak());
378         assertEquals(10, c1a.getViews());
379         assertEquals(99, c1a.getDismissals());
380 
381         ChannelImpressions c2 = mAssistant.getImpressions(key2);
382         assertEquals(7, c2.getStreak());
383         assertEquals(77, c2.getViews());
384         assertEquals(777, c2.getDismissals());
385     }
386 
387     @Test
testRoundTripXml()388     public void testRoundTripXml() throws Exception {
389         String key1 = mAssistant.getKey("pkg1", 1, "channel1");
390         ChannelImpressions ci1 = new ChannelImpressions();
391         String key2 = mAssistant.getKey("pkg1", 1, "channel2");
392         ChannelImpressions ci2 = new ChannelImpressions();
393         for (int i = 0; i < 3; i++) {
394             ci2.incrementViews();
395             ci2.incrementDismissals();
396         }
397         ChannelImpressions ci3 = new ChannelImpressions();
398         String key3 = mAssistant.getKey("pkg3", 3, "channel2");
399         for (int i = 0; i < 9; i++) {
400             ci3.incrementViews();
401             if (i % 3 == 0) {
402                 ci3.incrementDismissals();
403             }
404         }
405 
406         mAssistant.insertImpressions(key1, ci1);
407         mAssistant.insertImpressions(key2, ci2);
408         mAssistant.insertImpressions(key3, ci3);
409 
410         XmlSerializer serializer = new FastXmlSerializer();
411         ByteArrayOutputStream baos = new ByteArrayOutputStream();
412         serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
413         mAssistant.writeXml(serializer);
414 
415         Assistant assistant = new Assistant();
416         // onCreate is not invoked, so settings won't be initialised, unless we do it here.
417         assistant.mSettings = mAssistant.mSettings;
418         assistant.readXml(new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())));
419 
420         assertEquals(ci1, assistant.getImpressions(key1));
421         assertEquals(ci2, assistant.getImpressions(key2));
422         assertEquals(ci3, assistant.getImpressions(key3));
423     }
424 
425     @Test
testSettingsProviderUpdate()426     public void testSettingsProviderUpdate() {
427         // Set up channels
428         String key = mAssistant.getKey("pkg1", 1, "channel1");
429         ChannelImpressions ci = new ChannelImpressions();
430         for (int i = 0; i < 3; i++) {
431             ci.incrementViews();
432             if (i % 2 == 0) {
433                 ci.incrementDismissals();
434             }
435         }
436 
437         mAssistant.insertImpressions(key, ci);
438 
439         // With default values, the blocking helper shouldn't be triggered.
440         assertEquals(false, ci.shouldTriggerBlock());
441 
442         // Update settings values.
443         mAssistant.mSettings.mDismissToViewRatioLimit = 0f;
444         mAssistant.mSettings.mStreakLimit = 0;
445 
446         // Notify for the settings values we updated.
447         mAssistant.mSettings.mOnUpdateRunnable.run();
448 
449         // With the new threshold, the blocking helper should be triggered.
450         assertEquals(true, ci.shouldTriggerBlock());
451     }
452 
453     @Test
testTrimLiveNotifications()454     public void testTrimLiveNotifications() {
455         StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C1, "no", null);
456         mAssistant.setFakeRanking(generateRanking(sbn, P1C1));
457 
458         mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
459 
460         assertTrue(mAssistant.mLiveNotifications.containsKey(sbn.getKey()));
461 
462         mAssistant.onNotificationRemoved(
463                 sbn, mock(RankingMap.class), new NotificationStats(), 0);
464 
465         assertFalse(mAssistant.mLiveNotifications.containsKey(sbn.getKey()));
466     }
467 
468     @Test
testAssistantNeverIncreasesImportanceWhenSuggestingSilent()469     public void testAssistantNeverIncreasesImportanceWhenSuggestingSilent() throws Exception {
470         StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C3, "min notif!", null);
471         Adjustment adjust = mAssistant.createEnqueuedNotificationAdjustment(
472                 new NotificationEntry(mContext, mPackageManager, sbn, P1C3, mSmsHelper),
473                 new ArrayList<>(),
474                 new ArrayList<>());
475         assertEquals(IMPORTANCE_MIN, adjust.getSignals().getInt(Adjustment.KEY_IMPORTANCE));
476     }
477 }
478