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