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 
17 package com.android.systemui.statusbar.notification.collection;
18 
19 import static android.app.Notification.CATEGORY_ALARM;
20 import static android.app.Notification.CATEGORY_CALL;
21 import static android.app.Notification.CATEGORY_EVENT;
22 import static android.app.Notification.CATEGORY_MESSAGE;
23 import static android.app.Notification.CATEGORY_REMINDER;
24 import static android.app.Notification.FLAG_FSI_REQUESTED_BUT_DENIED;
25 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT;
26 
27 import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking;
28 import static com.android.systemui.statusbar.NotificationEntryHelper.modifySbn;
29 
30 import static org.junit.Assert.assertEquals;
31 import static org.junit.Assert.assertFalse;
32 import static org.junit.Assert.assertTrue;
33 import static org.mockito.Mockito.doReturn;
34 import static org.mockito.Mockito.mock;
35 
36 import android.app.ActivityManager;
37 import android.app.Notification;
38 import android.app.NotificationChannel;
39 import android.app.PendingIntent;
40 import android.app.Person;
41 import android.content.Intent;
42 import android.graphics.drawable.Icon;
43 import android.media.session.MediaSession;
44 import android.os.Bundle;
45 import android.os.UserHandle;
46 import android.service.notification.NotificationListenerService.Ranking;
47 import android.service.notification.SnoozeCriterion;
48 import android.service.notification.StatusBarNotification;
49 
50 import androidx.test.ext.junit.runners.AndroidJUnit4;
51 import androidx.test.filters.SmallTest;
52 
53 import com.android.systemui.SysuiTestCase;
54 import com.android.systemui.res.R;
55 import com.android.systemui.statusbar.RankingBuilder;
56 import com.android.systemui.statusbar.SbnBuilder;
57 import com.android.systemui.util.time.FakeSystemClock;
58 
59 import org.junit.Before;
60 import org.junit.Test;
61 import org.junit.runner.RunWith;
62 import org.mockito.Mockito;
63 
64 import java.util.ArrayList;
65 
66 @SmallTest
67 @RunWith(AndroidJUnit4.class)
68 public class NotificationEntryTest extends SysuiTestCase {
69     private static final String TEST_PACKAGE_NAME = "test";
70     private static final int TEST_UID = 0;
71     private static final int UID_NORMAL = 123;
72     private static final NotificationChannel NOTIFICATION_CHANNEL =
73             new NotificationChannel("id", "name", NotificationChannel.USER_LOCKED_IMPORTANCE);
74 
75     private int mId;
76 
77     private NotificationEntry mEntry;
78     private NotificationChannel mChannel = Mockito.mock(NotificationChannel.class);
79     private final FakeSystemClock mClock = new FakeSystemClock();
80 
81     @Before
setup()82     public void setup() {
83         Notification.Builder n = new Notification.Builder(mContext, "")
84                 .setSmallIcon(R.drawable.ic_person)
85                 .setContentTitle("Title")
86                 .setContentText("Text");
87 
88         mEntry = new NotificationEntryBuilder()
89                 .setPkg(TEST_PACKAGE_NAME)
90                 .setOpPkg(TEST_PACKAGE_NAME)
91                 .setUid(TEST_UID)
92                 .setChannel(mChannel)
93                 .setId(mId++)
94                 .setNotification(n.build())
95                 .setUser(new UserHandle(ActivityManager.getCurrentUser()))
96                 .build();
97 
98         doReturn(false).when(mChannel).isBlockable();
99     }
100 
101     @Test
testIsExemptFromDndVisualSuppression_foreground()102     public void testIsExemptFromDndVisualSuppression_foreground() {
103         mEntry.getSbn().getNotification().flags = Notification.FLAG_FOREGROUND_SERVICE;
104 
105         assertTrue(mEntry.isExemptFromDndVisualSuppression());
106         assertFalse(mEntry.shouldSuppressAmbient());
107     }
108 
109     @Test
testBlockableEntryWhenCritical()110     public void testBlockableEntryWhenCritical() {
111         doReturn(true).when(mChannel).isBlockable();
112         mEntry.setRanking(mEntry.getRanking());
113 
114         assertTrue(mEntry.isBlockable());
115     }
116 
117 
118     @Test
testBlockableEntryWhenCriticalAndChannelNotBlockable()119     public void testBlockableEntryWhenCriticalAndChannelNotBlockable() {
120         doReturn(true).when(mChannel).isBlockable();
121         doReturn(true).when(mChannel).isImportanceLockedByCriticalDeviceFunction();
122         mEntry.setRanking(mEntry.getRanking());
123 
124         assertTrue(mEntry.isBlockable());
125     }
126 
127     @Test
testNonBlockableEntryWhenCriticalAndChannelNotBlockable()128     public void testNonBlockableEntryWhenCriticalAndChannelNotBlockable() {
129         doReturn(false).when(mChannel).isBlockable();
130         doReturn(true).when(mChannel).isImportanceLockedByCriticalDeviceFunction();
131         mEntry.setRanking(mEntry.getRanking());
132 
133         assertFalse(mEntry.isBlockable());
134     }
135 
136     @Test
testBlockableWhenEntryHasNoChannel()137     public void testBlockableWhenEntryHasNoChannel() {
138         StatusBarNotification sbn = new SbnBuilder().build();
139         Ranking ranking = new RankingBuilder()
140                 .setChannel(null)
141                 .setKey(sbn.getKey())
142                 .build();
143 
144         NotificationEntry entry =
145                 new NotificationEntry(sbn, ranking, mClock.uptimeMillis());
146 
147         assertFalse(entry.isBlockable());
148     }
149 
150     @Test
testIsExemptFromDndVisualSuppression_media()151     public void testIsExemptFromDndVisualSuppression_media() {
152         Notification.Builder n = new Notification.Builder(mContext, "")
153                 .setStyle(new Notification.MediaStyle()
154                         .setMediaSession(mock(MediaSession.Token.class)))
155                 .setSmallIcon(R.drawable.ic_person)
156                 .setContentTitle("Title")
157                 .setContentText("Text");
158         NotificationEntry e1 = new NotificationEntryBuilder()
159                 .setNotification(n.build())
160                 .build();
161 
162         assertTrue(e1.isExemptFromDndVisualSuppression());
163         assertFalse(e1.shouldSuppressAmbient());
164     }
165 
166     @Test
testIsExemptFromDndVisualSuppression_system()167     public void testIsExemptFromDndVisualSuppression_system() {
168         doReturn(true).when(mChannel).isImportanceLockedByCriticalDeviceFunction();
169         doReturn(false).when(mChannel).isBlockable();
170 
171         mEntry.setRanking(mEntry.getRanking());
172 
173         assertFalse(mEntry.isBlockable());
174         assertTrue(mEntry.isExemptFromDndVisualSuppression());
175         assertFalse(mEntry.shouldSuppressAmbient());
176     }
177 
178     @Test
testIsNotExemptFromDndVisualSuppression_hiddenCategories()179     public void testIsNotExemptFromDndVisualSuppression_hiddenCategories() {
180         NotificationEntry entry = new NotificationEntryBuilder()
181                 .setUid(UID_NORMAL)
182                 .build();
183         doReturn(true).when(mChannel).isImportanceLockedByCriticalDeviceFunction();
184         modifyRanking(entry).setSuppressedVisualEffects(SUPPRESSED_EFFECT_AMBIENT).build();
185 
186         modifySbn(entry)
187                 .setNotification(
188                         new Notification.Builder(mContext, "").setCategory(CATEGORY_CALL).build())
189                 .build();
190         assertFalse(entry.isExemptFromDndVisualSuppression());
191         assertTrue(entry.shouldSuppressAmbient());
192 
193         modifySbn(entry)
194                 .setNotification(
195                         new Notification.Builder(mContext, "")
196                                 .setCategory(CATEGORY_REMINDER)
197                                 .build())
198                 .build();
199         assertFalse(entry.isExemptFromDndVisualSuppression());
200 
201         modifySbn(entry)
202                 .setNotification(
203                         new Notification.Builder(mContext, "").setCategory(CATEGORY_ALARM).build())
204                 .build();
205         assertFalse(entry.isExemptFromDndVisualSuppression());
206 
207         modifySbn(entry)
208                 .setNotification(
209                         new Notification.Builder(mContext, "").setCategory(CATEGORY_EVENT).build())
210                 .build();
211         assertFalse(entry.isExemptFromDndVisualSuppression());
212 
213         modifySbn(entry)
214                 .setNotification(
215                         new Notification.Builder(mContext, "")
216                                 .setCategory(CATEGORY_MESSAGE)
217                                 .build())
218                 .build();
219         assertFalse(entry.isExemptFromDndVisualSuppression());
220     }
221 
222     @Test
testCreateNotificationDataEntry_RankingUpdate()223     public void testCreateNotificationDataEntry_RankingUpdate() {
224         StatusBarNotification sbn = new SbnBuilder().build();
225         sbn.getNotification().actions =
226                 new Notification.Action[]{createContextualAction("appGeneratedAction")};
227 
228         ArrayList<Notification.Action> systemGeneratedSmartActions =
229                 createActions("systemGeneratedAction");
230 
231         SnoozeCriterion snoozeCriterion = new SnoozeCriterion("id", "explanation", "confirmation");
232         ArrayList<SnoozeCriterion> snoozeCriterions = new ArrayList<>();
233         snoozeCriterions.add(snoozeCriterion);
234 
235         Ranking ranking = new RankingBuilder()
236                 .setKey(sbn.getKey())
237                 .setSmartActions(systemGeneratedSmartActions)
238                 .setChannel(NOTIFICATION_CHANNEL)
239                 .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE)
240                 .setSnoozeCriteria(snoozeCriterions)
241                 .build();
242 
243         NotificationEntry entry =
244                 new NotificationEntry(sbn, ranking, mClock.uptimeMillis());
245 
246         assertEquals(systemGeneratedSmartActions, entry.getSmartActions());
247         assertEquals(NOTIFICATION_CHANNEL, entry.getChannel());
248         assertEquals(Ranking.USER_SENTIMENT_NEGATIVE, entry.getUserSentiment());
249         assertEquals(snoozeCriterions, entry.getSnoozeCriteria());
250     }
251 
252     @Test
testIsStickyAndNotDemoted_noFlagAndDemoted_returnFalse()253     public void testIsStickyAndNotDemoted_noFlagAndDemoted_returnFalse() {
254         mEntry.getSbn().getNotification().flags &= ~FLAG_FSI_REQUESTED_BUT_DENIED;
255         assertFalse(mEntry.isStickyAndNotDemoted());
256     }
257 
258     @Test
testIsStickyAndNotDemoted_noFlagAndNotDemoted_demoteAndReturnFalse()259     public void testIsStickyAndNotDemoted_noFlagAndNotDemoted_demoteAndReturnFalse() {
260         mEntry.getSbn().getNotification().flags &= ~FLAG_FSI_REQUESTED_BUT_DENIED;
261 
262         assertFalse(mEntry.isStickyAndNotDemoted());
263         assertTrue(mEntry.isDemoted());
264     }
265 
266     @Test
testIsStickyAndNotDemoted_hasFlagButAlreadyDemoted_returnFalse()267     public void testIsStickyAndNotDemoted_hasFlagButAlreadyDemoted_returnFalse() {
268         mEntry.getSbn().getNotification().flags |= FLAG_FSI_REQUESTED_BUT_DENIED;
269         mEntry.demoteStickyHun();
270 
271         assertFalse(mEntry.isStickyAndNotDemoted());
272     }
273 
274     @Test
testIsStickyAndNotDemoted_hasFlagAndNotDemoted_returnTrue()275     public void testIsStickyAndNotDemoted_hasFlagAndNotDemoted_returnTrue() {
276         mEntry.getSbn().getNotification().flags |= FLAG_FSI_REQUESTED_BUT_DENIED;
277 
278         assertFalse(mEntry.isDemoted());
279         assertTrue(mEntry.isStickyAndNotDemoted());
280     }
281 
282     @Test
testIsNotificationVisibilityPrivate_true()283     public void testIsNotificationVisibilityPrivate_true() {
284         assertTrue(mEntry.isNotificationVisibilityPrivate());
285     }
286 
287     @Test
testIsNotificationVisibilityPrivate_visibilityPublic_false()288     public void testIsNotificationVisibilityPrivate_visibilityPublic_false() {
289         Notification.Builder notification = new Notification.Builder(mContext, "")
290                 .setVisibility(Notification.VISIBILITY_PUBLIC)
291                 .setSmallIcon(R.drawable.ic_person)
292                 .setContentTitle("Title")
293                 .setContentText("Text");
294 
295         NotificationEntry entry = new NotificationEntryBuilder()
296                 .setPkg(TEST_PACKAGE_NAME)
297                 .setOpPkg(TEST_PACKAGE_NAME)
298                 .setUid(TEST_UID)
299                 .setChannel(mChannel)
300                 .setId(mId++)
301                 .setNotification(notification.build())
302                 .setUser(new UserHandle(ActivityManager.getCurrentUser()))
303                 .build();
304 
305         assertFalse(entry.isNotificationVisibilityPrivate());
306     }
307 
308     @Test
testIsChannelVisibilityPrivate_true()309     public void testIsChannelVisibilityPrivate_true() {
310         assertTrue(mEntry.isChannelVisibilityPrivate());
311     }
312 
313     @Test
testIsChannelVisibilityPrivate_visibilityPublic_false()314     public void testIsChannelVisibilityPrivate_visibilityPublic_false() {
315         NotificationChannel channel =
316                 new NotificationChannel("id", "name", NotificationChannel.USER_LOCKED_IMPORTANCE);
317         channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
318         StatusBarNotification sbn = new SbnBuilder().build();
319         Ranking ranking = new RankingBuilder()
320                 .setChannel(channel)
321                 .setKey(sbn.getKey())
322                 .build();
323         NotificationEntry entry =
324                 new NotificationEntry(sbn, ranking, mClock.uptimeMillis());
325 
326         assertFalse(entry.isChannelVisibilityPrivate());
327     }
328 
329     @Test
testIsChannelVisibilityPrivate_entryHasNoChannel_false()330     public void testIsChannelVisibilityPrivate_entryHasNoChannel_false() {
331         StatusBarNotification sbn = new SbnBuilder().build();
332         Ranking ranking = new RankingBuilder()
333                 .setChannel(null)
334                 .setKey(sbn.getKey())
335                 .build();
336         NotificationEntry entry =
337                 new NotificationEntry(sbn, ranking, mClock.uptimeMillis());
338 
339         assertFalse(entry.isChannelVisibilityPrivate());
340     }
341 
342     @Test
notificationDataEntry_testIsLastMessageFromReply()343     public void notificationDataEntry_testIsLastMessageFromReply() {
344         Person.Builder person = new Person.Builder()
345                 .setName("name")
346                 .setKey("abc")
347                 .setUri("uri")
348                 .setBot(true);
349 
350         // EXTRA_MESSAGING_PERSON is the same Person as the sender in last message in EXTRA_MESSAGES
351         Bundle bundle = new Bundle();
352         bundle.putParcelable(Notification.EXTRA_MESSAGING_PERSON, person.build());
353         Bundle[] messagesBundle = new Bundle[]{new Notification.MessagingStyle.Message(
354                 "text", 0, person.build()).toBundle()};
355         bundle.putParcelableArray(Notification.EXTRA_MESSAGES, messagesBundle);
356 
357         Notification notification = new Notification.Builder(mContext, "test")
358                 .addExtras(bundle)
359                 .build();
360 
361         NotificationEntry entry = new NotificationEntryBuilder()
362                 .setPkg("pkg")
363                 .setOpPkg("pkg")
364                 .setTag("tag")
365                 .setNotification(notification)
366                 .setUser(mContext.getUser())
367                 .setOverrideGroupKey("")
368                 .build();
369         entry.setHasSentReply();
370 
371         assertTrue(entry.isLastMessageFromReply());
372     }
373 
374     @Test
notificationDataEntry_testIsLastMessageFromReply_invalidPerson_noCrash()375     public void notificationDataEntry_testIsLastMessageFromReply_invalidPerson_noCrash() {
376         Person.Builder person = new Person.Builder()
377                 .setName("name")
378                 .setKey("abc")
379                 .setUri("uri")
380                 .setBot(true);
381 
382         Bundle bundle = new Bundle();
383         // should be Person.class
384         bundle.putParcelable(Notification.EXTRA_MESSAGING_PERSON, new Bundle());
385         Bundle[] messagesBundle = new Bundle[]{new Notification.MessagingStyle.Message(
386                 "text", 0, person.build()).toBundle()};
387         bundle.putParcelableArray(Notification.EXTRA_MESSAGES, messagesBundle);
388 
389         Notification notification = new Notification.Builder(mContext, "test")
390                 .addExtras(bundle)
391                 .build();
392 
393         NotificationEntry entry = new NotificationEntryBuilder()
394                 .setPkg("pkg")
395                 .setOpPkg("pkg")
396                 .setTag("tag")
397                 .setNotification(notification)
398                 .setUser(mContext.getUser())
399                 .setOverrideGroupKey("")
400                 .build();
401         entry.setHasSentReply();
402 
403         entry.isLastMessageFromReply();
404 
405         // no crash, good
406     }
407 
408 
createContextualAction(String title)409     private Notification.Action createContextualAction(String title) {
410         return new Notification.Action.Builder(
411                 Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon),
412                 title,
413                 PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"),
414                     PendingIntent.FLAG_IMMUTABLE))
415                 .setContextual(true)
416                 .build();
417     }
418 
createAction(String title)419     private Notification.Action createAction(String title) {
420         return new Notification.Action.Builder(
421                 Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon),
422                 title,
423                 PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"),
424                     PendingIntent.FLAG_IMMUTABLE)).build();
425     }
426 
createActions(String... titles)427     private ArrayList<Notification.Action> createActions(String... titles) {
428         ArrayList<Notification.Action> actions = new ArrayList<>();
429         for (String title : titles) {
430             actions.add(createAction(title));
431         }
432         return actions;
433     }
434 }
435