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 import android.testing.AndroidTestingRunner; 50 51 import androidx.test.filters.SmallTest; 52 53 import com.android.systemui.R; 54 import com.android.systemui.SysuiTestCase; 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(AndroidTestingRunner.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 notificationDataEntry_testIsLastMessageFromReply()283 public void notificationDataEntry_testIsLastMessageFromReply() { 284 Person.Builder person = new Person.Builder() 285 .setName("name") 286 .setKey("abc") 287 .setUri("uri") 288 .setBot(true); 289 290 // EXTRA_MESSAGING_PERSON is the same Person as the sender in last message in EXTRA_MESSAGES 291 Bundle bundle = new Bundle(); 292 bundle.putParcelable(Notification.EXTRA_MESSAGING_PERSON, person.build()); 293 Bundle[] messagesBundle = new Bundle[]{new Notification.MessagingStyle.Message( 294 "text", 0, person.build()).toBundle()}; 295 bundle.putParcelableArray(Notification.EXTRA_MESSAGES, messagesBundle); 296 297 Notification notification = new Notification.Builder(mContext, "test") 298 .addExtras(bundle) 299 .build(); 300 301 NotificationEntry entry = new NotificationEntryBuilder() 302 .setPkg("pkg") 303 .setOpPkg("pkg") 304 .setTag("tag") 305 .setNotification(notification) 306 .setUser(mContext.getUser()) 307 .setOverrideGroupKey("") 308 .build(); 309 entry.setHasSentReply(); 310 311 assertTrue(entry.isLastMessageFromReply()); 312 } 313 314 @Test notificationDataEntry_testIsLastMessageFromReply_invalidPerson_noCrash()315 public void notificationDataEntry_testIsLastMessageFromReply_invalidPerson_noCrash() { 316 Person.Builder person = new Person.Builder() 317 .setName("name") 318 .setKey("abc") 319 .setUri("uri") 320 .setBot(true); 321 322 Bundle bundle = new Bundle(); 323 // should be Person.class 324 bundle.putParcelable(Notification.EXTRA_MESSAGING_PERSON, new Bundle()); 325 Bundle[] messagesBundle = new Bundle[]{new Notification.MessagingStyle.Message( 326 "text", 0, person.build()).toBundle()}; 327 bundle.putParcelableArray(Notification.EXTRA_MESSAGES, messagesBundle); 328 329 Notification notification = new Notification.Builder(mContext, "test") 330 .addExtras(bundle) 331 .build(); 332 333 NotificationEntry entry = new NotificationEntryBuilder() 334 .setPkg("pkg") 335 .setOpPkg("pkg") 336 .setTag("tag") 337 .setNotification(notification) 338 .setUser(mContext.getUser()) 339 .setOverrideGroupKey("") 340 .build(); 341 entry.setHasSentReply(); 342 343 entry.isLastMessageFromReply(); 344 345 // no crash, good 346 } 347 348 createContextualAction(String title)349 private Notification.Action createContextualAction(String title) { 350 return new Notification.Action.Builder( 351 Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon), 352 title, 353 PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"), 354 PendingIntent.FLAG_IMMUTABLE)) 355 .setContextual(true) 356 .build(); 357 } 358 createAction(String title)359 private Notification.Action createAction(String title) { 360 return new Notification.Action.Builder( 361 Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon), 362 title, 363 PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"), 364 PendingIntent.FLAG_IMMUTABLE)).build(); 365 } 366 createActions(String... titles)367 private ArrayList<Notification.Action> createActions(String... titles) { 368 ArrayList<Notification.Action> actions = new ArrayList<>(); 369 for (String title : titles) { 370 actions.add(createAction(title)); 371 } 372 return actions; 373 } 374 } 375