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