1 /* 2 * Copyright (C) 2022 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.cts; 18 19 import static android.app.Notification.CATEGORY_CALL; 20 import static android.app.NotificationManager.IMPORTANCE_DEFAULT; 21 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; 22 23 import static org.junit.Assert.assertNotEquals; 24 25 import android.app.ActivityManager; 26 import android.app.Instrumentation; 27 import android.app.Notification; 28 import android.app.NotificationChannel; 29 import android.app.NotificationChannelGroup; 30 import android.app.NotificationManager; 31 import android.app.PendingIntent; 32 import android.app.Person; 33 import android.app.UiAutomation; 34 import android.app.cts.android.app.cts.tools.NotificationHelper; 35 import android.app.role.RoleManager; 36 import android.app.stubs.BubbledActivity; 37 import android.app.stubs.R; 38 import android.app.stubs.TestNotificationAssistant; 39 import android.app.stubs.TestNotificationListener; 40 import android.content.ComponentName; 41 import android.content.Context; 42 import android.content.Intent; 43 import android.content.pm.PackageManager; 44 import android.content.pm.ShortcutInfo; 45 import android.content.pm.ShortcutManager; 46 import android.graphics.drawable.Icon; 47 import android.media.AudioManager; 48 import android.net.Uri; 49 import android.os.Bundle; 50 import android.os.ParcelFileDescriptor; 51 import android.os.SystemClock; 52 import android.provider.Telephony; 53 import android.service.notification.StatusBarNotification; 54 import android.test.AndroidTestCase; 55 import android.util.ArraySet; 56 import android.util.Log; 57 58 import androidx.test.platform.app.InstrumentationRegistry; 59 60 import java.io.FileInputStream; 61 import java.io.IOException; 62 import java.io.InputStream; 63 import java.util.ArrayList; 64 import java.util.Arrays; 65 import java.util.Collections; 66 import java.util.List; 67 import java.util.Set; 68 69 /* Base class for NotificationManager tests. Handles some of the common set up logic for tests. */ 70 public abstract class BaseNotificationManagerTest extends AndroidTestCase { 71 72 protected static final String NOTIFICATION_CHANNEL_ID = "NotificationManagerTest"; 73 protected static final String SHARE_SHORTCUT_CATEGORY = 74 "android.app.stubs.SHARE_SHORTCUT_CATEGORY"; 75 protected static final String SHARE_SHORTCUT_ID = "shareShortcut"; 76 77 private static final String TAG = BaseNotificationManagerTest.class.getSimpleName(); 78 79 protected PackageManager mPackageManager; 80 protected AudioManager mAudioManager; 81 protected RoleManager mRoleManager; 82 protected NotificationManager mNotificationManager; 83 protected ActivityManager mActivityManager; 84 protected TestNotificationAssistant mAssistant; 85 protected TestNotificationListener mListener; 86 protected List<String> mRuleIds; 87 protected Instrumentation mInstrumentation; 88 protected NotificationHelper mNotificationHelper; 89 toggleListenerAccess(Context context, boolean on)90 public static void toggleListenerAccess(Context context, boolean on) throws IOException { 91 String command = " cmd notification " + (on ? "allow_listener " : "disallow_listener ") 92 + TestNotificationListener.getId(); 93 94 runCommand(command, InstrumentationRegistry.getInstrumentation()); 95 96 final NotificationManager nm = context.getSystemService(NotificationManager.class); 97 final ComponentName listenerComponent = TestNotificationListener.getComponentName(); 98 assertEquals(listenerComponent + " has incorrect listener access", 99 on, nm.isNotificationListenerAccessGranted(listenerComponent)); 100 } 101 102 @SuppressWarnings("StatementWithEmptyBody") runCommand(String command, Instrumentation instrumentation)103 protected static void runCommand(String command, Instrumentation instrumentation) 104 throws IOException { 105 UiAutomation uiAutomation = instrumentation.getUiAutomation(); 106 // Execute command 107 try (ParcelFileDescriptor fd = uiAutomation.executeShellCommand(command)) { 108 assertNotNull("Failed to execute shell command: " + command, fd); 109 // Wait for the command to finish by reading until EOF 110 try (InputStream in = new FileInputStream(fd.getFileDescriptor())) { 111 byte[] buffer = new byte[4096]; 112 while (in.read(buffer) > 0) { 113 // discard output 114 } 115 } catch (IOException e) { 116 throw new IOException("Could not read stdout of command: " + command, e); 117 } 118 } 119 } 120 121 @Override setUp()122 protected void setUp() throws Exception { 123 super.setUp(); 124 mNotificationManager = mContext.getSystemService(NotificationManager.class); 125 mNotificationHelper = new NotificationHelper(mContext, () -> mListener); 126 // clear the deck so that our getActiveNotifications results are predictable 127 mNotificationManager.cancelAll(); 128 129 assertEquals("Previous test left system in a bad state", 130 0, mNotificationManager.getActiveNotifications().length); 131 132 mNotificationManager.createNotificationChannel(new NotificationChannel( 133 NOTIFICATION_CHANNEL_ID, "name", IMPORTANCE_DEFAULT)); 134 mActivityManager = mContext.getSystemService(ActivityManager.class); 135 mPackageManager = mContext.getPackageManager(); 136 mAudioManager = mContext.getSystemService(AudioManager.class); 137 mRoleManager = mContext.getSystemService(RoleManager.class); 138 mRuleIds = new ArrayList<>(); 139 140 // ensure listener access isn't allowed before test runs (other tests could put 141 // TestListener in an unexpected state) 142 toggleListenerAccess(false); 143 toggleAssistantAccess(false); 144 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 145 toggleNotificationPolicyAccess(mContext.getPackageName(), mInstrumentation, true); 146 mNotificationManager.setInterruptionFilter(INTERRUPTION_FILTER_ALL); 147 toggleNotificationPolicyAccess(mContext.getPackageName(), mInstrumentation, false); 148 149 // Ensure that the tests are exempt from global service-related rate limits 150 setEnableServiceNotificationRateLimit(false); 151 } 152 153 @Override tearDown()154 protected void tearDown() throws Exception { 155 super.tearDown(); 156 157 setEnableServiceNotificationRateLimit(true); 158 159 mNotificationManager.cancelAll(); 160 for (String id : mRuleIds) { 161 mNotificationManager.removeAutomaticZenRule(id); 162 } 163 164 assertExpectedDndState(INTERRUPTION_FILTER_ALL); 165 166 List<NotificationChannel> channels = mNotificationManager.getNotificationChannels(); 167 // Delete all channels. 168 for (NotificationChannel nc : channels) { 169 if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(nc.getId())) { 170 continue; 171 } 172 mNotificationManager.deleteNotificationChannel(nc.getId()); 173 } 174 175 // Unsuspend package if it was suspended in the test 176 suspendPackage(mContext.getPackageName(), mInstrumentation, false); 177 178 toggleListenerAccess(false); 179 toggleNotificationPolicyAccess(mContext.getPackageName(), mInstrumentation, false); 180 181 List<NotificationChannelGroup> groups = mNotificationManager.getNotificationChannelGroups(); 182 // Delete all groups. 183 for (NotificationChannelGroup ncg : groups) { 184 mNotificationManager.deleteNotificationChannelGroup(ncg.getId()); 185 } 186 } 187 findPostedNotification(int id, boolean all)188 protected StatusBarNotification findPostedNotification(int id, boolean all) { 189 return mNotificationHelper.findPostedNotification(id, all); 190 } 191 setUpNotifListener()192 protected void setUpNotifListener() { 193 try { 194 toggleListenerAccess(true); 195 mListener = TestNotificationListener.getInstance(); 196 assertNotNull(mListener); 197 mListener.resetData(); 198 } catch (IOException e) { 199 } 200 } 201 checkNotificationExistence(int id, boolean shouldExist)202 protected boolean checkNotificationExistence(int id, boolean shouldExist) { 203 // notification is a bit asynchronous so it may take a few ms to appear in 204 // getActiveNotifications() 205 // we will check for it for up to 300ms before giving up 206 boolean found = false; 207 for (int tries = 3; tries-- > 0; ) { 208 // Need reset flag. 209 found = false; 210 final StatusBarNotification[] sbns = mNotificationManager.getActiveNotifications(); 211 for (StatusBarNotification sbn : sbns) { 212 Log.d(TAG, "Found " + sbn.getKey()); 213 if (sbn.getId() == id) { 214 found = true; 215 break; 216 } 217 } 218 if (found == shouldExist) break; 219 try { 220 Thread.sleep(100); 221 } catch (InterruptedException ex) { 222 // pass 223 } 224 } 225 return found == shouldExist; 226 } 227 toggleListenerAccess(boolean on)228 protected void toggleListenerAccess(boolean on) throws IOException { 229 toggleListenerAccess(mContext, on); 230 } 231 toggleAssistantAccess(boolean on)232 protected void toggleAssistantAccess(boolean on) throws IOException { 233 final ComponentName assistantComponent = TestNotificationAssistant.getComponentName(); 234 235 InstrumentationRegistry.getInstrumentation().getUiAutomation() 236 .adoptShellPermissionIdentity("android.permission.STATUS_BAR_SERVICE", 237 "android.permission.REQUEST_NOTIFICATION_ASSISTANT_SERVICE"); 238 mNotificationManager.setNotificationAssistantAccessGranted(assistantComponent, on); 239 240 assertTrue(assistantComponent + " has not been " + (on ? "allowed" : "disallowed"), 241 mNotificationManager.isNotificationAssistantAccessGranted(assistantComponent) 242 == on); 243 if (on) { 244 assertEquals(assistantComponent, 245 mNotificationManager.getAllowedNotificationAssistant()); 246 } else { 247 assertNotEquals(assistantComponent, 248 mNotificationManager.getAllowedNotificationAssistant()); 249 } 250 251 InstrumentationRegistry.getInstrumentation() 252 .getUiAutomation().dropShellPermissionIdentity(); 253 } 254 assertExpectedDndState(int expectedState)255 protected void assertExpectedDndState(int expectedState) { 256 int tries = 3; 257 for (int i = tries; i >= 0; i--) { 258 if (expectedState 259 == mNotificationManager.getCurrentInterruptionFilter()) { 260 break; 261 } 262 try { 263 Thread.sleep(100); 264 } catch (InterruptedException e) { 265 e.printStackTrace(); 266 } 267 } 268 269 assertEquals(expectedState, mNotificationManager.getCurrentInterruptionFilter()); 270 } 271 272 /** Creates a dynamic, longlived, sharing shortcut. Call {@link #deleteShortcuts()} after. */ createDynamicShortcut()273 protected void createDynamicShortcut() { 274 Person person = new Person.Builder() 275 .setBot(false) 276 .setIcon(Icon.createWithResource(mContext, R.drawable.icon_black)) 277 .setName("BubbleBot") 278 .setImportant(true) 279 .build(); 280 281 Set<String> categorySet = new ArraySet<>(); 282 categorySet.add(SHARE_SHORTCUT_CATEGORY); 283 Intent shortcutIntent = new Intent(mContext, BubbledActivity.class); 284 shortcutIntent.setAction(Intent.ACTION_VIEW); 285 286 ShortcutInfo shortcut = new ShortcutInfo.Builder(mContext, SHARE_SHORTCUT_ID) 287 .setShortLabel(SHARE_SHORTCUT_ID) 288 .setIcon(Icon.createWithResource(mContext, R.drawable.icon_black)) 289 .setIntent(shortcutIntent) 290 .setPerson(person) 291 .setCategories(categorySet) 292 .setLongLived(true) 293 .build(); 294 295 ShortcutManager scManager = mContext.getSystemService(ShortcutManager.class); 296 scManager.addDynamicShortcuts(Arrays.asList(shortcut)); 297 } 298 deleteShortcuts()299 protected void deleteShortcuts() { 300 ShortcutManager scManager = mContext.getSystemService(ShortcutManager.class); 301 scManager.removeAllDynamicShortcuts(); 302 scManager.removeLongLivedShortcuts(Collections.singletonList(SHARE_SHORTCUT_ID)); 303 } 304 305 /** 306 * Notification fulfilling conversation policy; for the shortcut to be valid 307 * call {@link #createDynamicShortcut()} 308 */ getConversationNotification()309 protected Notification.Builder getConversationNotification() { 310 Person person = new Person.Builder() 311 .setName("bubblebot") 312 .build(); 313 return new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID) 314 .setContentTitle("foo") 315 .setShortcutId(SHARE_SHORTCUT_ID) 316 .setStyle(new Notification.MessagingStyle(person) 317 .setConversationTitle("Bubble Chat") 318 .addMessage("Hello?", 319 SystemClock.currentThreadTimeMillis() - 300000, person) 320 .addMessage("Is it me you're looking for?", 321 SystemClock.currentThreadTimeMillis(), person) 322 ) 323 .setSmallIcon(android.R.drawable.sym_def_app_icon); 324 } 325 sendNotification(final int id, final int icon)326 protected void sendNotification(final int id, 327 final int icon) throws Exception { 328 sendNotification(id, null, icon); 329 } 330 sendNotification(final int id, String groupKey, final int icon)331 protected void sendNotification(final int id, 332 String groupKey, final int icon) { 333 sendNotification(id, groupKey, icon, false, null); 334 } 335 sendNotification(final int id, String groupKey, final int icon, boolean isCall, Uri phoneNumber)336 protected void sendNotification(final int id, 337 String groupKey, final int icon, 338 boolean isCall, Uri phoneNumber) { 339 final Intent intent = new Intent(Intent.ACTION_MAIN, Telephony.Threads.CONTENT_URI); 340 341 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP 342 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 343 intent.setAction(Intent.ACTION_MAIN); 344 345 final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 346 PendingIntent.FLAG_MUTABLE); 347 Notification.Builder nb = new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID) 348 .setSmallIcon(icon) 349 .setWhen(System.currentTimeMillis()) 350 .setContentTitle("notify#" + id) 351 .setContentText("This is #" + id + "notification ") 352 .setContentIntent(pendingIntent) 353 .setGroup(groupKey); 354 355 if (isCall) { 356 nb.setCategory(CATEGORY_CALL); 357 if (phoneNumber != null) { 358 Bundle extras = new Bundle(); 359 ArrayList<Person> pList = new ArrayList<>(); 360 pList.add(new Person.Builder().setUri(phoneNumber.toString()).build()); 361 extras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, pList); 362 nb.setExtras(extras); 363 } 364 } 365 366 final Notification notification = nb.build(); 367 mNotificationManager.notify(id, notification); 368 369 if (!checkNotificationExistence(id, /*shouldExist=*/ true)) { 370 fail("couldn't find posted notification id=" + id); 371 } 372 } 373 setEnableServiceNotificationRateLimit(boolean enable)374 protected void setEnableServiceNotificationRateLimit(boolean enable) throws IOException { 375 String command = "cmd activity fgs-notification-rate-limit " 376 + (enable ? "enable" : "disable"); 377 378 runCommand(command, InstrumentationRegistry.getInstrumentation()); 379 } 380 suspendPackage(String packageName, Instrumentation instrumentation, boolean suspend)381 protected void suspendPackage(String packageName, 382 Instrumentation instrumentation, boolean suspend) throws IOException { 383 int userId = mContext.getUserId(); 384 String command = " cmd package " + (suspend ? "suspend " : "unsuspend ") 385 + "--user " + userId + " " + packageName; 386 387 runCommand(command, instrumentation); 388 } 389 toggleNotificationPolicyAccess(String packageName, Instrumentation instrumentation, boolean on)390 protected void toggleNotificationPolicyAccess(String packageName, 391 Instrumentation instrumentation, boolean on) throws IOException { 392 393 String command = " cmd notification " + (on ? "allow_dnd " : "disallow_dnd ") + packageName; 394 395 runCommand(command, instrumentation); 396 397 NotificationManager nm = mContext.getSystemService(NotificationManager.class); 398 assertEquals("Notification Policy Access Grant is " 399 + nm.isNotificationPolicyAccessGranted() + " not " + on + " for " 400 + packageName, on, nm.isNotificationPolicyAccessGranted()); 401 } 402 } 403