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