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