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