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 * If this activity would be launched at virtual display, please finishes this activity before 97 * this test ended. Otherwise it will be displayed on default display and impacts the next test. 98 */ launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, Class<T> clazz, int displayId)99 public static <T extends Activity> T launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( 100 Instrumentation instrumentation, UiAutomation uiAutomation, Class<T> clazz, 101 int displayId) throws Exception { 102 final ActivityOptions options = ActivityOptions.makeBasic(); 103 options.setLaunchDisplayId(displayId); 104 final Intent intent = new Intent(instrumentation.getTargetContext(), clazz); 105 // Add clear task because this activity may on other display. 106 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); 107 108 ActivityLauncher activityLauncher = new ActivityLauncher() { 109 @Override 110 Activity launchActivity() { 111 uiAutomation.adoptShellPermissionIdentity(); 112 try { 113 return instrumentation.startActivitySync(intent, options.toBundle()); 114 } finally { 115 uiAutomation.dropShellPermissionIdentity(); 116 } 117 } 118 }; 119 return launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(instrumentation, 120 uiAutomation, activityLauncher, displayId); 121 } 122 getActivityTitle( Instrumentation instrumentation, Activity activity)123 public static CharSequence getActivityTitle( 124 Instrumentation instrumentation, Activity activity) { 125 final StringBuilder titleBuilder = new StringBuilder(); 126 instrumentation.runOnMainSync(() -> titleBuilder.append(activity.getTitle())); 127 return titleBuilder; 128 } 129 findWindowByTitle( UiAutomation uiAutomation, CharSequence title)130 public static AccessibilityWindowInfo findWindowByTitle( 131 UiAutomation uiAutomation, CharSequence title) { 132 final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows(); 133 return findWindowByTitleWithList(title, windows); 134 } 135 findWindowByTitleAndDisplay( UiAutomation uiAutomation, CharSequence title, int displayId)136 public static AccessibilityWindowInfo findWindowByTitleAndDisplay( 137 UiAutomation uiAutomation, CharSequence title, int displayId) { 138 final SparseArray<List<AccessibilityWindowInfo>> allWindows = 139 uiAutomation.getWindowsOnAllDisplays(); 140 final List<AccessibilityWindowInfo> windowsOfDisplay = allWindows.get(displayId); 141 return findWindowByTitleWithList(title, windowsOfDisplay); 142 } 143 homeScreenOrBust(Context context, UiAutomation uiAutomation)144 public static void homeScreenOrBust(Context context, UiAutomation uiAutomation) { 145 wakeUpOrBust(context, uiAutomation); 146 if (context.getPackageManager().isInstantApp()) return; 147 if (isHomeScreenShowing(context, uiAutomation)) return; 148 final AccessibilityServiceInfo serviceInfo = uiAutomation.getServiceInfo(); 149 final int enabledFlags = serviceInfo.flags; 150 // Make sure we could query windows. 151 serviceInfo.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; 152 uiAutomation.setServiceInfo(serviceInfo); 153 try { 154 KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class); 155 if (keyguardManager != null) { 156 TestUtils.waitUntil("Screen is unlocked", 157 (int) DEFAULT_TIMEOUT_MS / 1000, 158 () -> { 159 if (!keyguardManager.isKeyguardLocked()) { 160 return true; 161 } 162 execShellCommand(uiAutomation, INPUT_KEYEVENT_KEYCODE_MENU); 163 return false; 164 }); 165 } 166 execShellCommand(uiAutomation, AM_START_HOME_ACTIVITY_COMMAND); 167 execShellCommand(uiAutomation, AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND); 168 execShellCommand(uiAutomation, INPUT_KEYEVENT_KEYCODE_BACK); 169 TestUtils.waitUntil("Home screen is showing", 170 (int) DEFAULT_TIMEOUT_MS / 1000, 171 () -> { 172 if (isHomeScreenShowing(context, uiAutomation)) { 173 return true; 174 } 175 // Attempt to close any newly-appeared system dialogs which can prevent the 176 // home screen activity from becoming visible, active, and focused. 177 execShellCommand(uiAutomation, AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND); 178 execShellCommand(uiAutomation, INPUT_KEYEVENT_KEYCODE_BACK); 179 execShellCommand(uiAutomation, AM_START_HOME_ACTIVITY_COMMAND); 180 return false; 181 }); 182 } catch (Exception error) { 183 Log.e(LOG_TAG, "Timed out looking for home screen. Dumping window list"); 184 final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows(); 185 if (windows == null) { 186 Log.e(LOG_TAG, "Window list is null"); 187 } else if (windows.isEmpty()) { 188 Log.e(LOG_TAG, "Window list is empty"); 189 } else { 190 for (AccessibilityWindowInfo window : windows) { 191 Log.e(LOG_TAG, window.toString()); 192 } 193 } 194 195 fail("Unable to reach home screen"); 196 } finally { 197 serviceInfo.flags = enabledFlags; 198 uiAutomation.setServiceInfo(serviceInfo); 199 } 200 } 201 supportsMultiDisplay(Context context)202 public static boolean supportsMultiDisplay(Context context) { 203 return context.getPackageManager().hasSystemFeature( 204 FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS); 205 } 206 isHomeScreenShowing(Context context, UiAutomation uiAutomation)207 public static boolean isHomeScreenShowing(Context context, UiAutomation uiAutomation) { 208 final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows(); 209 final PackageManager packageManager = context.getPackageManager(); 210 final List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities( 211 new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME), 212 PackageManager.MATCH_DEFAULT_ONLY); 213 final boolean isAuto = isAutomotive(context); 214 215 // Look for an active focused window with a package name that matches 216 // the default home screen. 217 for (AccessibilityWindowInfo window : windows) { 218 if (!isAuto) { 219 // Auto does not set its home screen app as active+focused, so only non-auto 220 // devices enforce that the home screen is active+focused. 221 if (!window.isActive() || !window.isFocused()) { 222 continue; 223 } 224 } 225 final AccessibilityNodeInfo root = window.getRoot(); 226 if (root != null) { 227 final CharSequence packageName = root.getPackageName(); 228 if (packageName != null) { 229 for (ResolveInfo resolveInfo : resolveInfos) { 230 if ((resolveInfo.activityInfo != null) 231 && packageName.equals(resolveInfo.activityInfo.packageName)) { 232 return true; 233 } 234 } 235 } 236 } 237 } 238 // List unexpected package names of default home screen that invoking ResolverActivity 239 final CharSequence homePackageNames = resolveInfos.stream() 240 .map(r -> r.activityInfo).filter(Objects::nonNull) 241 .map(a -> a.packageName).collect(Collectors.joining(", ")); 242 Log.v(LOG_TAG, "No window matched with package names of home screen: " + homePackageNames); 243 return false; 244 } 245 wakeUpOrBust(Context context, UiAutomation uiAutomation)246 private static void wakeUpOrBust(Context context, UiAutomation uiAutomation) { 247 final long deadlineUptimeMillis = SystemClock.uptimeMillis() + DEFAULT_TIMEOUT_MS; 248 final PowerManager powerManager = context.getSystemService(PowerManager.class); 249 do { 250 if (powerManager.isInteractive()) { 251 Log.d(LOG_TAG, "Device is interactive"); 252 return; 253 } 254 255 Log.d(LOG_TAG, "Sending wakeup keycode"); 256 final long eventTime = SystemClock.uptimeMillis(); 257 uiAutomation.injectInputEvent( 258 new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, 259 KeyEvent.KEYCODE_WAKEUP, 0 /* repeat */, 0 /* metastate */, 260 KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 0 /* flags */, 261 InputDevice.SOURCE_KEYBOARD), true /* sync */); 262 uiAutomation.injectInputEvent( 263 new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, 264 KeyEvent.KEYCODE_WAKEUP, 0 /* repeat */, 0 /* metastate */, 265 KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 0 /* flags */, 266 InputDevice.SOURCE_KEYBOARD), true /* sync */); 267 try { 268 Thread.sleep(50); 269 } catch (InterruptedException e) { 270 } 271 } while (SystemClock.uptimeMillis() < deadlineUptimeMillis); 272 fail("Unable to wake up screen"); 273 } 274 launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, ActivityLauncher activityLauncher, int displayId)275 private static <T extends Activity> T launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( 276 Instrumentation instrumentation, UiAutomation uiAutomation, 277 ActivityLauncher activityLauncher, int displayId) throws Exception { 278 final int[] location = new int[2]; 279 final StringBuilder activityPackage = new StringBuilder(); 280 final Rect bounds = new Rect(); 281 final StringBuilder activityTitle = new StringBuilder(); 282 final StringBuilder timeoutExceptionRecords = new StringBuilder(); 283 // Make sure we get window events, so we'll know when the window appears 284 AccessibilityServiceInfo info = uiAutomation.getServiceInfo(); 285 info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; 286 uiAutomation.setServiceInfo(info); 287 // There is no any window on virtual display even doing GLOBAL_ACTION_HOME, so only 288 // checking the home screen for default display. 289 if (displayId == Display.DEFAULT_DISPLAY) { 290 homeScreenOrBust(instrumentation.getContext(), uiAutomation); 291 } 292 293 try { 294 final AccessibilityEvent awaitedEvent = uiAutomation.executeAndWaitForEvent( 295 () -> { 296 mTempActivity = activityLauncher.launchActivity(); 297 instrumentation.runOnMainSync(() -> { 298 mTempActivity.getWindow().getDecorView().getLocationOnScreen(location); 299 activityPackage.append(mTempActivity.getPackageName()); 300 }); 301 instrumentation.waitForIdleSync(); 302 activityTitle.append(getActivityTitle(instrumentation, mTempActivity)); 303 }, 304 (event) -> { 305 final AccessibilityWindowInfo window = 306 findWindowByTitleAndDisplay(uiAutomation, activityTitle, displayId); 307 if (window == null || window.getRoot() == null 308 // Ignore the active & focused check for virtual displays, 309 // which don't get focused on launch. 310 || (displayId == Display.DEFAULT_DISPLAY 311 && (!window.isActive() || !window.isFocused()))) { 312 // Attempt to close any system dialogs which can prevent the launched 313 // activity from becoming visible, active, and focused. 314 execShellCommand(uiAutomation, 315 AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND); 316 return false; 317 } 318 319 window.getBoundsInScreen(bounds); 320 mTempActivity.getWindow().getDecorView().getLocationOnScreen(location); 321 322 // Stores the related information including event, location and window 323 // as a timeout exception record. 324 timeoutExceptionRecords.append(String.format("{Received event: %s \n" 325 + "Window location: %s \nA11y window: %s}\n", 326 event, Arrays.toString(location), window)); 327 328 return (!bounds.isEmpty()) 329 && Math.abs(bounds.left - location[0]) <= BOUNDS_PRECISION_PX 330 && Math.abs(bounds.top - location[1]) <= BOUNDS_PRECISION_PX; 331 }, DEFAULT_TIMEOUT_MS); 332 assertNotNull(awaitedEvent); 333 } catch (TimeoutException timeout) { 334 throw new TimeoutException(timeout.getMessage() + "\n\nTimeout exception records : \n" 335 + timeoutExceptionRecords); 336 } 337 instrumentation.waitForIdleSync(); 338 return (T) mTempActivity; 339 } 340 findWindowByTitleWithList(CharSequence title, List<AccessibilityWindowInfo> windows)341 public static AccessibilityWindowInfo findWindowByTitleWithList(CharSequence title, 342 List<AccessibilityWindowInfo> windows) { 343 AccessibilityWindowInfo returnValue = null; 344 if (windows != null && windows.size() > 0) { 345 for (int i = 0; i < windows.size(); i++) { 346 final AccessibilityWindowInfo window = windows.get(i); 347 if (TextUtils.equals(title, window.getTitle())) { 348 returnValue = window; 349 } else { 350 window.recycle(); 351 } 352 } 353 } 354 return returnValue; 355 } 356 357 private static abstract class ActivityLauncher { launchActivity()358 abstract Activity launchActivity(); 359 } 360 } 361