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.notification.current.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 junit.framework.TestCase.assertTrue; 24 25 import static org.junit.Assert.assertEquals; 26 import static org.junit.Assert.assertNotNull; 27 28 import android.Manifest; 29 import android.app.ActivityManager; 30 import android.app.Instrumentation; 31 import android.app.Notification; 32 import android.app.Notification.CallStyle; 33 import android.app.NotificationChannel; 34 import android.app.NotificationChannelGroup; 35 import android.app.NotificationManager; 36 import android.app.PendingIntent; 37 import android.app.Person; 38 import android.app.role.RoleManager; 39 import android.app.stubs.BubbledActivity; 40 import android.app.stubs.R; 41 import android.app.stubs.shared.NotificationHelper; 42 import android.app.stubs.shared.NotificationHelper.SEARCH_TYPE; 43 import android.app.stubs.shared.TestNotificationAssistant; 44 import android.app.stubs.shared.TestNotificationListener; 45 import android.content.ComponentName; 46 import android.content.Context; 47 import android.content.Intent; 48 import android.content.pm.PackageManager; 49 import android.content.pm.ShortcutInfo; 50 import android.content.pm.ShortcutManager; 51 import android.graphics.drawable.Icon; 52 import android.media.AudioManager; 53 import android.net.Uri; 54 import android.os.Bundle; 55 import android.os.SystemClock; 56 import android.platform.test.flag.junit.CheckFlagsRule; 57 import android.platform.test.flag.junit.DeviceFlagsValueProvider; 58 import android.provider.Telephony; 59 import android.util.ArraySet; 60 import android.util.Log; 61 62 import androidx.annotation.NonNull; 63 import androidx.test.platform.app.InstrumentationRegistry; 64 65 import com.android.compatibility.common.util.AmUtils; 66 import com.android.compatibility.common.util.SystemUtil; 67 import com.android.compatibility.common.util.ThrowingRunnable; 68 69 import org.junit.After; 70 import org.junit.Before; 71 import org.junit.Rule; 72 73 import java.io.IOException; 74 import java.util.ArrayList; 75 import java.util.Arrays; 76 import java.util.Collections; 77 import java.util.List; 78 import java.util.Set; 79 import java.util.concurrent.Callable; 80 81 /* Base class for NotificationManager tests. Handles some of the common set up logic for tests. */ 82 public abstract class BaseNotificationManagerTest { 83 84 static final String STUB_PACKAGE_NAME = "android.app.stubs"; 85 protected static final String NOTIFICATION_CHANNEL_ID = "NotificationManagerTest"; 86 protected static final NotificationChannel NOTIFICATION_CHANNEL = new NotificationChannel( 87 NOTIFICATION_CHANNEL_ID, "name", IMPORTANCE_DEFAULT); 88 protected static final String SHARE_SHORTCUT_CATEGORY = 89 "android.app.stubs.SHARE_SHORTCUT_CATEGORY"; 90 protected static final String SHARE_SHORTCUT_ID = "shareShortcut"; 91 // Constants for GetResultActivity and return codes from MatchesCallFilterTestActivity 92 // the permitted/not permitted values need to stay the same as in the test activity. 93 protected static final int REQUEST_CODE = 42; 94 protected static final String TEST_APP = "com.android.test.notificationapp"; 95 96 private static final String TAG = BaseNotificationManagerTest.class.getSimpleName(); 97 98 @Rule 99 public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); 100 101 protected Context mContext; 102 protected PackageManager mPackageManager; 103 protected AudioManager mAudioManager; 104 protected RoleManager mRoleManager; 105 protected NotificationManager mNotificationManager; 106 protected ActivityManager mActivityManager; 107 protected TestNotificationAssistant mAssistant; 108 protected TestNotificationListener mListener; 109 protected Instrumentation mInstrumentation; 110 protected NotificationHelper mNotificationHelper; 111 protected String mPreviousEnabledAssistant; 112 113 @Before baseSetUp()114 public void baseSetUp() throws Exception { 115 mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 116 mNotificationManager = mContext.getSystemService(NotificationManager.class); 117 mNotificationHelper = new NotificationHelper(mContext); 118 // clear the deck so that our getActiveNotifications results are predictable 119 mNotificationManager.cancelAll(); 120 121 assertEquals("Previous test left system in a bad state ", 122 0, mNotificationManager.getActiveNotifications().length); 123 124 mNotificationManager.createNotificationChannel(NOTIFICATION_CHANNEL); 125 mActivityManager = mContext.getSystemService(ActivityManager.class); 126 mPackageManager = mContext.getPackageManager(); 127 mAudioManager = mContext.getSystemService(AudioManager.class); 128 mRoleManager = mContext.getSystemService(RoleManager.class); 129 130 mPreviousEnabledAssistant = mNotificationHelper.getEnabledAssistant(); 131 // ensure listener access isn't allowed before test runs (other tests could put 132 // TestListener in an unexpected state) 133 mNotificationHelper.disableListener(STUB_PACKAGE_NAME); 134 mNotificationHelper.disableAssistant(STUB_PACKAGE_NAME); 135 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 136 toggleNotificationPolicyAccess(mContext.getPackageName(), mInstrumentation, true); 137 runAsSystemUi(() -> mNotificationManager.setInterruptionFilter(INTERRUPTION_FILTER_ALL)); 138 toggleNotificationPolicyAccess(mContext.getPackageName(), mInstrumentation, false); 139 140 // Ensure that the tests are exempt from global service-related rate limits 141 setEnableServiceNotificationRateLimit(false); 142 } 143 144 @After baseTearDown()145 public void baseTearDown() throws Exception { 146 setEnableServiceNotificationRateLimit(true); 147 148 mNotificationManager.cancelAll(); 149 150 assertExpectedDndState(INTERRUPTION_FILTER_ALL); 151 152 List<NotificationChannel> channels = mNotificationManager.getNotificationChannels(); 153 // Delete all channels. 154 for (NotificationChannel nc : channels) { 155 if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(nc.getId())) { 156 continue; 157 } 158 mNotificationManager.deleteNotificationChannel(nc.getId()); 159 } 160 161 // Unsuspend package if it was suspended in the test 162 suspendPackage(mContext.getPackageName(), mInstrumentation, false); 163 164 mNotificationHelper.disableListener(STUB_PACKAGE_NAME); 165 mNotificationHelper.disableAssistant(STUB_PACKAGE_NAME); 166 mNotificationHelper.enableOtherPkgAssistantIfNeeded(mPreviousEnabledAssistant); 167 toggleNotificationPolicyAccess(mContext.getPackageName(), mInstrumentation, false); 168 169 List<NotificationChannelGroup> groups = mNotificationManager.getNotificationChannelGroups(); 170 // Delete all groups. 171 for (NotificationChannelGroup ncg : groups) { 172 mNotificationManager.deleteNotificationChannelGroup(ncg.getId()); 173 } 174 } 175 176 /** 177 * Runs a {@link ThrowingRunnable} as the Shell, while adopting SystemUI's permission (as 178 * checked by {@code NotificationManagerService#isCallerSystemOrSystemUi}). 179 */ runAsSystemUi(@onNull ThrowingRunnable runnable)180 protected static void runAsSystemUi(@NonNull ThrowingRunnable runnable) { 181 SystemUtil.runWithShellPermissionIdentity(runnable, Manifest.permission.STATUS_BAR_SERVICE); 182 } 183 184 /** 185 * Calls a {@link Callable} as the Shell, while adopting SystemUI's permission (as checked by 186 * {@code NotificationManagerService#isCallerSystemOrSystemUi}). 187 */ callAsSystemUi(@onNull Callable<T> callable)188 protected static <T> T callAsSystemUi(@NonNull Callable<T> callable) { 189 try { 190 return SystemUtil.callWithShellPermissionIdentity(callable, 191 Manifest.permission.STATUS_BAR_SERVICE); 192 } catch (Exception e) { 193 throw new RuntimeException(e); 194 } 195 } 196 197 @SuppressWarnings("InlineMeInliner") setUpNotifListener()198 protected void setUpNotifListener() { 199 try { 200 mListener = mNotificationHelper.enableListener(STUB_PACKAGE_NAME); 201 assertNotNull(mListener); 202 mListener.resetData(); 203 } catch (Exception e) { 204 Log.e(TAG, "error in setUpNotifListener", e); 205 } 206 } 207 toggleExternalListenerAccess(ComponentName listenerComponent, boolean on)208 protected void toggleExternalListenerAccess(ComponentName listenerComponent, boolean on) 209 throws IOException { 210 String command = " cmd notification " + (on ? "allow_listener " : "disallow_listener ") 211 + listenerComponent.flattenToString() + " " + mContext.getUserId(); 212 mNotificationHelper.runCommand(command, InstrumentationRegistry.getInstrumentation()); 213 } 214 assertExpectedDndState(int expectedState)215 protected void assertExpectedDndState(int expectedState) throws Exception { 216 int tries = 3; 217 for (int i = tries; i >= 0; i--) { 218 if (expectedState 219 == mNotificationManager.getCurrentInterruptionFilter()) { 220 break; 221 } 222 Thread.sleep(100); 223 } 224 225 assertEquals(expectedState, mNotificationManager.getCurrentInterruptionFilter()); 226 } 227 228 /** Creates a dynamic, longlived, sharing shortcut. Call {@link #deleteShortcuts()} after. */ createDynamicShortcut()229 protected void createDynamicShortcut() { 230 Person person = new Person.Builder() 231 .setBot(false) 232 .setIcon(Icon.createWithResource(mContext, R.drawable.icon_black)) 233 .setName("BubbleBot") 234 .setImportant(true) 235 .build(); 236 237 Set<String> categorySet = new ArraySet<>(); 238 categorySet.add(SHARE_SHORTCUT_CATEGORY); 239 Intent shortcutIntent = new Intent(mContext, BubbledActivity.class); 240 shortcutIntent.setAction(Intent.ACTION_VIEW); 241 242 ShortcutInfo shortcut = new ShortcutInfo.Builder(mContext, SHARE_SHORTCUT_ID) 243 .setShortLabel(SHARE_SHORTCUT_ID) 244 .setIcon(Icon.createWithResource(mContext, R.drawable.icon_black)) 245 .setIntent(shortcutIntent) 246 .setPerson(person) 247 .setCategories(categorySet) 248 .setLongLived(true) 249 .build(); 250 251 ShortcutManager scManager = mContext.getSystemService(ShortcutManager.class); 252 scManager.addDynamicShortcuts(Arrays.asList(shortcut)); 253 } 254 deleteShortcuts()255 protected void deleteShortcuts() { 256 ShortcutManager scManager = mContext.getSystemService(ShortcutManager.class); 257 scManager.removeAllDynamicShortcuts(); 258 scManager.removeLongLivedShortcuts(Collections.singletonList(SHARE_SHORTCUT_ID)); 259 } 260 261 /** 262 * Notification fulfilling conversation policy; for the shortcut to be valid 263 * call {@link #createDynamicShortcut()} 264 */ getConversationNotification()265 protected Notification.Builder getConversationNotification() { 266 Person person = new Person.Builder() 267 .setName("bubblebot") 268 .build(); 269 return new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID) 270 .setContentTitle("foo") 271 .setShortcutId(SHARE_SHORTCUT_ID) 272 .setStyle(new Notification.MessagingStyle(person) 273 .setConversationTitle("Bubble Chat") 274 .addMessage("Hello?", 275 SystemClock.currentThreadTimeMillis() - 300000, person) 276 .addMessage("Is it me you're looking for?", 277 SystemClock.currentThreadTimeMillis(), person) 278 ) 279 .setSmallIcon(android.R.drawable.sym_def_app_icon); 280 } 281 getCallStyleNotification(final int id)282 protected Notification.Builder getCallStyleNotification(final int id) { 283 Person person = new Person.Builder().setName("Test name").build(); 284 PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, 285 new Intent().setPackage(mContext.getPackageName()), PendingIntent.FLAG_MUTABLE); 286 CallStyle cs = CallStyle.forIncomingCall(person, pendingIntent, pendingIntent); 287 288 return new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID) 289 .setSmallIcon(R.drawable.black) 290 .setContentTitle("notify#" + id) 291 .setContentText("This is #" + id + "notification ") 292 .setStyle(cs); 293 } 294 cancelAndPoll(int id)295 protected void cancelAndPoll(int id) { 296 mNotificationManager.cancel(id); 297 298 try { 299 Thread.sleep(500); 300 } catch (InterruptedException ex) { 301 // pass 302 } 303 assertTrue(mNotificationHelper.isNotificationGone(id, SEARCH_TYPE.APP)); 304 } 305 sendNotification(final int id, final int icon)306 protected void sendNotification(final int id, 307 final int icon) throws Exception { 308 sendNotification(id, null, icon); 309 } 310 sendNotification(final int id, String groupKey, final int icon)311 protected void sendNotification(final int id, 312 String groupKey, final int icon) { 313 sendNotification(id, groupKey, false, icon, false, null); 314 } 315 sendNotification(final int id, String groupKey, final int icon, boolean isCall, Uri phoneNumber)316 protected void sendNotification(final int id, String groupKey, final int icon, boolean isCall, 317 Uri phoneNumber) { 318 sendNotification(id, groupKey, false, icon, isCall, phoneNumber); 319 } 320 sendNotification(final int id, String groupKey, boolean isSummary, final int icon, boolean isCall, Uri phoneNumber)321 protected void sendNotification(final int id, 322 String groupKey, boolean isSummary, final int icon, 323 boolean isCall, Uri phoneNumber) { 324 final Intent intent = new Intent(Intent.ACTION_MAIN, Telephony.Threads.CONTENT_URI); 325 326 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP 327 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 328 intent.setAction(Intent.ACTION_MAIN); 329 intent.setPackage(mContext.getPackageName()); 330 331 final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 332 PendingIntent.FLAG_MUTABLE); 333 Notification.Builder nb = new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID) 334 .setSmallIcon(icon) 335 .setWhen(System.currentTimeMillis()) 336 .setContentTitle("notify#" + id) 337 .setContentText("This is #" + id + "notification ") 338 .setContentIntent(pendingIntent) 339 .setGroup(groupKey) 340 .setGroupSummary(isSummary); 341 342 if (isCall) { 343 nb.setCategory(CATEGORY_CALL); 344 if (phoneNumber != null) { 345 Bundle extras = new Bundle(); 346 ArrayList<Person> pList = new ArrayList<>(); 347 pList.add(new Person.Builder().setUri(phoneNumber.toString()).build()); 348 extras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, pList); 349 nb.setExtras(extras); 350 } 351 } 352 353 final Notification notification = nb.build(); 354 mNotificationManager.notify(id, notification); 355 356 assertNotNull(mNotificationHelper.findPostedNotification(null, id, SEARCH_TYPE.APP)); 357 } 358 setEnableServiceNotificationRateLimit(boolean enable)359 protected void setEnableServiceNotificationRateLimit(boolean enable) throws IOException { 360 String command = "cmd activity fgs-notification-rate-limit " 361 + (enable ? "enable" : "disable"); 362 363 mNotificationHelper.runCommand(command, InstrumentationRegistry.getInstrumentation()); 364 } 365 suspendPackage(String packageName, Instrumentation instrumentation, boolean suspend)366 protected void suspendPackage(String packageName, 367 Instrumentation instrumentation, boolean suspend) throws IOException { 368 int userId = mContext.getUserId(); 369 String command = " cmd package " + (suspend ? "suspend " : "unsuspend ") 370 + "--user " + userId + " " + packageName; 371 372 mNotificationHelper.runCommand(command, instrumentation); 373 AmUtils.waitForBroadcastBarrier(); 374 } 375 toggleNotificationPolicyAccess(String packageName, Instrumentation instrumentation, boolean on)376 protected void toggleNotificationPolicyAccess(String packageName, 377 Instrumentation instrumentation, boolean on) throws IOException { 378 379 String command = " cmd notification " + (on ? "allow_dnd " : "disallow_dnd ") + packageName 380 + " " + mContext.getUserId(); 381 382 mNotificationHelper.runCommand(command, instrumentation); 383 AmUtils.waitForBroadcastBarrier(); 384 385 NotificationManager nm = mContext.getSystemService(NotificationManager.class); 386 assertEquals("Notification Policy Access Grant is " 387 + nm.isNotificationPolicyAccessGranted() + " not " + on + " for " 388 + packageName, on, nm.isNotificationPolicyAccessGranted()); 389 } 390 } 391