1 /** 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 * express or implied. See the License for the specific language governing permissions and 12 * limitations under the License. 13 */ 14 15 package android.accessibilityservice.cts.utils; 16 17 import static android.accessibility.cts.common.ShellCommandBuilder.execShellCommand; 18 import static android.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS; 19 import static android.content.pm.PackageManager.FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS; 20 21 import static org.junit.Assert.assertNotNull; 22 import static org.junit.Assert.fail; 23 24 import android.accessibilityservice.AccessibilityServiceInfo; 25 import android.app.Activity; 26 import android.app.ActivityOptions; 27 import android.app.Instrumentation; 28 import android.app.UiAutomation; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.pm.PackageManager; 32 import android.content.pm.ResolveInfo; 33 import android.graphics.Rect; 34 import android.os.PowerManager; 35 import android.os.SystemClock; 36 import android.text.TextUtils; 37 import android.util.Log; 38 import android.util.SparseArray; 39 import android.view.Display; 40 import android.view.InputDevice; 41 import android.view.KeyCharacterMap; 42 import android.view.KeyEvent; 43 import android.view.accessibility.AccessibilityEvent; 44 import android.view.accessibility.AccessibilityNodeInfo; 45 import android.view.accessibility.AccessibilityWindowInfo; 46 47 import androidx.test.rule.ActivityTestRule; 48 49 import com.android.compatibility.common.util.TestUtils; 50 51 import java.util.Arrays; 52 import java.util.List; 53 import java.util.Objects; 54 import java.util.concurrent.TimeoutException; 55 import java.util.function.BooleanSupplier; 56 import java.util.stream.Collectors; 57 58 /** 59 * Utilities useful when launching an activity to make sure it's all the way on the screen 60 * before we start testing it. 61 */ 62 public class ActivityLaunchUtils { 63 private static final String LOG_TAG = "ActivityLaunchUtils"; 64 private static final String AM_START_HOME_ACTIVITY_COMMAND = 65 "am start -a android.intent.action.MAIN -c android.intent.category.HOME"; 66 public static final String AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND = 67 "am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS"; 68 public static final String INPUT_KEYEVENT_KEYCODE_BACK = 69 "input keyevent KEYCODE_BACK"; 70 71 // Using a static variable so it can be used in lambdas. Not preserving state in it. 72 private static Activity mTempActivity; 73 launchActivityAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, ActivityTestRule<T> rule)74 public static <T extends Activity> T launchActivityAndWaitForItToBeOnscreen( 75 Instrumentation instrumentation, UiAutomation uiAutomation, 76 ActivityTestRule<T> rule) throws Exception { 77 ActivityLauncher activityLauncher = new ActivityLauncher() { 78 @Override 79 Activity launchActivity() { 80 return rule.launchActivity(null); 81 } 82 }; 83 return launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(instrumentation, 84 uiAutomation, activityLauncher, Display.DEFAULT_DISPLAY); 85 } 86 87 /** 88 * If this activity would be launched at virtual display, please finishes this activity before 89 * this test ended. Otherwise it will be displayed on default display and impacts the next test. 90 */ launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, Class<T> clazz, int displayId)91 public static <T extends Activity> T launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( 92 Instrumentation instrumentation, UiAutomation uiAutomation, Class<T> clazz, 93 int displayId) throws Exception { 94 final ActivityOptions options = ActivityOptions.makeBasic(); 95 options.setLaunchDisplayId(displayId); 96 final Intent intent = new Intent(instrumentation.getTargetContext(), clazz); 97 // Add clear task because this activity may on other display. 98 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK|Intent.FLAG_ACTIVITY_NEW_TASK); 99 100 ActivityLauncher activityLauncher = new ActivityLauncher() { 101 @Override 102 Activity launchActivity() { 103 uiAutomation.adoptShellPermissionIdentity(); 104 try { 105 return instrumentation.startActivitySync(intent, options.toBundle()); 106 } finally { 107 uiAutomation.dropShellPermissionIdentity(); 108 } 109 } 110 }; 111 return launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(instrumentation, 112 uiAutomation, activityLauncher, displayId); 113 } 114 getActivityTitle( Instrumentation instrumentation, Activity activity)115 public static CharSequence getActivityTitle( 116 Instrumentation instrumentation, Activity activity) { 117 final StringBuilder titleBuilder = new StringBuilder(); 118 instrumentation.runOnMainSync(() -> titleBuilder.append(activity.getTitle())); 119 return titleBuilder; 120 } 121 findWindowByTitle( UiAutomation uiAutomation, CharSequence title)122 public static AccessibilityWindowInfo findWindowByTitle( 123 UiAutomation uiAutomation, CharSequence title) { 124 final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows(); 125 return findWindowByTitleWithList(title, windows); 126 } 127 findWindowByTitleAndDisplay( UiAutomation uiAutomation, CharSequence title, int displayId)128 public static AccessibilityWindowInfo findWindowByTitleAndDisplay( 129 UiAutomation uiAutomation, CharSequence title, int displayId) { 130 final SparseArray<List<AccessibilityWindowInfo>> allWindows = 131 uiAutomation.getWindowsOnAllDisplays(); 132 final List<AccessibilityWindowInfo> windowsOfDisplay = allWindows.get(displayId); 133 return findWindowByTitleWithList(title, windowsOfDisplay); 134 } 135 homeScreenOrBust(Context context, UiAutomation uiAutomation)136 public static void homeScreenOrBust(Context context, UiAutomation uiAutomation) { 137 wakeUpOrBust(context, uiAutomation); 138 if (context.getPackageManager().isInstantApp()) return; 139 if (isHomeScreenShowing(context, uiAutomation)) return; 140 final AccessibilityServiceInfo serviceInfo = uiAutomation.getServiceInfo(); 141 final int enabledFlags = serviceInfo.flags; 142 // Make sure we could query windows. 143 serviceInfo.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; 144 uiAutomation.setServiceInfo(serviceInfo); 145 try { 146 executeAndWaitOn( 147 uiAutomation, 148 () -> { 149 execShellCommand(uiAutomation, AM_START_HOME_ACTIVITY_COMMAND); 150 execShellCommand(uiAutomation, AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND); 151 execShellCommand(uiAutomation, INPUT_KEYEVENT_KEYCODE_BACK); 152 }, 153 () -> isHomeScreenShowing(context, uiAutomation), 154 DEFAULT_TIMEOUT_MS, 155 "home screen"); 156 } catch (AssertionError error) { 157 Log.e(LOG_TAG, "Timed out looking for home screen. Dumping window list"); 158 final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows(); 159 if (windows == null) { 160 Log.e(LOG_TAG, "Window list is null"); 161 } else if (windows.isEmpty()) { 162 Log.e(LOG_TAG, "Window list is empty"); 163 } else { 164 for (AccessibilityWindowInfo window : windows) { 165 Log.e(LOG_TAG, window.toString()); 166 } 167 } 168 169 fail("Unable to reach home screen"); 170 } finally { 171 serviceInfo.flags = enabledFlags; 172 uiAutomation.setServiceInfo(serviceInfo); 173 } 174 } 175 supportsMultiDisplay(Context context)176 public static boolean supportsMultiDisplay(Context context) { 177 return context.getPackageManager().hasSystemFeature( 178 FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS); 179 } 180 isHomeScreenShowing(Context context, UiAutomation uiAutomation)181 private static boolean isHomeScreenShowing(Context context, UiAutomation uiAutomation) { 182 final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows(); 183 final PackageManager packageManager = context.getPackageManager(); 184 final List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities( 185 new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME), 186 PackageManager.MATCH_DEFAULT_ONLY); 187 188 // Look for a window with a package name that matches the default home screen 189 for (AccessibilityWindowInfo window : windows) { 190 final AccessibilityNodeInfo root = window.getRoot(); 191 if (root != null) { 192 final CharSequence packageName = root.getPackageName(); 193 if (packageName != null) { 194 for (ResolveInfo resolveInfo : resolveInfos) { 195 if ((resolveInfo.activityInfo != null) 196 && packageName.equals(resolveInfo.activityInfo.packageName)) { 197 return true; 198 } 199 } 200 } 201 } 202 } 203 // List unexpected package names of default home screen that invoking ResolverActivity 204 final CharSequence homePackageNames = resolveInfos.stream() 205 .map(r -> r.activityInfo).filter(Objects::nonNull) 206 .map(a -> a.packageName).collect(Collectors.joining(", ")); 207 Log.v(LOG_TAG, "No window matched with package names of home screen: " + homePackageNames); 208 return false; 209 } 210 wakeUpOrBust(Context context, UiAutomation uiAutomation)211 private static void wakeUpOrBust(Context context, UiAutomation uiAutomation) { 212 final long deadlineUptimeMillis = SystemClock.uptimeMillis() + DEFAULT_TIMEOUT_MS; 213 final PowerManager powerManager = context.getSystemService(PowerManager.class); 214 do { 215 if (powerManager.isInteractive()) { 216 Log.d(LOG_TAG, "Device is interactive"); 217 return; 218 } 219 220 Log.d(LOG_TAG, "Sending wakeup keycode"); 221 final long eventTime = SystemClock.uptimeMillis(); 222 uiAutomation.injectInputEvent( 223 new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, 224 KeyEvent.KEYCODE_WAKEUP, 0 /* repeat */, 0 /* metastate */, 225 KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 0 /* flags */, 226 InputDevice.SOURCE_KEYBOARD), true /* sync */); 227 uiAutomation.injectInputEvent( 228 new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, 229 KeyEvent.KEYCODE_WAKEUP, 0 /* repeat */, 0 /* metastate */, 230 KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 0 /* flags */, 231 InputDevice.SOURCE_KEYBOARD), true /* sync */); 232 try { 233 Thread.sleep(50); 234 } catch (InterruptedException e) {} 235 } while (SystemClock.uptimeMillis() < deadlineUptimeMillis); 236 fail("Unable to wake up screen"); 237 } 238 239 /** 240 * Executes a command and waits for a specified condition up to a given wait timeout. It checks 241 * condition result each time when events delivered, and throws exception if the condition 242 * result is not {@code true} within the given timeout. 243 */ executeAndWaitOn(UiAutomation uiAutomation, Runnable command, BooleanSupplier condition, long timeoutMillis, String conditionName)244 private static void executeAndWaitOn(UiAutomation uiAutomation, Runnable command, 245 BooleanSupplier condition, long timeoutMillis, String conditionName) { 246 final Object waitObject = new Object(); 247 final long executionStartTimeMillis = SystemClock.uptimeMillis(); 248 try { 249 uiAutomation.setOnAccessibilityEventListener((event) -> { 250 if (event.getEventTime() < executionStartTimeMillis) { 251 return; 252 } 253 synchronized (waitObject) { 254 waitObject.notifyAll(); 255 } 256 }); 257 command.run(); 258 TestUtils.waitOn(waitObject, condition, timeoutMillis, conditionName); 259 } finally { 260 uiAutomation.setOnAccessibilityEventListener(null); 261 } 262 } 263 launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, ActivityLauncher activityLauncher, int displayId)264 private static <T extends Activity> T launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( 265 Instrumentation instrumentation, UiAutomation uiAutomation, 266 ActivityLauncher activityLauncher, int displayId) throws Exception { 267 final int[] location = new int[2]; 268 final StringBuilder activityPackage = new StringBuilder(); 269 final Rect bounds = new Rect(); 270 final StringBuilder activityTitle = new StringBuilder(); 271 final StringBuilder timeoutExceptionRecords = new StringBuilder(); 272 // Make sure we get window events, so we'll know when the window appears 273 AccessibilityServiceInfo info = uiAutomation.getServiceInfo(); 274 info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; 275 uiAutomation.setServiceInfo(info); 276 // There is no any window on virtual display even doing GLOBAL_ACTION_HOME, so only 277 // checking the home screen for default display. 278 if (displayId == Display.DEFAULT_DISPLAY) { 279 homeScreenOrBust(instrumentation.getContext(), uiAutomation); 280 } 281 282 try { 283 final AccessibilityEvent awaitedEvent = uiAutomation.executeAndWaitForEvent( 284 () -> { 285 mTempActivity = activityLauncher.launchActivity(); 286 instrumentation.runOnMainSync(() -> { 287 mTempActivity.getWindow().getDecorView().getLocationOnScreen(location); 288 activityPackage.append(mTempActivity.getPackageName()); 289 }); 290 instrumentation.waitForIdleSync(); 291 activityTitle.append(getActivityTitle(instrumentation, mTempActivity)); 292 }, 293 (event) -> { 294 final AccessibilityWindowInfo window = 295 findWindowByTitleAndDisplay(uiAutomation, activityTitle, displayId); 296 if (window == null) return false; 297 if (window.getRoot() == null) return false; 298 if (displayId == Display.DEFAULT_DISPLAY 299 && (!window.isActive() || !window.isFocused())) { 300 // The window should get activated and focused. 301 // Launching activity in non-default display in CTS is usually in a 302 // virtual display, which doesn't get focused on launch. 303 return false; 304 } 305 306 window.getBoundsInScreen(bounds); 307 mTempActivity.getWindow().getDecorView().getLocationOnScreen(location); 308 309 // Stores the related information including event, location and window 310 // as a timeout exception record. 311 timeoutExceptionRecords.append(String.format("{Received event: %s \n" 312 + "Window location: %s \nA11y window: %s}\n", 313 event, Arrays.toString(location), window)); 314 315 return (!bounds.isEmpty()) 316 && (bounds.left == location[0]) && (bounds.top == location[1]); 317 }, DEFAULT_TIMEOUT_MS); 318 assertNotNull(awaitedEvent); 319 } catch (TimeoutException timeout) { 320 throw new TimeoutException(timeout.getMessage() + "\n\nTimeout exception records : \n" 321 + timeoutExceptionRecords); 322 } 323 return (T) mTempActivity; 324 } 325 findWindowByTitleWithList(CharSequence title, List<AccessibilityWindowInfo> windows)326 private static AccessibilityWindowInfo findWindowByTitleWithList(CharSequence title, 327 List<AccessibilityWindowInfo> windows) { 328 AccessibilityWindowInfo returnValue = null; 329 if (windows != null && windows.size() > 0) { 330 for (int i = 0; i < windows.size(); i++) { 331 final AccessibilityWindowInfo window = windows.get(i); 332 if (TextUtils.equals(title, window.getTitle())) { 333 returnValue = window; 334 } else { 335 window.recycle(); 336 } 337 } 338 } 339 return returnValue; 340 } 341 342 private static abstract class ActivityLauncher { launchActivity()343 abstract Activity launchActivity(); 344 } 345 } 346