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