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.Manifest.permission.POST_NOTIFICATIONS;
20 import static android.Manifest.permission.REVOKE_POST_NOTIFICATIONS_WITHOUT_KILL;
21 import static android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS;
22 import static android.app.Notification.FLAG_BUBBLE;
23 import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL;
24 import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE;
25 import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED;
26 import static android.app.stubs.BubbledActivity.EXTRA_LOCUS_ID;
27 import static android.app.stubs.BubblesTestService.EXTRA_TEST_CASE;
28 import static android.app.stubs.BubblesTestService.TEST_CALL;
29 import static android.app.stubs.BubblesTestService.TEST_MESSAGING;
30 import static android.app.stubs.SendBubbleActivity.BUBBLE_NOTIF_ID;
31 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
32 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
33 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
34 
35 import static org.junit.Assert.assertEquals;
36 import static org.junit.Assert.assertFalse;
37 import static org.junit.Assert.assertNotNull;
38 import static org.junit.Assert.assertNull;
39 import static org.junit.Assert.assertThrows;
40 import static org.junit.Assert.assertTrue;
41 
42 import android.app.Activity;
43 import android.app.ActivityOptions;
44 import android.app.Instrumentation;
45 import android.app.KeyguardManager;
46 import android.app.Notification;
47 import android.app.PendingIntent;
48 import android.app.stubs.BubbledActivity;
49 import android.app.stubs.BubblesTestService;
50 import android.app.stubs.R;
51 import android.app.stubs.SendBubbleActivity;
52 import android.app.stubs.shared.NotificationHelper.SEARCH_TYPE;
53 import android.content.BroadcastReceiver;
54 import android.content.Context;
55 import android.content.Intent;
56 import android.content.IntentFilter;
57 import android.content.LocusId;
58 import android.content.res.Resources;
59 import android.graphics.drawable.Icon;
60 import android.os.Bundle;
61 import android.permission.PermissionManager;
62 import android.permission.cts.PermissionUtils;
63 import android.provider.Settings;
64 import android.service.notification.NotificationListenerService;
65 import android.service.notification.StatusBarNotification;
66 
67 import androidx.test.platform.app.InstrumentationRegistry;
68 import androidx.test.runner.AndroidJUnit4;
69 
70 import com.android.compatibility.common.util.FeatureUtil;
71 import com.android.compatibility.common.util.SystemUtil;
72 
73 import org.junit.After;
74 import org.junit.Before;
75 import org.junit.Test;
76 import org.junit.runner.RunWith;
77 
78 import java.util.concurrent.CountDownLatch;
79 import java.util.concurrent.TimeUnit;
80 
81 /**
82  * Tests bubbles related logic in NotificationManager.
83  */
84 @RunWith(AndroidJUnit4.class)
85 public class NotificationManagerBubbleTest extends BaseNotificationManagerTest {
86 
87     private static final String TAG = NotificationManagerBubbleTest.class.getSimpleName();
88 
89     private static final String STUB_PACKAGE_NAME = "android.app.stubs";
90 
91     // use a value of 10000 for consistency with other CTS tests (see
92     // android.server.wm.intentLaunchRunner#ACTIVITY_LAUNCH_TIMEOUT)
93     private static final int ACTIVITY_LAUNCH_TIMEOUT = 10000;
94 
95     private BroadcastReceiver mBubbleBroadcastReceiver;
96     private boolean mBubblesEnabledSettingToRestore;
97 
98     @Before
setUp()99     public void setUp() throws Exception {
100         PermissionUtils.grantPermission(STUB_PACKAGE_NAME, POST_NOTIFICATIONS);
101         PermissionUtils.grantPermission(mContext.getPackageName(), POST_NOTIFICATIONS);
102 
103         // This setting is forced on / off for certain tests, save it & restore what's on the
104         // device after tests are run
105         mBubblesEnabledSettingToRestore = Settings.Secure.getInt(mContext.getContentResolver(),
106                 Settings.Secure.NOTIFICATION_BUBBLES) == 1;
107 
108         // ensure listener access isn't allowed before test runs (other tests could put
109         // TestListener in an unexpected state)
110         mNotificationHelper.disableListener(STUB_PACKAGE_NAME);
111 
112         // delay between tests so notifications aren't dropped by the rate limiter
113         sleep();
114     }
115 
116     @After
tearDown()117     public void tearDown() throws Exception {
118 
119         // Restore bubbles setting
120         setBubblesGlobal(mBubblesEnabledSettingToRestore);
121 
122         // Use test API to prevent PermissionManager from killing the test process when revoking
123         // permission.
124         SystemUtil.runWithShellPermissionIdentity(
125                 () -> mContext.getSystemService(PermissionManager.class)
126                         .revokePostNotificationPermissionWithoutKillForTest(
127                                 mContext.getPackageName(),
128                                 android.os.Process.myUserHandle().getIdentifier()),
129                 REVOKE_POST_NOTIFICATIONS_WITHOUT_KILL,
130                 REVOKE_RUNTIME_PERMISSIONS);
131         PermissionUtils.revokePermission(STUB_PACKAGE_NAME, POST_NOTIFICATIONS);
132     }
133 
isBubblesFeatureSupported()134     private boolean isBubblesFeatureSupported() {
135         // These do not support bubbles.
136         return (!mActivityManager.isLowRamDevice() && !FeatureUtil.isWatch()
137                 && !FeatureUtil.isAutomotive() && !FeatureUtil.isTV()
138                 && mContext.getResources().getBoolean(Resources.getSystem().getIdentifier(
139                         "config_supportsBubble", "bool", "android")));
140     }
141 
sleep()142     private void sleep() {
143         try {
144             Thread.sleep(500);
145         } catch (InterruptedException ignored) {
146         }
147     }
148 
sendAndVerifyBubble(final int id, Notification.Builder builder, boolean shouldBeBubble)149     private void sendAndVerifyBubble(final int id, Notification.Builder builder,
150             boolean shouldBeBubble) {
151         setUpNotifListener();
152 
153         Notification notif = builder.build();
154         mNotificationManager.notify(id, notif);
155 
156         verifyNotificationBubbleState(id, shouldBeBubble);
157     }
158 
getDefaultNotifBuilder(int id)159     private Notification.Builder getDefaultNotifBuilder(int id) {
160         return new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
161                 .setSmallIcon(R.drawable.black)
162                 .setWhen(System.currentTimeMillis())
163                 .setContentTitle("notify#" + id)
164                 .setContentText("This is #" + id + "notification  ")
165                 .setContentIntent(getDefaultNotifPendingIntent());
166     }
167 
getDefaultNotifPendingIntent()168     private PendingIntent getDefaultNotifPendingIntent() {
169         final Intent intent = new Intent(mContext, BubbledActivity.class);
170 
171         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP
172                 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
173         intent.setAction(Intent.ACTION_MAIN);
174         return PendingIntent.getActivity(mContext, 0, intent,
175                 PendingIntent.FLAG_MUTABLE_UNAUDITED);
176     }
177 
getDefaultBubbleMetadata()178     private Notification.BubbleMetadata getDefaultBubbleMetadata() {
179         return new Notification.BubbleMetadata.Builder(
180                 getDefaultNotifPendingIntent(),
181                 Icon.createWithResource(mContext, R.drawable.black))
182                 .build();
183     }
184 
185     /**
186      * Make sure {@link #setUpNotifListener()} is called prior to sending the notif and verifying
187      * in this method.
188      */
verifyNotificationBubbleState(int id, boolean shouldBeBubble)189     private void verifyNotificationBubbleState(int id, boolean shouldBeBubble) {
190         StatusBarNotification sbn =
191                 mNotificationHelper.findPostedNotification(null, id, SEARCH_TYPE.POSTED);
192         assertNotNull("Cannot find notification id=" + id, sbn);
193         assertEquals("Unexpected bubble state for id=" + id + ", expected " + shouldBeBubble,
194                 shouldBeBubble, ((sbn.getNotification().flags & FLAG_BUBBLE) != 0));
195     }
196 
setBubblesGlobal(boolean enabled)197     private void setBubblesGlobal(boolean enabled) {
198         SystemUtil.runWithShellPermissionIdentity(() ->
199                 Settings.Secure.putInt(mContext.getContentResolver(),
200                         Settings.Secure.NOTIFICATION_BUBBLES, enabled ? 1 : 0));
201     }
202 
setBubblesAppPref(int pref)203     private void setBubblesAppPref(int pref) throws Exception {
204         int userId = mContext.getUser().getIdentifier();
205         String pkg = mContext.getPackageName();
206         String command = " cmd notification set_bubbles " + pkg
207                 + " " + Integer.toString(pref)
208                 + " " + userId;
209         mNotificationHelper.runCommand(command, InstrumentationRegistry.getInstrumentation());
210     }
211 
setBubblesChannelAllowed(boolean allowed)212     private void setBubblesChannelAllowed(boolean allowed) throws Exception {
213         int userId = mContext.getUser().getIdentifier();
214         String pkg = mContext.getPackageName();
215         String command = " cmd notification set_bubbles_channel " + pkg
216                 + " " + NOTIFICATION_CHANNEL_ID
217                 + " " + allowed
218                 + " " + userId;
219         mNotificationHelper.runCommand(command, InstrumentationRegistry.getInstrumentation());
220     }
221 
allowAllNotificationsToBubble()222     private void allowAllNotificationsToBubble() throws Exception {
223         setBubblesGlobal(true);
224         setBubblesAppPref(1 /* all */);
225         setBubblesChannelAllowed(true);
226         sleep(); // wait for ranking update
227     }
228 
229     /**
230      * Starts an activity that is able to send a bubble; also handles unlocking the device.
231      * Any tests that use this method should be sure to call {@link #cleanupSendBubbleActivity()}
232      * to unregister the related broadcast receiver.
233      *
234      * @return the SendBubbleActivity that was opened.
235      */
startSendBubbleActivity()236     private SendBubbleActivity startSendBubbleActivity() throws Exception {
237         final CountDownLatch latch = new CountDownLatch(1);
238         mBubbleBroadcastReceiver = new BroadcastReceiver() {
239             @Override
240             public void onReceive(Context context, Intent intent) {
241                 latch.countDown();
242             }
243         };
244         IntentFilter filter = new IntentFilter(SendBubbleActivity.BUBBLE_ACTIVITY_OPENED);
245         mContext.registerReceiver(mBubbleBroadcastReceiver, filter, Context.RECEIVER_EXPORTED);
246 
247         // Start & get the activity
248         Class clazz = SendBubbleActivity.class;
249 
250         Instrumentation.ActivityResult result =
251                 new Instrumentation.ActivityResult(0, new Intent());
252         Instrumentation.ActivityMonitor monitor =
253                 new Instrumentation.ActivityMonitor(clazz.getName(), result, false);
254         InstrumentationRegistry.getInstrumentation().addMonitor(monitor);
255 
256         Intent i = new Intent(mContext, SendBubbleActivity.class);
257         i.setFlags(FLAG_ACTIVITY_NEW_TASK);
258         InstrumentationRegistry.getInstrumentation().startActivitySync(i);
259         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
260 
261         SendBubbleActivity sendBubbleActivity = (SendBubbleActivity) monitor.waitForActivity();
262 
263         // Make sure device is unlocked
264         ensureDeviceUnlocked(sendBubbleActivity);
265         latch.await(500, TimeUnit.MILLISECONDS);
266 
267         return sendBubbleActivity;
268     }
269 
startBubbledActivityMonitor()270     private Instrumentation.ActivityMonitor startBubbledActivityMonitor() {
271         Class<BubbledActivity> clazz = BubbledActivity.class;
272         Instrumentation.ActivityResult result =
273                 new Instrumentation.ActivityResult(0, new Intent());
274         Instrumentation.ActivityMonitor monitor =
275                 new Instrumentation.ActivityMonitor(clazz.getName(), result, false);
276         InstrumentationRegistry.getInstrumentation().addMonitor(monitor);
277         return monitor;
278     }
279 
startBubbleActivity(int id)280     private BubbledActivity startBubbleActivity(int id) {
281         return startBubbleActivity(id, true /* addLocusId */);
282     }
283 
284     /**
285      * Starts the same activity that is in the bubble produced by this activity.
286      */
startBubbleActivity(int id, boolean addLocusId)287     private BubbledActivity startBubbleActivity(int id, boolean addLocusId) {
288         Instrumentation.ActivityMonitor monitor = startBubbledActivityMonitor();
289 
290         final Intent intent = new Intent(mContext, BubbledActivity.class);
291         // Clear any previous instance of this activity
292         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
293         if (addLocusId) {
294             intent.putExtra(EXTRA_LOCUS_ID, String.valueOf(id));
295         }
296 
297         InstrumentationRegistry.getInstrumentation().startActivitySync(intent);
298         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
299 
300         BubbledActivity bubbledActivity = (BubbledActivity) monitor.waitForActivity();
301         ensureDeviceUnlocked(bubbledActivity);
302         return bubbledActivity;
303     }
304 
305     /**
306      * Make sure device is unlocked so the activity can become visible
307      */
ensureDeviceUnlocked(Activity activity)308     private void ensureDeviceUnlocked(Activity activity) {
309         // Make sure device is unlocked
310         KeyguardManager keyguardManager = mContext.getSystemService(KeyguardManager.class);
311         if (keyguardManager.isKeyguardLocked()) {
312             CountDownLatch latch = new CountDownLatch(1);
313             keyguardManager.requestDismissKeyguard(activity,
314                     new KeyguardManager.KeyguardDismissCallback() {
315                         @Override
316                         public void onDismissSucceeded() {
317                             latch.countDown();
318                         }
319                     });
320             try {
321                 latch.await(500, TimeUnit.MILLISECONDS);
322             } catch (InterruptedException ignored) {
323             }
324         }
325     }
326 
cleanupSendBubbleActivity()327     private void cleanupSendBubbleActivity() {
328         mContext.unregisterReceiver(mBubbleBroadcastReceiver);
329     }
330 
331     @Test
testCanBubble_ranking()332     public void testCanBubble_ranking() throws Exception {
333         if (!isBubblesFeatureSupported()) {
334             return;
335         }
336 
337         // turn on bubbles globally
338         setBubblesGlobal(true);
339         sleep();
340 
341         assertEquals(1, Settings.Secure.getInt(
342                 mContext.getContentResolver(), Settings.Secure.NOTIFICATION_BUBBLES));
343 
344         mListener = mNotificationHelper.enableListener(STUB_PACKAGE_NAME);
345         assertNotNull(mListener);
346 
347         sendNotification(1, R.drawable.black);
348         // wait for notification listener to receive notification
349         mNotificationHelper.findPostedNotification(null, 1, SEARCH_TYPE.POSTED);
350         NotificationListenerService.RankingMap rankingMap = mListener.mRankingMap;
351         NotificationListenerService.Ranking outRanking =
352                 new NotificationListenerService.Ranking();
353         for (String key : rankingMap.getOrderedKeys()) {
354             if (key.contains(mListener.getPackageName())) {
355                 rankingMap.getRanking(key, outRanking);
356                 // by default nothing can bubble
357                 assertFalse(outRanking.canBubble());
358             }
359         }
360 
361         // turn off bubbles globally
362         setBubblesGlobal(false);
363         sleep();
364 
365         rankingMap = mListener.mRankingMap;
366         outRanking = new NotificationListenerService.Ranking();
367         for (String key : rankingMap.getOrderedKeys()) {
368             if (key.contains(mListener.getPackageName())) {
369                 rankingMap.getRanking(key, outRanking);
370                 assertFalse(outRanking.canBubble());
371             }
372         }
373 
374         mListener.resetData();
375     }
376 
377     @Test
testAreBubblesAllowed_appNone()378     public void testAreBubblesAllowed_appNone() throws Exception {
379         if (!isBubblesFeatureSupported()) {
380             return;
381         }
382         setBubblesAppPref(BUBBLE_PREFERENCE_NONE);
383         sleep();
384         assertFalse(mNotificationManager.areBubblesAllowed());
385     }
386 
387     @Test
testAreBubblesAllowed_appSelected()388     public void testAreBubblesAllowed_appSelected() throws Exception {
389         if (!isBubblesFeatureSupported()) {
390             return;
391         }
392         setBubblesAppPref(BUBBLE_PREFERENCE_SELECTED);
393         sleep();
394         assertFalse(mNotificationManager.areBubblesAllowed());
395     }
396 
397     @Test
testAreBubblesAllowed_appAll()398     public void testAreBubblesAllowed_appAll() throws Exception {
399         if (!isBubblesFeatureSupported()) {
400             return;
401         }
402         setBubblesAppPref(BUBBLE_PREFERENCE_ALL);
403         sleep();
404         assertTrue(mNotificationManager.areBubblesAllowed());
405     }
406 
407     @Test
testGetBubblePreference_appNone()408     public void testGetBubblePreference_appNone() throws Exception {
409         if (!isBubblesFeatureSupported()) {
410             return;
411         }
412         setBubblesAppPref(BUBBLE_PREFERENCE_NONE);
413         sleep();
414         assertEquals(BUBBLE_PREFERENCE_NONE, mNotificationManager.getBubblePreference());
415     }
416 
417     @Test
testGetBubblePreference_appSelected()418     public void testGetBubblePreference_appSelected() throws Exception {
419         if (!isBubblesFeatureSupported()) {
420             return;
421         }
422         setBubblesAppPref(BUBBLE_PREFERENCE_SELECTED);
423         sleep();
424         assertEquals(BUBBLE_PREFERENCE_SELECTED, mNotificationManager.getBubblePreference());
425     }
426 
427     @Test
testGetBubblePreference_appAll()428     public void testGetBubblePreference_appAll() throws Exception {
429         if (!isBubblesFeatureSupported()) {
430             return;
431         }
432         setBubblesAppPref(BUBBLE_PREFERENCE_ALL);
433         sleep();
434         assertEquals(BUBBLE_PREFERENCE_ALL, mNotificationManager.getBubblePreference());
435     }
436 
437     @Test
testAreBubblesEnabled()438     public void testAreBubblesEnabled() {
439         if (!isBubblesFeatureSupported()) {
440             return;
441         }
442         setBubblesGlobal(true);
443         sleep();
444         assertTrue(mNotificationManager.areBubblesEnabled());
445     }
446 
447     @Test
testAreBubblesEnabled_false()448     public void testAreBubblesEnabled_false() {
449         if (!isBubblesFeatureSupported()) {
450             return;
451         }
452         setBubblesGlobal(false);
453         sleep();
454         assertFalse(mNotificationManager.areBubblesEnabled());
455     }
456 
457     @Test
testNotificationManagerBubblePolicy_flag_intentBubble()458     public void testNotificationManagerBubblePolicy_flag_intentBubble()
459             throws Exception {
460         if (!isBubblesFeatureSupported()) {
461             return;
462         }
463         try {
464             allowAllNotificationsToBubble();
465             createDynamicShortcut();
466 
467             Notification.Builder nb = getConversationNotification();
468             nb.setBubbleMetadata(getDefaultBubbleMetadata());
469             boolean shouldBeBubble = !mActivityManager.isLowRamDevice();
470             sendAndVerifyBubble(1, nb, shouldBeBubble);
471         } finally {
472             deleteShortcuts();
473         }
474     }
475 
476     @Test
testNotificationManagerBubblePolicy_noFlag_service()477     public void testNotificationManagerBubblePolicy_noFlag_service()
478             throws Exception {
479         if (!isBubblesFeatureSupported()) {
480             return;
481         }
482         Intent serviceIntent = new Intent(mContext, BubblesTestService.class);
483         serviceIntent.putExtra(EXTRA_TEST_CASE, TEST_MESSAGING);
484         try {
485             allowAllNotificationsToBubble();
486 
487             createDynamicShortcut();
488             setUpNotifListener();
489 
490             mContext.startService(serviceIntent);
491 
492             // No services in R (allowed in Q)
493             verifyNotificationBubbleState(BUBBLE_NOTIF_ID, false /* shouldBeBubble */);
494         } finally {
495             deleteShortcuts();
496             mContext.stopService(serviceIntent);
497         }
498     }
499 
500     @Test
testNotificationManagerBubblePolicy_noFlag_phonecall()501     public void testNotificationManagerBubblePolicy_noFlag_phonecall()
502             throws Exception {
503         if (!isBubblesFeatureSupported()) {
504             return;
505         }
506         Intent serviceIntent = new Intent(mContext, BubblesTestService.class);
507         serviceIntent.putExtra(EXTRA_TEST_CASE, TEST_CALL);
508 
509         try {
510             allowAllNotificationsToBubble();
511 
512             createDynamicShortcut();
513             setUpNotifListener();
514 
515             mContext.startService(serviceIntent);
516 
517             // No phonecalls in R (allowed in Q)
518             verifyNotificationBubbleState(BUBBLE_NOTIF_ID, false /* shouldBeBubble */);
519         } finally {
520             deleteShortcuts();
521             mContext.stopService(serviceIntent);
522         }
523     }
524 
525     @Test
testNotificationManagerBubblePolicy_noFlag_foreground()526     public void testNotificationManagerBubblePolicy_noFlag_foreground() throws Exception {
527         if (!isBubblesFeatureSupported()) {
528             return;
529         }
530         try {
531             allowAllNotificationsToBubble();
532 
533             createDynamicShortcut();
534             setUpNotifListener();
535 
536             // Start & get the activity
537             SendBubbleActivity a = startSendBubbleActivity();
538             // Send a bubble that doesn't fulfill policy from foreground
539             a.sendInvalidBubble(BUBBLE_NOTIF_ID, false /* autoExpand */);
540 
541             // No foreground bubbles that don't fulfill policy in R (allowed in Q)
542             verifyNotificationBubbleState(BUBBLE_NOTIF_ID, false /* shouldBeBubble */);
543         } finally {
544             deleteShortcuts();
545             cleanupSendBubbleActivity();
546         }
547     }
548 
549     @Test
testNotificationManagerBubble_checkActivityFlagsDocumentLaunchMode()550     public void testNotificationManagerBubble_checkActivityFlagsDocumentLaunchMode()
551             throws Exception {
552         if (!isBubblesFeatureSupported()) {
553             return;
554         }
555         try {
556             allowAllNotificationsToBubble();
557 
558             createDynamicShortcut();
559             setUpNotifListener();
560 
561             // make ourselves foreground so we can auto-expand the bubble & check the intent flags
562             SendBubbleActivity a = startSendBubbleActivity();
563 
564             // Prep to find bubbled activity
565             Instrumentation.ActivityMonitor monitor = startBubbledActivityMonitor();
566 
567             a.sendBubble(BUBBLE_NOTIF_ID, true /* autoExpand */, false /* suppressNotif */);
568 
569             verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
570 
571             InstrumentationRegistry.getInstrumentation().waitForIdleSync();
572 
573             BubbledActivity activity = (BubbledActivity) monitor.waitForActivityWithTimeout(
574                     ACTIVITY_LAUNCH_TIMEOUT);
575             assertNotNull(String.format(
576                     "Failed to detect BubbleActivity after %d ms", ACTIVITY_LAUNCH_TIMEOUT),
577                     activity);
578             assertTrue((activity.getIntent().getFlags() & FLAG_ACTIVITY_NEW_DOCUMENT) != 0);
579             assertTrue((activity.getIntent().getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0);
580         } finally {
581             deleteShortcuts();
582             cleanupSendBubbleActivity();
583         }
584     }
585 
586     @Test
testNotificationManagerBubblePolicy_flag_shortcutBubble()587     public void testNotificationManagerBubblePolicy_flag_shortcutBubble()
588             throws Exception {
589         if (!isBubblesFeatureSupported()) {
590             return;
591         }
592         try {
593             allowAllNotificationsToBubble();
594 
595             createDynamicShortcut();
596 
597             Notification.Builder nb = getConversationNotification();
598             nb.setBubbleMetadata(
599                     new Notification.BubbleMetadata.Builder(SHARE_SHORTCUT_ID).build());
600 
601             boolean shouldBeBubble = !mActivityManager.isLowRamDevice();
602             sendAndVerifyBubble(1, nb, shouldBeBubble);
603         } finally {
604             deleteShortcuts();
605         }
606     }
607 
608     @Test
testNotificationManagerBubblePolicy_noFlag_invalidShortcut()609     public void testNotificationManagerBubblePolicy_noFlag_invalidShortcut()
610             throws Exception {
611         if (!isBubblesFeatureSupported()) {
612             return;
613         }
614         try {
615             allowAllNotificationsToBubble();
616 
617             createDynamicShortcut();
618 
619             Notification.Builder nb = getConversationNotification();
620             nb.setShortcutId("invalid");
621             nb.setBubbleMetadata(new Notification.BubbleMetadata.Builder("invalid").build());
622 
623             sendAndVerifyBubble(1, nb, false);
624         } finally {
625             deleteShortcuts();
626         }
627     }
628 
629     @Test
testNotificationManagerBubblePolicy_noFlag_invalidNotif()630     public void testNotificationManagerBubblePolicy_noFlag_invalidNotif()
631             throws Exception {
632         if (!isBubblesFeatureSupported()) {
633             return;
634         }
635         try {
636             allowAllNotificationsToBubble();
637 
638             createDynamicShortcut();
639 
640             int id = 1;
641             Notification.Builder nb = getDefaultNotifBuilder(id);
642             nb.setBubbleMetadata(
643                     new Notification.BubbleMetadata.Builder(SHARE_SHORTCUT_ID).build());
644 
645             sendAndVerifyBubble(id, nb, false /* shouldBeBubble */);
646         } finally {
647             deleteShortcuts();
648         }
649     }
650 
651     @Test
testNotificationManagerBubblePolicy_appAll_globalOn()652     public void testNotificationManagerBubblePolicy_appAll_globalOn() throws Exception {
653         if (!isBubblesFeatureSupported()) {
654             return;
655         }
656         try {
657             allowAllNotificationsToBubble();
658 
659             createDynamicShortcut();
660             Notification.Builder nb = getConversationNotification();
661             nb.setBubbleMetadata(
662                     new Notification.BubbleMetadata.Builder(SHARE_SHORTCUT_ID).build());
663 
664             boolean shouldBeBubble = !mActivityManager.isLowRamDevice();
665             sendAndVerifyBubble(1, nb, shouldBeBubble);
666         } finally {
667             deleteShortcuts();
668         }
669     }
670 
671     @Test
testNotificationManagerBubblePolicy_appAll_globalOff()672     public void testNotificationManagerBubblePolicy_appAll_globalOff() throws Exception {
673         if (!isBubblesFeatureSupported()) {
674             return;
675         }
676         try {
677             setBubblesGlobal(false);
678             setBubblesAppPref(1 /* all */);
679             setBubblesChannelAllowed(true);
680             sleep();
681 
682             createDynamicShortcut();
683             Notification.Builder nb = getConversationNotification();
684             nb.setBubbleMetadata(new Notification.BubbleMetadata.Builder(SHARE_SHORTCUT_ID)
685                     .build());
686 
687             sendAndVerifyBubble(1, nb, false);
688         } finally {
689             deleteShortcuts();
690         }
691     }
692 
693     @Test
testNotificationManagerBubblePolicy_appAll_channelNo()694     public void testNotificationManagerBubblePolicy_appAll_channelNo() throws Exception {
695         if (!isBubblesFeatureSupported()) {
696             return;
697         }
698         try {
699             setBubblesGlobal(true);
700             setBubblesAppPref(1 /* all */);
701             setBubblesChannelAllowed(false);
702             sleep();
703 
704             createDynamicShortcut();
705             Notification.Builder nb = getConversationNotification();
706             nb.setBubbleMetadata(new Notification.BubbleMetadata.Builder(SHARE_SHORTCUT_ID)
707                     .build());
708 
709             sendAndVerifyBubble(1, nb, false);
710         } finally {
711             deleteShortcuts();
712         }
713     }
714 
715     @Test
testNotificationManagerBubblePolicy_appSelected_channelNo()716     public void testNotificationManagerBubblePolicy_appSelected_channelNo() throws Exception {
717         if (!isBubblesFeatureSupported()) {
718             return;
719         }
720         try {
721             setBubblesGlobal(true);
722             setBubblesAppPref(2 /* selected */);
723             setBubblesChannelAllowed(false);
724             sleep();
725 
726             createDynamicShortcut();
727             Notification.Builder nb = getConversationNotification();
728             nb.setBubbleMetadata(new Notification.BubbleMetadata.Builder(SHARE_SHORTCUT_ID)
729                     .build());
730 
731             sendAndVerifyBubble(1, nb, false);
732         } finally {
733             deleteShortcuts();
734         }
735     }
736 
737     @Test
testNotificationManagerBubblePolicy_appSelected_channelYes()738     public void testNotificationManagerBubblePolicy_appSelected_channelYes() throws Exception {
739         if (!isBubblesFeatureSupported()) {
740             return;
741         }
742         try {
743             setBubblesGlobal(true);
744             setBubblesAppPref(2 /* selected */);
745             setBubblesChannelAllowed(true);
746             sleep();
747 
748             createDynamicShortcut();
749             Notification.Builder nb = getConversationNotification();
750             nb.setBubbleMetadata(new Notification.BubbleMetadata.Builder(SHARE_SHORTCUT_ID)
751                     .build());
752 
753             boolean shouldBeBubble = !mActivityManager.isLowRamDevice();
754             sendAndVerifyBubble(1, nb, shouldBeBubble);
755         } finally {
756             deleteShortcuts();
757         }
758     }
759 
760     @Test
testNotificationManagerBubblePolicy_appNone_channelNo()761     public void testNotificationManagerBubblePolicy_appNone_channelNo() throws Exception {
762         if (!isBubblesFeatureSupported()) {
763             return;
764         }
765         try {
766             setBubblesGlobal(true);
767             setBubblesAppPref(0 /* none */);
768             setBubblesChannelAllowed(false);
769             sleep();
770 
771             createDynamicShortcut();
772             Notification.Builder nb = getConversationNotification();
773             nb.setBubbleMetadata(new Notification.BubbleMetadata.Builder(SHARE_SHORTCUT_ID)
774                     .build());
775 
776             sendAndVerifyBubble(1, nb, false);
777         } finally {
778             deleteShortcuts();
779         }
780     }
781 
782     @Test
testNotificationManagerBubblePolicy_noFlag_shortcutRemoved()783     public void testNotificationManagerBubblePolicy_noFlag_shortcutRemoved()
784             throws Exception {
785         if (!isBubblesFeatureSupported()) {
786             return;
787         }
788 
789         try {
790             allowAllNotificationsToBubble();
791             createDynamicShortcut();
792             Notification.Builder nb = getConversationNotification();
793             nb.setBubbleMetadata(new Notification.BubbleMetadata.Builder(SHARE_SHORTCUT_ID)
794                     .build());
795 
796             sendAndVerifyBubble(42, nb, true /* shouldBeBubble */);
797             mListener.resetData();
798 
799             deleteShortcuts();
800             verifyNotificationBubbleState(42, false /* should be bubble */);
801         } finally {
802             deleteShortcuts();
803         }
804     }
805 
806     @Test
testNotificationManagerBubbleNotificationSuppression()807     public void testNotificationManagerBubbleNotificationSuppression() throws Exception {
808         if (!isBubblesFeatureSupported()) {
809             return;
810         }
811         try {
812             allowAllNotificationsToBubble();
813 
814             createDynamicShortcut();
815             setUpNotifListener();
816 
817             // make ourselves foreground so we can specify suppress notification flag
818             SendBubbleActivity a = startSendBubbleActivity();
819 
820             // send the bubble with notification suppressed
821             a.sendBubble(BUBBLE_NOTIF_ID, false /* autoExpand */, true /* suppressNotif */);
822             verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
823 
824             // check for the notification
825             StatusBarNotification sbnSuppressed = mListener.mPosted.get(0);
826             assertNotNull(sbnSuppressed);
827             // check for suppression state
828             Notification.BubbleMetadata metadata =
829                     sbnSuppressed.getNotification().getBubbleMetadata();
830             assertNotNull(metadata);
831             assertTrue(metadata.isNotificationSuppressed());
832 
833             mListener.resetData();
834 
835             // send the bubble with notification NOT suppressed
836             a.sendBubble(BUBBLE_NOTIF_ID, false /* autoExpand */, false /* suppressNotif */);
837             verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBubble */);
838 
839             // check for the notification
840             StatusBarNotification sbnNotSuppressed = mListener.mPosted.get(0);
841             assertNotNull(sbnNotSuppressed);
842             // check for suppression state
843             metadata = sbnNotSuppressed.getNotification().getBubbleMetadata();
844             assertNotNull(metadata);
845             assertFalse(metadata.isNotificationSuppressed());
846         } finally {
847             cleanupSendBubbleActivity();
848             deleteShortcuts();
849         }
850     }
851 
852     @Test
testNotificationManagerBubble_checkIsBubbled_pendingIntent()853     public void testNotificationManagerBubble_checkIsBubbled_pendingIntent()
854             throws Exception {
855         if (!isBubblesFeatureSupported()) {
856             return;
857         }
858         try {
859             allowAllNotificationsToBubble();
860 
861             createDynamicShortcut();
862             setUpNotifListener();
863 
864             SendBubbleActivity a = startSendBubbleActivity();
865 
866             // Prep to find bubbled activity
867             Instrumentation.ActivityMonitor monitor = startBubbledActivityMonitor();
868 
869             a.sendBubble(BUBBLE_NOTIF_ID, true /* autoExpand */, false /* suppressNotif */);
870 
871             verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
872 
873             BubbledActivity activity = (BubbledActivity) monitor.waitForActivity();
874             assertTrue(activity.isLaunchedFromBubble());
875         } finally {
876             deleteShortcuts();
877             cleanupSendBubbleActivity();
878         }
879     }
880 
881     @Test
testNotificationManagerBubble_checkIsBubbled_shortcut()882     public void testNotificationManagerBubble_checkIsBubbled_shortcut()
883             throws Exception {
884         if (!isBubblesFeatureSupported()) {
885             return;
886         }
887         try {
888             allowAllNotificationsToBubble();
889 
890             createDynamicShortcut();
891             setUpNotifListener();
892 
893             SendBubbleActivity a = startSendBubbleActivity();
894 
895             // Prep to find bubbled activity
896             Instrumentation.ActivityMonitor monitor = startBubbledActivityMonitor();
897 
898             a.sendBubble(BUBBLE_NOTIF_ID, true /* autoExpand */,
899                     false /* suppressNotif */,
900                     false /* suppressBubble */,
901                     true /* useShortcut */,
902                     true /* setLocus */);
903 
904             verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
905 
906             BubbledActivity activity = (BubbledActivity) monitor.waitForActivity();
907             assertTrue(activity.isLaunchedFromBubble());
908         } finally {
909             deleteShortcuts();
910             cleanupSendBubbleActivity();
911         }
912     }
913 
914     /** Verifies the bubble is suppressed when it should be. */
915     @Test
testNotificationManagerBubble_setSuppressBubble()916     public void testNotificationManagerBubble_setSuppressBubble()
917             throws Exception {
918         if (!isBubblesFeatureSupported()) {
919             return;
920         }
921         try {
922             allowAllNotificationsToBubble();
923 
924             createDynamicShortcut();
925             setUpNotifListener();
926 
927             final int notifId = 3;
928 
929             // Make a bubble
930             SendBubbleActivity a = startSendBubbleActivity();
931             a.sendBubble(notifId,
932                     false /* autoExpand */,
933                     false /* suppressNotif */,
934                     true /* suppressBubble */);
935 
936             verifyNotificationBubbleState(notifId, true /* shouldBeBubble */);
937             mListener.resetData();
938 
939             // Launch same activity as whats in the bubble
940             BubbledActivity activity = startBubbleActivity(notifId);
941 
942             // It should have the locusId
943             assertEquals(new LocusId(String.valueOf(notifId)),
944                     activity.getLocusId());
945 
946             // notif gets posted with update, so wait
947             verifyNotificationBubbleState(notifId, true /* shouldBeBubble */);
948 
949             // Bubble should have suppressed flag
950             StatusBarNotification sbn = mNotificationHelper.findPostedNotification(null, notifId,
951                     SEARCH_TYPE.POSTED);
952             assertTrue(sbn.getNotification().getBubbleMetadata().isBubbleSuppressable());
953             assertTrue(sbn.getNotification().getBubbleMetadata().isBubbleSuppressed());
954         } finally {
955             deleteShortcuts();
956             cleanupSendBubbleActivity();
957         }
958     }
959 
960     /** Verifies the bubble is not suppressed if dev didn't specify suppressable */
961     @Test
testNotificationManagerBubble_setSuppressBubble_notSuppressable()962     public void testNotificationManagerBubble_setSuppressBubble_notSuppressable()
963             throws Exception {
964         if (!isBubblesFeatureSupported()) {
965             return;
966         }
967         try {
968             allowAllNotificationsToBubble();
969 
970             createDynamicShortcut();
971             setUpNotifListener();
972 
973             // Make a bubble
974             SendBubbleActivity a = startSendBubbleActivity();
975             a.sendBubble(BUBBLE_NOTIF_ID,
976                     false /* autoExpand */,
977                     false /* suppressNotif */,
978                     false /* suppressBubble */);
979 
980             verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
981             mListener.resetData();
982 
983             // Launch same activity as whats in the bubble
984             BubbledActivity activity = startBubbleActivity(BUBBLE_NOTIF_ID);
985 
986             // It should have the locusId
987             assertEquals(new LocusId(String.valueOf(BUBBLE_NOTIF_ID)),
988                     activity.getLocusId());
989 
990             // Wait a little (if it wrongly updates it'd be a new post so wait for that))
991             sleep();
992             assertTrue(mListener.mPosted.isEmpty());
993 
994             // Bubble should not be suppressed
995             StatusBarNotification sbn = mNotificationHelper.findPostedNotification(null,
996                     BUBBLE_NOTIF_ID, SEARCH_TYPE.LISTENER);
997             assertFalse(sbn.getNotification().getBubbleMetadata().isBubbleSuppressable());
998             assertFalse(sbn.getNotification().getBubbleMetadata().isBubbleSuppressed());
999         } finally {
1000             deleteShortcuts();
1001             cleanupSendBubbleActivity();
1002         }
1003     }
1004 
1005     /** Verifies the bubble is not suppressed if the activity doesn't have a locusId. */
1006     @Test
testNotificationManagerBubble_setSuppressBubble_activityNoLocusId()1007     public void testNotificationManagerBubble_setSuppressBubble_activityNoLocusId()
1008             throws Exception {
1009         if (!isBubblesFeatureSupported()) {
1010             return;
1011         }
1012         try {
1013             allowAllNotificationsToBubble();
1014 
1015             createDynamicShortcut();
1016             setUpNotifListener();
1017 
1018             // Make a bubble
1019             SendBubbleActivity a = startSendBubbleActivity();
1020             a.sendBubble(BUBBLE_NOTIF_ID,
1021                     false /* autoExpand */,
1022                     false /* suppressNotif */,
1023                     true /* suppressBubble */);
1024 
1025             verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
1026             mListener.resetData();
1027 
1028             // Launch same activity as whats in the bubble
1029             BubbledActivity activity = startBubbleActivity(BUBBLE_NOTIF_ID, false /* addLocusId */);
1030 
1031             // It shouldn't have the locusId
1032             assertNull(activity.getLocusId());
1033 
1034             // Wait a little (if it wrongly updates it'd be a new post so wait for that))
1035             sleep();
1036             assertTrue(mListener.mPosted.isEmpty());
1037 
1038             // Bubble should not be suppressed
1039             StatusBarNotification sbn = mNotificationHelper.findPostedNotification(null,
1040                     BUBBLE_NOTIF_ID, SEARCH_TYPE.LISTENER);
1041             assertTrue(sbn.getNotification().getBubbleMetadata().isBubbleSuppressable());
1042             assertFalse(sbn.getNotification().getBubbleMetadata().isBubbleSuppressed());
1043         } finally {
1044             deleteShortcuts();
1045             cleanupSendBubbleActivity();
1046         }
1047     }
1048 
1049     /** Verifies the bubble is not suppressed if the notification doesn't have a locusId. */
1050     @Test
testNotificationManagerBubble_setSuppressBubble_notificationNoLocusId()1051     public void testNotificationManagerBubble_setSuppressBubble_notificationNoLocusId()
1052             throws Exception {
1053         if (!isBubblesFeatureSupported()) {
1054             return;
1055         }
1056         try {
1057             allowAllNotificationsToBubble();
1058 
1059             createDynamicShortcut();
1060             setUpNotifListener();
1061 
1062             // Make a bubble
1063             SendBubbleActivity a = startSendBubbleActivity();
1064             a.sendBubble(BUBBLE_NOTIF_ID,
1065                     false /* autoExpand */,
1066                     false /* suppressNotif */,
1067                     true /* suppressBubble */,
1068                     false /* useShortcut */,
1069                     false /* setLocusId */);
1070 
1071             verifyNotificationBubbleState(BUBBLE_NOTIF_ID, true /* shouldBeBubble */);
1072             mListener.resetData();
1073 
1074             // Launch same activity as whats in the bubble
1075             BubbledActivity activity = startBubbleActivity(BUBBLE_NOTIF_ID, true /* addLocusId */);
1076 
1077             // Activity has the locus
1078             assertNotNull(activity.getLocusId());
1079 
1080             // Wait a little (if it wrongly updates it'd be a new post so wait for that))
1081             sleep();
1082             assertTrue(mListener.mPosted.isEmpty());
1083 
1084             // Bubble should not be suppressed & not have a locusId
1085             StatusBarNotification sbn = mNotificationHelper.findPostedNotification(null,
1086                     BUBBLE_NOTIF_ID, SEARCH_TYPE.LISTENER);
1087             assertNull(sbn.getNotification().getLocusId());
1088             assertTrue(sbn.getNotification().getBubbleMetadata().isBubbleSuppressable());
1089             assertFalse(sbn.getNotification().getBubbleMetadata().isBubbleSuppressed());
1090         } finally {
1091             deleteShortcuts();
1092             cleanupSendBubbleActivity();
1093         }
1094     }
1095 
1096     /** Verifies the bubble is unsuppressed when the locus activity is hidden. */
1097     @Test
testNotificationManagerBubble_setSuppressBubble_dismissLocusActivity()1098     public void testNotificationManagerBubble_setSuppressBubble_dismissLocusActivity()
1099             throws Exception {
1100         if (!isBubblesFeatureSupported()) {
1101             return;
1102         }
1103         try {
1104             allowAllNotificationsToBubble();
1105 
1106             createDynamicShortcut();
1107             setUpNotifListener();
1108 
1109             final int notifId = 2;
1110 
1111             // Make a bubble
1112             SendBubbleActivity a = startSendBubbleActivity();
1113             a.sendBubble(notifId,
1114                     false /* autoExpand */,
1115                     false /* suppressNotif */,
1116                     true /* suppressBubble */);
1117 
1118             verifyNotificationBubbleState(notifId, true);
1119             StatusBarNotification sbn = mNotificationHelper.findPostedNotification(null, notifId,
1120                     SEARCH_TYPE.POSTED);
1121             assertTrue(sbn.getNotification().getBubbleMetadata().isBubbleSuppressable());
1122             assertFalse(sbn.getNotification().getBubbleMetadata().isBubbleSuppressed());
1123             mListener.resetData();
1124 
1125             // Launch same activity as whats in the bubble
1126             BubbledActivity activity = startBubbleActivity(notifId);
1127 
1128             // It should have the locusId
1129             assertEquals(new LocusId(String.valueOf(notifId)), activity.getLocusId());
1130 
1131             // notif gets posted with update, so wait
1132             verifyNotificationBubbleState(notifId, true /* shouldBeBubble */);
1133             // Bubble should have suppressed flag
1134             sbn = mNotificationHelper.findPostedNotification(null, notifId, SEARCH_TYPE.LISTENER);
1135             assertTrue(sbn.getNotification().getBubbleMetadata().isBubbleSuppressable());
1136             assertTrue(sbn.getNotification().getBubbleMetadata().isBubbleSuppressed());
1137             mListener.resetData();
1138 
1139             activity.finish();
1140 
1141             // notif gets posted with update, so wait
1142             verifyNotificationBubbleState(notifId, true /* shouldBeBubble */);
1143             sbn = mNotificationHelper.findPostedNotification(null, notifId, SEARCH_TYPE.LISTENER);
1144             assertTrue(sbn.getNotification().getBubbleMetadata().isBubbleSuppressable());
1145             assertFalse(sbn.getNotification().getBubbleMetadata().isBubbleSuppressed());
1146         } finally {
1147             deleteShortcuts();
1148             cleanupSendBubbleActivity();
1149         }
1150     }
1151 
1152     /** Verifies that a regular activity can't specify a bubble in ActivityOptions */
1153     @Test
1154     @SuppressWarnings("AssertThrowsMultipleStatements")
testNotificationManagerBubble_launchBubble_activityOptions_fails()1155     public void testNotificationManagerBubble_launchBubble_activityOptions_fails()
1156             throws Exception {
1157         if (!isBubblesFeatureSupported()) {
1158             return;
1159         }
1160         try {
1161             // Start test activity
1162             SendBubbleActivity activity = startSendBubbleActivity();
1163             assertFalse(activity.isLaunchedFromBubble());
1164 
1165             // Should have exception
1166             assertThrows(SecurityException.class, () -> {
1167                 Intent i = new Intent(mContext, BubbledActivity.class);
1168                 ActivityOptions options = ActivityOptions.makeBasic();
1169                 Bundle b = options.toBundle();
1170                 b.putBoolean("android.activity.launchTypeBubble", true);
1171                 activity.startActivity(i, b);
1172             });
1173         } finally {
1174             cleanupSendBubbleActivity();
1175         }
1176     }
1177 }
1178