• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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