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 android.app.notification.legacy29.cts; 18 19 import static junit.framework.Assert.assertEquals; 20 import static junit.framework.Assert.assertTrue; 21 import static junit.framework.TestCase.assertFalse; 22 import static junit.framework.TestCase.assertNotNull; 23 24 import static org.junit.Assert.assertNotEquals; 25 import static org.junit.Assert.fail; 26 27 import android.app.ActivityManager; 28 import android.app.Instrumentation; 29 import android.app.Notification; 30 import android.app.NotificationChannel; 31 import android.app.NotificationManager; 32 import android.app.PendingIntent; 33 import android.app.UiAutomation; 34 import android.content.ComponentName; 35 import android.content.Context; 36 import android.content.Intent; 37 import android.os.Bundle; 38 import android.os.ParcelFileDescriptor; 39 import android.provider.Telephony; 40 import android.service.notification.Adjustment; 41 import android.service.notification.NotificationAssistantService; 42 import android.service.notification.NotificationListenerService; 43 import android.service.notification.StatusBarNotification; 44 45 import junit.framework.Assert; 46 47 import org.junit.After; 48 import org.junit.Before; 49 import org.junit.Test; 50 import org.junit.runner.RunWith; 51 52 import java.io.FileInputStream; 53 import java.io.IOException; 54 import java.io.InputStream; 55 import java.util.ArrayList; 56 import java.util.List; 57 import java.util.concurrent.TimeUnit; 58 59 import androidx.test.InstrumentationRegistry; 60 import androidx.test.runner.AndroidJUnit4; 61 62 @RunWith(AndroidJUnit4.class) 63 public class NotificationAssistantServiceTest { 64 65 final String TAG = "NotAsstServiceTest"; 66 final String NOTIFICATION_CHANNEL_ID = "NotificationAssistantServiceTest"; 67 final int ICON_ID = android.R.drawable.sym_def_app_icon; 68 final long SLEEP_TIME = 500; // milliseconds 69 70 private TestNotificationAssistant mNotificationAssistantService; 71 private TestNotificationListener mNotificationListenerService; 72 private NotificationManager mNotificationManager; 73 private ActivityManager mActivityManager; 74 private Context mContext; 75 private UiAutomation mUi; 76 77 @Before 78 public void setUp() throws IOException { 79 mUi = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 80 mContext = InstrumentationRegistry.getContext(); 81 mNotificationManager = (NotificationManager) mContext.getSystemService( 82 Context.NOTIFICATION_SERVICE); 83 mNotificationManager.createNotificationChannel(new NotificationChannel( 84 NOTIFICATION_CHANNEL_ID, "name", NotificationManager.IMPORTANCE_DEFAULT)); 85 mActivityManager = mContext.getSystemService(ActivityManager.class); 86 } 87 88 @After 89 public void tearDown() throws IOException { 90 if (mNotificationListenerService != null) mNotificationListenerService.resetData(); 91 if (!mActivityManager.isLowRamDevice()) { 92 toggleListenerAccess(false); 93 toggleAssistantAccess(false); 94 } 95 mUi.dropShellPermissionIdentity(); 96 } 97 98 @Test 99 public void testOnNotificationEnqueued() throws Exception { 100 if (mActivityManager.isLowRamDevice()) { 101 return; 102 } 103 toggleListenerAccess(true); 104 Thread.sleep(SLEEP_TIME); 105 106 mUi.adoptShellPermissionIdentity("android.permission.STATUS_BAR_SERVICE"); 107 mNotificationManager.allowAssistantAdjustment(Adjustment.KEY_USER_SENTIMENT); 108 mUi.dropShellPermissionIdentity(); 109 110 mNotificationListenerService = TestNotificationListener.getInstance(); 111 112 sendNotification(1, ICON_ID); 113 StatusBarNotification sbn = getFirstNotificationFromPackage(TestNotificationListener.PKG); 114 NotificationListenerService.Ranking out = new NotificationListenerService.Ranking(); 115 mNotificationListenerService.mRankingMap.getRanking(sbn.getKey(), out); 116 117 // No modification because the Notification Assistant is not enabled 118 assertEquals(NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL, 119 out.getUserSentiment()); 120 mNotificationListenerService.resetData(); 121 122 toggleAssistantAccess(true); 123 Thread.sleep(SLEEP_TIME); // wait for listener and assistant to be allowed 124 mNotificationAssistantService = TestNotificationAssistant.getInstance(); 125 126 sendNotification(1, ICON_ID); 127 sbn = getFirstNotificationFromPackage(TestNotificationListener.PKG); 128 mNotificationListenerService.mRankingMap.getRanking(sbn.getKey(), out); 129 130 // Assistant modifies notification 131 assertEquals(NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE, 132 out.getUserSentiment()); 133 } 134 135 @Test 136 public void testAdjustNotification_userSentimentKey() throws Exception { 137 if (mActivityManager.isLowRamDevice()) { 138 return; 139 } 140 setUpListeners(); 141 142 mUi.adoptShellPermissionIdentity("android.permission.STATUS_BAR_SERVICE"); 143 mNotificationManager.allowAssistantAdjustment(Adjustment.KEY_USER_SENTIMENT); 144 mUi.dropShellPermissionIdentity(); 145 146 sendNotification(1, ICON_ID); 147 StatusBarNotification sbn = getFirstNotificationFromPackage(TestNotificationListener.PKG); 148 NotificationListenerService.Ranking out = new NotificationListenerService.Ranking(); 149 mNotificationListenerService.mRankingMap.getRanking(sbn.getKey(), out); 150 151 assertEquals(NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE, 152 out.getUserSentiment()); 153 154 Bundle signals = new Bundle(); 155 signals.putInt(Adjustment.KEY_USER_SENTIMENT, 156 NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE); 157 Adjustment adjustment = new Adjustment(sbn.getPackageName(), sbn.getKey(), signals, "", 158 sbn.getUser()); 159 160 mNotificationAssistantService.adjustNotification(adjustment); 161 Thread.sleep(SLEEP_TIME); // wait for adjustment to be processed 162 163 mNotificationListenerService.mRankingMap.getRanking(sbn.getKey(), out); 164 165 assertEquals(NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE, 166 out.getUserSentiment()); 167 } 168 169 @Test 170 public void testAdjustNotification_importanceKey() throws Exception { 171 if (mActivityManager.isLowRamDevice()) { 172 return; 173 } 174 setUpListeners(); 175 176 mUi.adoptShellPermissionIdentity("android.permission.STATUS_BAR_SERVICE"); 177 mNotificationManager.allowAssistantAdjustment(Adjustment.KEY_IMPORTANCE); 178 mUi.dropShellPermissionIdentity(); 179 180 sendNotification(1, ICON_ID); 181 StatusBarNotification sbn = getFirstNotificationFromPackage(TestNotificationListener.PKG); 182 NotificationListenerService.Ranking out = new NotificationListenerService.Ranking(); 183 mNotificationListenerService.mRankingMap.getRanking(sbn.getKey(), out); 184 185 int currentImportance = out.getImportance(); 186 int newImportance = currentImportance == NotificationManager.IMPORTANCE_DEFAULT 187 ? NotificationManager.IMPORTANCE_HIGH : NotificationManager.IMPORTANCE_DEFAULT; 188 189 Bundle signals = new Bundle(); 190 signals.putInt(Adjustment.KEY_IMPORTANCE, newImportance); 191 Adjustment adjustment = new Adjustment(sbn.getPackageName(), sbn.getKey(), signals, "", 192 sbn.getUser()); 193 194 mNotificationAssistantService.adjustNotification(adjustment); 195 Thread.sleep(SLEEP_TIME); // wait for adjustment to be processed 196 197 mNotificationListenerService.mRankingMap.getRanking(sbn.getKey(), out); 198 199 assertEquals(newImportance, out.getImportance()); 200 } 201 202 @Test 203 public void testAdjustNotification_smartActionKey() throws Exception { 204 if (mActivityManager.isLowRamDevice()) { 205 return; 206 } 207 setUpListeners(); 208 209 mUi.adoptShellPermissionIdentity("android.permission.STATUS_BAR_SERVICE"); 210 mNotificationManager.allowAssistantAdjustment(Adjustment.KEY_CONTEXTUAL_ACTIONS); 211 mUi.dropShellPermissionIdentity(); 212 213 PendingIntent sendIntent = PendingIntent.getActivity(mContext, 0, 214 new Intent(Intent.ACTION_SEND), 0); 215 Notification.Action sendAction = new Notification.Action.Builder(ICON_ID, "SEND", 216 sendIntent).build(); 217 218 sendNotification(1, ICON_ID); 219 StatusBarNotification sbn = getFirstNotificationFromPackage(TestNotificationListener.PKG); 220 NotificationListenerService.Ranking out = new NotificationListenerService.Ranking(); 221 mNotificationListenerService.mRankingMap.getRanking(sbn.getKey(), out); 222 223 List<Notification.Action> smartActions = out.getSmartActions(); 224 if (smartActions != null) { 225 for (int i = 0; i < smartActions.size(); i++) { 226 Notification.Action action = smartActions.get(i); 227 assertNotEquals(sendIntent, action.actionIntent); 228 } 229 } 230 231 ArrayList<Notification.Action> extraAction = new ArrayList<>(); 232 extraAction.add(sendAction); 233 Bundle signals = new Bundle(); 234 signals.putParcelableArrayList(Adjustment.KEY_CONTEXTUAL_ACTIONS, extraAction); 235 Adjustment adjustment = new Adjustment(sbn.getPackageName(), sbn.getKey(), signals, "", 236 sbn.getUser()); 237 238 mNotificationAssistantService.adjustNotification(adjustment); 239 Thread.sleep(SLEEP_TIME); //wait for adjustment to be processed 240 241 mNotificationListenerService.mRankingMap.getRanking(sbn.getKey(), out); 242 243 boolean actionFound = false; 244 smartActions = out.getSmartActions(); 245 for (int i = 0; i < smartActions.size(); i++) { 246 Notification.Action action = smartActions.get(i); 247 actionFound = actionFound || action.actionIntent.equals(sendIntent); 248 } 249 assertTrue(actionFound); 250 } 251 252 @Test 253 public void testAdjustNotification_smartReplyKey() throws Exception { 254 if (mActivityManager.isLowRamDevice()) { 255 return; 256 } 257 setUpListeners(); 258 CharSequence smartReply = "Smart Reply!"; 259 260 mUi.adoptShellPermissionIdentity("android.permission.STATUS_BAR_SERVICE"); 261 mNotificationManager.allowAssistantAdjustment(Adjustment.KEY_TEXT_REPLIES); 262 mUi.dropShellPermissionIdentity(); 263 264 sendNotification(1, ICON_ID); 265 StatusBarNotification sbn = getFirstNotificationFromPackage(TestNotificationListener.PKG); 266 NotificationListenerService.Ranking out = new NotificationListenerService.Ranking(); 267 mNotificationListenerService.mRankingMap.getRanking(sbn.getKey(), out); 268 269 List<CharSequence> smartReplies = out.getSmartReplies(); 270 if (smartReplies != null) { 271 for (int i = 0; i < smartReplies.size(); i++) { 272 CharSequence reply = smartReplies.get(i); 273 assertNotEquals(smartReply, reply); 274 } 275 } 276 277 ArrayList<CharSequence> extraReply = new ArrayList<>(); 278 extraReply.add(smartReply); 279 Bundle signals = new Bundle(); 280 signals.putCharSequenceArrayList(Adjustment.KEY_TEXT_REPLIES, extraReply); 281 Adjustment adjustment = new Adjustment(sbn.getPackageName(), sbn.getKey(), signals, "", 282 sbn.getUser()); 283 284 mNotificationAssistantService.adjustNotification(adjustment); 285 Thread.sleep(SLEEP_TIME); //wait for adjustment to be processed 286 287 mNotificationListenerService.mRankingMap.getRanking(sbn.getKey(), out); 288 289 boolean replyFound = false; 290 smartReplies = out.getSmartReplies(); 291 for (int i = 0; i < smartReplies.size(); i++) { 292 CharSequence reply = smartReplies.get(i); 293 replyFound = replyFound || reply.equals(smartReply); 294 } 295 assertTrue(replyFound); 296 } 297 298 @Test 299 public void testAdjustNotification_importanceKey_notAllowed() throws Exception { 300 if (mActivityManager.isLowRamDevice()) { 301 return; 302 } 303 setUpListeners(); 304 305 mUi.adoptShellPermissionIdentity("android.permission.STATUS_BAR_SERVICE"); 306 mNotificationManager.disallowAssistantAdjustment(Adjustment.KEY_IMPORTANCE); 307 mUi.dropShellPermissionIdentity(); 308 309 sendNotification(1, ICON_ID); 310 StatusBarNotification sbn = getFirstNotificationFromPackage( 311 TestNotificationListener.PKG); 312 NotificationListenerService.Ranking out = new NotificationListenerService.Ranking(); 313 mNotificationListenerService.mRankingMap.getRanking(sbn.getKey(), out); 314 315 int currentImportance = out.getImportance(); 316 int newImportance = currentImportance == NotificationManager.IMPORTANCE_DEFAULT 317 ? NotificationManager.IMPORTANCE_HIGH : NotificationManager.IMPORTANCE_DEFAULT; 318 319 Bundle signals = new Bundle(); 320 signals.putInt(Adjustment.KEY_IMPORTANCE, newImportance); 321 Adjustment adjustment = new Adjustment(sbn.getPackageName(), sbn.getKey(), signals, "", 322 sbn.getUser()); 323 324 mNotificationAssistantService.adjustNotification(adjustment); 325 Thread.sleep(SLEEP_TIME); // wait for adjustment to be processed 326 327 mNotificationListenerService.mRankingMap.getRanking(sbn.getKey(), out); 328 329 assertEquals(currentImportance, out.getImportance()); 330 331 } 332 333 @Test 334 public void testGetAllowedAssistantCapabilities_permission() throws Exception { 335 if (mActivityManager.isLowRamDevice()) { 336 return; 337 } 338 toggleAssistantAccess(false); 339 340 try { 341 mNotificationManager.getAllowedAssistantAdjustments(); 342 fail(" Non assistants cannot call this method"); 343 } catch (SecurityException e) { 344 //pass 345 } 346 } 347 348 @Test 349 public void testGetAllowedAssistantCapabilities() throws Exception { 350 if (mActivityManager.isLowRamDevice()) { 351 return; 352 } 353 toggleAssistantAccess(true); 354 Thread.sleep(SLEEP_TIME); // wait for assistant to be allowed 355 mNotificationAssistantService = TestNotificationAssistant.getInstance(); 356 mNotificationAssistantService.onAllowedAdjustmentsChanged(); 357 assertNotNull(mNotificationAssistantService.currentCapabilities); 358 359 mUi.adoptShellPermissionIdentity("android.permission.STATUS_BAR_SERVICE"); 360 mNotificationManager.allowAssistantAdjustment(Adjustment.KEY_SNOOZE_CRITERIA); 361 362 Thread.sleep(SLEEP_TIME); 363 assertTrue(mNotificationAssistantService.currentCapabilities.contains( 364 Adjustment.KEY_SNOOZE_CRITERIA)); 365 366 mNotificationManager.disallowAssistantAdjustment(Adjustment.KEY_SNOOZE_CRITERIA); 367 Thread.sleep(SLEEP_TIME); 368 assertFalse(mNotificationAssistantService.currentCapabilities.contains( 369 Adjustment.KEY_SNOOZE_CRITERIA)); 370 371 // just in case KEY_SNOOZE_CRITERIA was included in the original set, test adding again 372 mNotificationManager.allowAssistantAdjustment(Adjustment.KEY_SNOOZE_CRITERIA); 373 Thread.sleep(SLEEP_TIME); 374 assertTrue(mNotificationAssistantService.currentCapabilities.contains( 375 Adjustment.KEY_SNOOZE_CRITERIA)); 376 377 mUi.dropShellPermissionIdentity(); 378 } 379 380 @Test 381 public void testOnActionInvoked_methodExists() throws Exception { 382 if (mActivityManager.isLowRamDevice()) { 383 return; 384 } 385 setUpListeners(); 386 final Intent intent = new Intent(Intent.ACTION_MAIN, Telephony.Threads.CONTENT_URI); 387 388 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP 389 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 390 intent.setAction(Intent.ACTION_MAIN); 391 392 final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 393 Notification.Action action = new Notification.Action.Builder(null, "", 394 pendingIntent).build(); 395 // This method has to exist and the call cannot fail 396 mNotificationAssistantService.onActionInvoked("", action, 397 NotificationAssistantService.SOURCE_FROM_APP); 398 } 399 400 @Test 401 public void testOnNotificationDirectReplied_methodExists() throws Exception { 402 if (mActivityManager.isLowRamDevice()) { 403 return; 404 } 405 setUpListeners(); 406 // This method has to exist and the call cannot fail 407 mNotificationAssistantService.onNotificationDirectReplied(""); 408 } 409 410 @Test 411 public void testOnNotificationExpansionChanged_methodExists() throws Exception { 412 if (mActivityManager.isLowRamDevice()) { 413 return; 414 } 415 setUpListeners(); 416 // This method has to exist and the call cannot fail 417 mNotificationAssistantService.onNotificationExpansionChanged("", true, true); 418 } 419 420 @Test 421 public void testOnNotificationsSeen_methodExists() throws Exception { 422 if (mActivityManager.isLowRamDevice()) { 423 return; 424 } 425 setUpListeners(); 426 // This method has to exist and the call cannot fail 427 mNotificationAssistantService.onNotificationsSeen(new ArrayList<>()); 428 } 429 430 @Test 431 public void testOnSuggestedReplySent_methodExists() throws Exception { 432 if (mActivityManager.isLowRamDevice()) { 433 return; 434 } 435 setUpListeners(); 436 // This method has to exist and the call cannot fail 437 mNotificationAssistantService.onSuggestedReplySent("", "", 438 NotificationAssistantService.SOURCE_FROM_APP); 439 } 440 441 private StatusBarNotification getFirstNotificationFromPackage(String PKG) 442 throws InterruptedException { 443 StatusBarNotification sbn = mNotificationListenerService.mPosted.poll(SLEEP_TIME, 444 TimeUnit.MILLISECONDS); 445 assertNotNull(sbn); 446 while (!sbn.getPackageName().equals(PKG)) { 447 sbn = mNotificationListenerService.mPosted.poll(SLEEP_TIME, TimeUnit.MILLISECONDS); 448 } 449 assertNotNull(sbn); 450 return sbn; 451 } 452 453 private void setUpListeners() throws Exception { 454 toggleListenerAccess(true); 455 toggleAssistantAccess(true); 456 Thread.sleep(2 * SLEEP_TIME); // wait for listener and assistant to be allowed 457 458 mNotificationListenerService = TestNotificationListener.getInstance(); 459 mNotificationAssistantService = TestNotificationAssistant.getInstance(); 460 461 assertNotNull(mNotificationListenerService); 462 assertNotNull(mNotificationAssistantService); 463 } 464 465 private void sendNotification(final int id, final int icon) throws Exception { 466 sendNotification(id, null, icon); 467 } 468 469 private void sendNotification(final int id, String groupKey, final int icon) throws Exception { 470 final Intent intent = new Intent(Intent.ACTION_MAIN, Telephony.Threads.CONTENT_URI); 471 472 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP 473 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 474 intent.setAction(Intent.ACTION_MAIN); 475 476 final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 477 final Notification notification = 478 new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID) 479 .setSmallIcon(icon) 480 .setWhen(System.currentTimeMillis()) 481 .setContentTitle("notify#" + id) 482 .setContentText("This is #" + id + "notification ") 483 .setContentIntent(pendingIntent) 484 .setGroup(groupKey) 485 .build(); 486 mNotificationManager.notify(id, notification); 487 } 488 489 private void toggleListenerAccess(boolean on) throws IOException { 490 491 Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 492 String componentName = TestNotificationListener.getId(); 493 494 String command = " cmd notification " + (on ? "allow_listener " : "disallow_listener ") 495 + componentName; 496 497 runCommand(command, instrumentation); 498 499 final ComponentName listenerComponent = TestNotificationListener.getComponentName(); 500 final NotificationManager nm = mContext.getSystemService(NotificationManager.class); 501 Assert.assertTrue(listenerComponent + " has not been " + (on ? "allowed" : "disallowed"), 502 nm.isNotificationListenerAccessGranted(listenerComponent) == on); 503 } 504 505 private void toggleAssistantAccess(boolean on) { 506 final ComponentName assistantComponent = TestNotificationAssistant.getComponentName(); 507 508 mUi.adoptShellPermissionIdentity("android.permission.STATUS_BAR_SERVICE", 509 "android.permission.REQUEST_NOTIFICATION_ASSISTANT_SERVICE"); 510 mNotificationManager.setNotificationAssistantAccessGranted(assistantComponent, on); 511 512 assertTrue(assistantComponent + " has not been " + (on ? "allowed" : "disallowed"), 513 mNotificationManager.isNotificationAssistantAccessGranted(assistantComponent) 514 == on); 515 if (on) { 516 assertEquals(assistantComponent, 517 mNotificationManager.getAllowedNotificationAssistant()); 518 } else { 519 assertNotEquals(assistantComponent, 520 mNotificationManager.getAllowedNotificationAssistant()); 521 } 522 523 mUi.dropShellPermissionIdentity(); 524 } 525 526 private void runCommand(String command, Instrumentation instrumentation) throws IOException { 527 UiAutomation uiAutomation = instrumentation.getUiAutomation(); 528 // Execute command 529 try (ParcelFileDescriptor fd = uiAutomation.executeShellCommand(command)) { 530 assertNotNull("Failed to execute shell command: " + command, fd); 531 // Wait for the command to finish by reading until EOF 532 try (InputStream in = new FileInputStream(fd.getFileDescriptor())) { 533 byte[] buffer = new byte[4096]; 534 while (in.read(buffer) > 0) { 535 } 536 } catch (IOException e) { 537 throw new IOException("Could not read stdout of command:" + command, e); 538 } 539 } 540 } 541 }