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(); 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, 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, 317 String groupKey, final int icon, 318 boolean isCall, Uri phoneNumber) { 319 final Intent intent = new Intent(Intent.ACTION_MAIN, Telephony.Threads.CONTENT_URI); 320 321 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP 322 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 323 intent.setAction(Intent.ACTION_MAIN); 324 intent.setPackage(mContext.getPackageName()); 325 326 final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 327 PendingIntent.FLAG_MUTABLE); 328 Notification.Builder nb = new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID) 329 .setSmallIcon(icon) 330 .setWhen(System.currentTimeMillis()) 331 .setContentTitle("notify#" + id) 332 .setContentText("This is #" + id + "notification ") 333 .setContentIntent(pendingIntent) 334 .setGroup(groupKey); 335 336 if (isCall) { 337 nb.setCategory(CATEGORY_CALL); 338 if (phoneNumber != null) { 339 Bundle extras = new Bundle(); 340 ArrayList<Person> pList = new ArrayList<>(); 341 pList.add(new Person.Builder().setUri(phoneNumber.toString()).build()); 342 extras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, pList); 343 nb.setExtras(extras); 344 } 345 } 346 347 final Notification notification = nb.build(); 348 mNotificationManager.notify(id, notification); 349 350 assertNotNull(mNotificationHelper.findPostedNotification(null, id, SEARCH_TYPE.APP)); 351 } 352 setEnableServiceNotificationRateLimit(boolean enable)353 protected void setEnableServiceNotificationRateLimit(boolean enable) throws IOException { 354 String command = "cmd activity fgs-notification-rate-limit " 355 + (enable ? "enable" : "disable"); 356 357 mNotificationHelper.runCommand(command, InstrumentationRegistry.getInstrumentation()); 358 } 359 suspendPackage(String packageName, Instrumentation instrumentation, boolean suspend)360 protected void suspendPackage(String packageName, 361 Instrumentation instrumentation, boolean suspend) throws IOException { 362 int userId = mContext.getUserId(); 363 String command = " cmd package " + (suspend ? "suspend " : "unsuspend ") 364 + "--user " + userId + " " + packageName; 365 366 mNotificationHelper.runCommand(command, instrumentation); 367 AmUtils.waitForBroadcastBarrier(); 368 } 369 toggleNotificationPolicyAccess(String packageName, Instrumentation instrumentation, boolean on)370 protected void toggleNotificationPolicyAccess(String packageName, 371 Instrumentation instrumentation, boolean on) throws IOException { 372 373 String command = " cmd notification " + (on ? "allow_dnd " : "disallow_dnd ") + packageName; 374 375 mNotificationHelper.runCommand(command, instrumentation); 376 AmUtils.waitForBroadcastBarrier(); 377 378 NotificationManager nm = mContext.getSystemService(NotificationManager.class); 379 assertEquals("Notification Policy Access Grant is " 380 + nm.isNotificationPolicyAccessGranted() + " not " + on + " for " 381 + packageName, on, nm.isNotificationPolicyAccessGranted()); 382 } 383 } 384