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.accessibilityservice.cts.utils.CtsTestUtils.isAutomotive; 20 import static android.content.pm.PackageManager.FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS; 21 import static android.os.UserHandle.USER_ALL; 22 23 import static org.junit.Assert.assertNotNull; 24 import static org.junit.Assert.fail; 25 26 import android.accessibilityservice.AccessibilityServiceInfo; 27 import android.app.Activity; 28 import android.app.ActivityOptions; 29 import android.app.Instrumentation; 30 import android.app.KeyguardManager; 31 import android.app.UiAutomation; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.content.pm.PackageManager; 35 import android.content.pm.ResolveInfo; 36 import android.graphics.Rect; 37 import android.os.PowerManager; 38 import android.os.SystemClock; 39 import android.text.TextUtils; 40 import android.util.Log; 41 import android.util.SparseArray; 42 import android.view.Display; 43 import android.view.InputDevice; 44 import android.view.KeyCharacterMap; 45 import android.view.KeyEvent; 46 import android.view.accessibility.AccessibilityEvent; 47 import android.view.accessibility.AccessibilityNodeInfo; 48 import android.view.accessibility.AccessibilityWindowInfo; 49 50 import androidx.test.rule.ActivityTestRule; 51 52 import com.android.compatibility.common.util.TestUtils; 53 54 import java.util.Arrays; 55 import java.util.List; 56 import java.util.Objects; 57 import java.util.concurrent.TimeoutException; 58 import java.util.stream.Collectors; 59 60 /** 61 * Utilities useful when launching an activity to make sure it's all the way on the screen 62 * before we start testing it. 63 */ 64 public class ActivityLaunchUtils { 65 private static final String LOG_TAG = "ActivityLaunchUtils"; 66 private static final String AM_START_HOME_ACTIVITY_COMMAND = 67 "am start -a android.intent.action.MAIN -c android.intent.category.HOME"; 68 // Close the system dialogs for all users 69 public static final String AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND = 70 "am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS --user " + USER_ALL; 71 public static final String INPUT_KEYEVENT_KEYCODE_BACK = 72 "input keyevent KEYCODE_BACK"; 73 public static final String INPUT_KEYEVENT_KEYCODE_MENU = 74 "input keyevent KEYCODE_MENU"; 75 76 // Precision when asserting the launched activity bounds equals the reported a11y window bounds. 77 private static final int BOUNDS_PRECISION_PX = 1; 78 79 // Using a static variable so it can be used in lambdas. Not preserving state in it. 80 private static Activity mTempActivity; 81 launchActivityAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, ActivityTestRule<T> rule)82 public static <T extends Activity> T launchActivityAndWaitForItToBeOnscreen( 83 Instrumentation instrumentation, UiAutomation uiAutomation, 84 ActivityTestRule<T> rule) throws Exception { 85 ActivityLauncher activityLauncher = new ActivityLauncher() { 86 @Override 87 Activity launchActivity() { 88 return rule.launchActivity(null); 89 } 90 }; 91 return launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(instrumentation, 92 uiAutomation, activityLauncher, Display.DEFAULT_DISPLAY); 93 } 94 95 /** 96 * Launches an activity on a specified display and waits for it to be onscreen. 97 * 98 * <p>This method creates an intent to launch the specified activity class on the given display 99 * ID. It ensures that the activity is launched in a new task and clears any existing task that 100 * may be associated with the activity. The method also handles shell permissions required for 101 * launching the activity.</p> 102 * 103 * @param instrumentation The instrumentation instance used to start the activity. 104 * @param uiAutomation The UiAutomation instance used to manage UI interactions. 105 * @param clazz The class of the activity to be launched. 106 * @param displayId The ID of the display on which the activity should be launched. 107 * @return An instance of the launched activity. 108 * @throws Exception If there is an error launching the activity or waiting for it. 109 */ launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, Class<T> clazz, int displayId)110 public static <T extends Activity> T launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( 111 Instrumentation instrumentation, UiAutomation uiAutomation, Class<T> clazz, 112 int displayId) throws Exception { 113 final ActivityOptions options = ActivityOptions.makeBasic(); 114 options.setLaunchDisplayId(displayId); 115 final Intent intent = new Intent(instrumentation.getTargetContext(), clazz); 116 // Add clear task because this activity may on other display. 117 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); 118 119 ActivityLauncher activityLauncher = new ActivityLauncher() { 120 @Override 121 Activity launchActivity() { 122 uiAutomation.adoptShellPermissionIdentity(); 123 try { 124 return instrumentation.startActivitySync(intent, options.toBundle()); 125 } finally { 126 uiAutomation.dropShellPermissionIdentity(); 127 } 128 } 129 }; 130 return launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(instrumentation, 131 uiAutomation, activityLauncher, displayId); 132 } 133 getActivityTitle( Instrumentation instrumentation, Activity activity)134 public static CharSequence getActivityTitle( 135 Instrumentation instrumentation, Activity activity) { 136 final StringBuilder titleBuilder = new StringBuilder(); 137 instrumentation.runOnMainSync(() -> titleBuilder.append(activity.getTitle())); 138 return titleBuilder; 139 } 140 findWindowByTitle( UiAutomation uiAutomation, CharSequence title)141 public static AccessibilityWindowInfo findWindowByTitle( 142 UiAutomation uiAutomation, CharSequence title) { 143 final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows(); 144 return findWindowByTitleWithList(title, windows); 145 } 146 findWindowByTitleAndDisplay( UiAutomation uiAutomation, CharSequence title, int displayId)147 public static AccessibilityWindowInfo findWindowByTitleAndDisplay( 148 UiAutomation uiAutomation, CharSequence title, int displayId) { 149 final SparseArray<List<AccessibilityWindowInfo>> allWindows = 150 uiAutomation.getWindowsOnAllDisplays(); 151 final List<AccessibilityWindowInfo> windowsOfDisplay = allWindows.get(displayId); 152 return findWindowByTitleWithList(title, windowsOfDisplay); 153 } 154 homeScreenOrBust(Context context, UiAutomation uiAutomation)155 public static void homeScreenOrBust(Context context, UiAutomation uiAutomation) { 156 wakeUpOrBust(context, uiAutomation); 157 if (context.getPackageManager().isInstantApp()) return; 158 if (isHomeScreenShowing(context, uiAutomation)) return; 159 final AccessibilityServiceInfo serviceInfo = uiAutomation.getServiceInfo(); 160 final int enabledFlags = serviceInfo.flags; 161 // Make sure we could query windows. 162 serviceInfo.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; 163 uiAutomation.setServiceInfo(serviceInfo); 164 try { 165 KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class); 166 if (keyguardManager != null) { 167 TestUtils.waitUntil("Screen is unlocked", 168 (int) DEFAULT_TIMEOUT_MS / 1000, 169 () -> { 170 if (!keyguardManager.isKeyguardLocked()) { 171 return true; 172 } 173 execShellCommand(uiAutomation, INPUT_KEYEVENT_KEYCODE_MENU); 174 return false; 175 }); 176 } 177 execShellCommand(uiAutomation, AM_START_HOME_ACTIVITY_COMMAND); 178 execShellCommand(uiAutomation, AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND); 179 execShellCommand(uiAutomation, INPUT_KEYEVENT_KEYCODE_BACK); 180 TestUtils.waitUntil("Home screen is showing", 181 (int) DEFAULT_TIMEOUT_MS / 1000, 182 () -> { 183 if (isHomeScreenShowing(context, uiAutomation)) { 184 return true; 185 } 186 // Attempt to close any newly-appeared system dialogs which can prevent the 187 // home screen activity from becoming visible, active, and focused. 188 execShellCommand(uiAutomation, AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND); 189 execShellCommand(uiAutomation, INPUT_KEYEVENT_KEYCODE_BACK); 190 execShellCommand(uiAutomation, AM_START_HOME_ACTIVITY_COMMAND); 191 return false; 192 }); 193 } catch (Exception error) { 194 Log.e(LOG_TAG, "Timed out looking for home screen. Dumping window list"); 195 final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows(); 196 if (windows == null) { 197 Log.e(LOG_TAG, "Window list is null"); 198 } else if (windows.isEmpty()) { 199 Log.e(LOG_TAG, "Window list is empty"); 200 } else { 201 for (AccessibilityWindowInfo window : windows) { 202 Log.e(LOG_TAG, window.toString()); 203 } 204 } 205 206 fail("Unable to reach home screen"); 207 } finally { 208 serviceInfo.flags = enabledFlags; 209 uiAutomation.setServiceInfo(serviceInfo); 210 } 211 } 212 supportsMultiDisplay(Context context)213 public static boolean supportsMultiDisplay(Context context) { 214 return context.getPackageManager().hasSystemFeature( 215 FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS); 216 } 217 isHomeScreenShowing(Context context, UiAutomation uiAutomation)218 public static boolean isHomeScreenShowing(Context context, UiAutomation uiAutomation) { 219 final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows(); 220 final PackageManager packageManager = context.getPackageManager(); 221 final List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities( 222 new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME), 223 PackageManager.MATCH_DEFAULT_ONLY); 224 final boolean isAuto = isAutomotive(context); 225 226 // Look for an active focused window with a package name that matches 227 // the default home screen. 228 for (AccessibilityWindowInfo window : windows) { 229 if (!isAuto) { 230 // Auto does not set its home screen app as active+focused, so only non-auto 231 // devices enforce that the home screen is active+focused. 232 if (!window.isActive() || !window.isFocused()) { 233 continue; 234 } 235 } 236 final AccessibilityNodeInfo root = window.getRoot(); 237 if (root != null) { 238 final CharSequence packageName = root.getPackageName(); 239 if (packageName != null) { 240 for (ResolveInfo resolveInfo : resolveInfos) { 241 if ((resolveInfo.activityInfo != null) 242 && packageName.equals(resolveInfo.activityInfo.packageName)) { 243 return true; 244 } 245 } 246 } 247 } 248 } 249 // List unexpected package names of default home screen that invoking ResolverActivity 250 final CharSequence homePackageNames = resolveInfos.stream() 251 .map(r -> r.activityInfo).filter(Objects::nonNull) 252 .map(a -> a.packageName).collect(Collectors.joining(", ")); 253 Log.v(LOG_TAG, "No window matched with package names of home screen: " + homePackageNames); 254 return false; 255 } 256 wakeUpOrBust(Context context, UiAutomation uiAutomation)257 private static void wakeUpOrBust(Context context, UiAutomation uiAutomation) { 258 final long deadlineUptimeMillis = SystemClock.uptimeMillis() + DEFAULT_TIMEOUT_MS; 259 final PowerManager powerManager = context.getSystemService(PowerManager.class); 260 do { 261 if (powerManager.isInteractive()) { 262 Log.d(LOG_TAG, "Device is interactive"); 263 return; 264 } 265 266 Log.d(LOG_TAG, "Sending wakeup keycode"); 267 final long eventTime = SystemClock.uptimeMillis(); 268 uiAutomation.injectInputEvent( 269 new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, 270 KeyEvent.KEYCODE_WAKEUP, 0 /* repeat */, 0 /* metastate */, 271 KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 0 /* flags */, 272 InputDevice.SOURCE_KEYBOARD), true /* sync */); 273 uiAutomation.injectInputEvent( 274 new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, 275 KeyEvent.KEYCODE_WAKEUP, 0 /* repeat */, 0 /* metastate */, 276 KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 0 /* flags */, 277 InputDevice.SOURCE_KEYBOARD), true /* sync */); 278 try { 279 Thread.sleep(50); 280 } catch (InterruptedException e) { 281 } 282 } while (SystemClock.uptimeMillis() < deadlineUptimeMillis); 283 fail("Unable to wake up screen"); 284 } 285 launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, ActivityLauncher activityLauncher, int displayId)286 private static <T extends Activity> T launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( 287 Instrumentation instrumentation, UiAutomation uiAutomation, 288 ActivityLauncher activityLauncher, int displayId) throws Exception { 289 final int[] location = new int[2]; 290 final StringBuilder activityPackage = new StringBuilder(); 291 final Rect bounds = new Rect(); 292 final StringBuilder activityTitle = new StringBuilder(); 293 final StringBuilder timeoutExceptionRecords = new StringBuilder(); 294 // Make sure we get window events, so we'll know when the window appears 295 AccessibilityServiceInfo info = uiAutomation.getServiceInfo(); 296 info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; 297 uiAutomation.setServiceInfo(info); 298 // There is no any window on virtual display even doing GLOBAL_ACTION_HOME, so only 299 // checking the home screen for default display. 300 if (displayId == Display.DEFAULT_DISPLAY) { 301 homeScreenOrBust(instrumentation.getContext(), uiAutomation); 302 } 303 304 try { 305 final AccessibilityEvent awaitedEvent = uiAutomation.executeAndWaitForEvent( 306 () -> { 307 mTempActivity = activityLauncher.launchActivity(); 308 instrumentation.runOnMainSync(() -> { 309 mTempActivity.getWindow().getDecorView().getLocationOnScreen(location); 310 activityPackage.append(mTempActivity.getPackageName()); 311 }); 312 instrumentation.waitForIdleSync(); 313 activityTitle.append(getActivityTitle(instrumentation, mTempActivity)); 314 }, 315 (event) -> { 316 final AccessibilityWindowInfo window = 317 findWindowByTitleAndDisplay(uiAutomation, activityTitle, displayId); 318 if (window == null || window.getRoot() == null 319 // Ignore the active & focused check for virtual displays, 320 // which don't get focused on launch. 321 || (displayId == Display.DEFAULT_DISPLAY 322 && (!window.isActive() || !window.isFocused()))) { 323 // Attempt to close any system dialogs which can prevent the launched 324 // activity from becoming visible, active, and focused. 325 execShellCommand(uiAutomation, 326 AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND); 327 return false; 328 } 329 330 window.getBoundsInScreen(bounds); 331 mTempActivity.getWindow().getDecorView().getLocationOnScreen(location); 332 333 // Stores the related information including event, location and window 334 // as a timeout exception record. 335 timeoutExceptionRecords.append(String.format("{Received event: %s \n" 336 + "Window location: %s \nA11y window: %s}\n", 337 event, Arrays.toString(location), window)); 338 339 return (!bounds.isEmpty()) 340 && Math.abs(bounds.left - location[0]) <= BOUNDS_PRECISION_PX 341 && Math.abs(bounds.top - location[1]) <= BOUNDS_PRECISION_PX; 342 }, DEFAULT_TIMEOUT_MS); 343 assertNotNull(awaitedEvent); 344 } catch (TimeoutException timeout) { 345 throw new TimeoutException(timeout.getMessage() + "\n\nTimeout exception records : \n" 346 + timeoutExceptionRecords); 347 } 348 instrumentation.waitForIdleSync(); 349 return (T) mTempActivity; 350 } 351 findWindowByTitleWithList(CharSequence title, List<AccessibilityWindowInfo> windows)352 public static AccessibilityWindowInfo findWindowByTitleWithList(CharSequence title, 353 List<AccessibilityWindowInfo> windows) { 354 AccessibilityWindowInfo returnValue = null; 355 if (windows != null && windows.size() > 0) { 356 for (int i = 0; i < windows.size(); i++) { 357 final AccessibilityWindowInfo window = windows.get(i); 358 if (TextUtils.equals(title, window.getTitle())) { 359 returnValue = window; 360 } else { 361 window.recycle(); 362 } 363 } 364 } 365 return returnValue; 366 } 367 368 private static abstract class ActivityLauncher { launchActivity()369 abstract Activity launchActivity(); 370 } 371 } 372