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