• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.stubs.shared;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertNotEquals;
21 import static org.junit.Assert.assertNotNull;
22 import static org.junit.Assert.assertTrue;
23 
24 import android.app.Instrumentation;
25 import android.app.NotificationManager;
26 import android.app.PendingIntent.CanceledException;
27 import android.app.UiAutomation;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.os.ParcelFileDescriptor;
31 import android.service.notification.StatusBarNotification;
32 import android.util.Log;
33 
34 import androidx.test.platform.app.InstrumentationRegistry;
35 
36 import com.google.common.base.Objects;
37 
38 import java.io.FileInputStream;
39 import java.io.IOException;
40 import java.io.InputStream;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 
44 public class NotificationHelper {
45 
46     private static final String TAG = NotificationHelper.class.getSimpleName();
47     public static final long SHORT_WAIT_TIME = 100;
48     public static final long MAX_WAIT_TIME = 2000;
49 
50     public enum SEARCH_TYPE {
51         /**
52          * Search for the notification only within the posted app. This returns enqueued
53          * as well as posted notifications, so use with caution.
54          */
55         APP,
56         /**
57          * Search for the notification across all apps. Makes a binder call from the NLS to
58          * check currently posted notifications for all apps, which means it can return
59          * notifications the NLS hasn't been informed about yet.
60          */
61         LISTENER,
62         /**
63          * Search for the notification across all apps. Looks only in the list of notifications
64          * that the listener has been informed about via onNotificationPosted.
65          */
66         POSTED
67     }
68 
69     private final Context mContext;
70     private final NotificationManager mNotificationManager;
71     private TestNotificationListener mNotificationListener;
72     private TestNotificationAssistant mAssistant;
73 
NotificationHelper(Context context)74     public NotificationHelper(Context context) {
75         mContext = context;
76         mNotificationManager = mContext.getSystemService(NotificationManager.class);
77     }
78 
clickNotification(int notificationId, boolean searchAll)79     public void clickNotification(int notificationId, boolean searchAll) throws CanceledException {
80         findPostedNotification(null, notificationId,
81                 searchAll ? SEARCH_TYPE.LISTENER : SEARCH_TYPE.APP)
82                 .getNotification().contentIntent.send();
83     }
84 
findPostedNotification(String tag, int id, SEARCH_TYPE searchType)85     public StatusBarNotification findPostedNotification(String tag, int id,
86             SEARCH_TYPE searchType) {
87         // notification posting is asynchronous so it may take a few hundred ms to appear.
88         // we will check for it for up to MAX_WAIT_TIME ms before giving up.
89         for (long totalWait = 0; totalWait < MAX_WAIT_TIME; totalWait += SHORT_WAIT_TIME) {
90             StatusBarNotification n = findNotificationNoWait(tag, id, searchType);
91             if (n != null) {
92                 return n;
93             }
94             try {
95                 Thread.sleep(SHORT_WAIT_TIME);
96             } catch (InterruptedException ex) {
97                 // pass
98             }
99         }
100         return findNotificationNoWait(null, id, searchType);
101     }
102 
103     /**
104      * Returns true if the notification cannot be found. Polls for the notification to account for
105      * delays in posting
106      */
isNotificationGone(int id, SEARCH_TYPE searchType)107     public boolean isNotificationGone(int id, SEARCH_TYPE searchType) {
108         // notification is a bit asynchronous so it may take a few ms to appear in
109         // getActiveNotifications()
110         // we will check for it for up to MAX_WAIT_TIME ms before giving up.
111         boolean found = false;
112         for (long totalWait = 0; totalWait < MAX_WAIT_TIME; totalWait += SHORT_WAIT_TIME) {
113             // Need reset flag.
114             found = false;
115             for (StatusBarNotification sbn : getActiveNotifications(searchType)) {
116                 Log.d(TAG, "Found " + sbn.getKey());
117                 if (sbn.getId() == id) {
118                     found = true;
119                     break;
120                 }
121             }
122             if (!found) break;
123             try {
124                 Thread.sleep(SHORT_WAIT_TIME);
125             } catch (InterruptedException ex) {
126                 // pass
127             }
128         }
129         return !found;
130     }
131 
132     /**
133      * Checks whether the NLS has received a removal event for this notification
134      */
isNotificationGone(String key)135     public boolean isNotificationGone(String key) {
136         for (long totalWait = 0; totalWait < MAX_WAIT_TIME; totalWait += SHORT_WAIT_TIME) {
137             if (mNotificationListener.mRemoved.containsKey(key)) {
138                 return true;
139             }
140             try {
141                 Thread.sleep(SHORT_WAIT_TIME);
142             } catch (InterruptedException ex) {
143                 // pass
144             }
145         }
146         return false;
147     }
148 
findNotificationNoWait(String tag, int id, SEARCH_TYPE searchType)149     private StatusBarNotification findNotificationNoWait(String tag, int id,
150             SEARCH_TYPE searchType) {
151         for (StatusBarNotification sbn : getActiveNotifications(searchType)) {
152             if (sbn.getId() == id && Objects.equal(sbn.getTag(), tag)) {
153                 return sbn;
154             }
155         }
156         return null;
157     }
158 
getActiveNotifications(SEARCH_TYPE searchType)159     private ArrayList<StatusBarNotification> getActiveNotifications(SEARCH_TYPE searchType) {
160         switch (searchType) {
161             case APP:
162                 return new ArrayList<>(
163                         Arrays.asList(mNotificationManager.getActiveNotifications()));
164             case POSTED:
165                 return new ArrayList(mNotificationListener.mPosted);
166             case LISTENER:
167             default:
168                 return new ArrayList<>(
169                         Arrays.asList(mNotificationListener.getActiveNotifications()));
170         }
171     }
172 
enableListener(String pkg)173     public TestNotificationListener enableListener(String pkg) throws IOException {
174         String command = " cmd notification allow_listener "
175                 + pkg + "/" + TestNotificationListener.class.getName();
176         runCommand(command, InstrumentationRegistry.getInstrumentation());
177         mNotificationListener = TestNotificationListener.getInstance();
178         if (mNotificationListener != null) {
179             mNotificationListener.addTestPackage(pkg);
180         }
181         return mNotificationListener;
182     }
183 
disableListener(String pkg)184     public void disableListener(String pkg) throws IOException {
185         final ComponentName component =
186                 new ComponentName(pkg, TestNotificationListener.class.getName());
187         String command = " cmd notification disallow_listener " + component.flattenToString();
188 
189         runCommand(command, InstrumentationRegistry.getInstrumentation());
190 
191         final NotificationManager nm = mContext.getSystemService(NotificationManager.class);
192         assertEquals(component + " has incorrect listener access",
193                 false, nm.isNotificationListenerAccessGranted(component));
194     }
195 
enableAssistant(String pkg)196     public TestNotificationAssistant enableAssistant(String pkg) throws IOException {
197         final ComponentName component =
198                 new ComponentName(pkg, TestNotificationAssistant.class.getName());
199 
200         InstrumentationRegistry.getInstrumentation().getUiAutomation()
201                 .adoptShellPermissionIdentity("android.permission.STATUS_BAR_SERVICE",
202                         "android.permission.REQUEST_NOTIFICATION_ASSISTANT_SERVICE");
203         mNotificationManager.setNotificationAssistantAccessGranted(component, true);
204 
205         assertTrue(component + " has not been allowed",
206                 mNotificationManager.isNotificationAssistantAccessGranted(component));
207         assertEquals(component, mNotificationManager.getAllowedNotificationAssistant());
208 
209         mAssistant = TestNotificationAssistant.getInstance();
210 
211         InstrumentationRegistry.getInstrumentation()
212                 .getUiAutomation().dropShellPermissionIdentity();
213         return mAssistant;
214     }
215 
disableAssistant(String pkg)216     public void disableAssistant(String pkg) throws IOException {
217         final ComponentName component =
218                 new ComponentName(pkg, TestNotificationAssistant.class.getName());
219 
220         InstrumentationRegistry.getInstrumentation().getUiAutomation()
221                 .adoptShellPermissionIdentity("android.permission.STATUS_BAR_SERVICE",
222                         "android.permission.REQUEST_NOTIFICATION_ASSISTANT_SERVICE");
223         mNotificationManager.setNotificationAssistantAccessGranted(component, false);
224 
225         assertTrue(component + " has not been disallowed",
226                 !mNotificationManager.isNotificationAssistantAccessGranted(component));
227         assertNotEquals(component, mNotificationManager.getAllowedNotificationAssistant());
228 
229         InstrumentationRegistry.getInstrumentation()
230                 .getUiAutomation().dropShellPermissionIdentity();
231     }
232 
233     @SuppressWarnings("StatementWithEmptyBody")
runCommand(String command, Instrumentation instrumentation)234     public void runCommand(String command, Instrumentation instrumentation)
235             throws IOException {
236         UiAutomation uiAutomation = instrumentation.getUiAutomation();
237         // Execute command
238         try (ParcelFileDescriptor fd = uiAutomation.executeShellCommand(command)) {
239             assertNotNull("Failed to execute shell command: " + command, fd);
240             // Wait for the command to finish by reading until EOF
241             try (InputStream in = new FileInputStream(fd.getFileDescriptor())) {
242                 byte[] buffer = new byte[4096];
243                 while (in.read(buffer) > 0) {
244                     // discard output
245                 }
246             } catch (IOException e) {
247                 throw new IOException("Could not read stdout of command: " + command, e);
248             }
249         }
250     }
251 }
252