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