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 69 // Using a static variable so it can be used in lambdas. Not preserving state in it. 70 private static Activity mTempActivity; 71 launchActivityAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, ActivityTestRule<T> rule)72 public static <T extends Activity> T launchActivityAndWaitForItToBeOnscreen( 73 Instrumentation instrumentation, UiAutomation uiAutomation, 74 ActivityTestRule<T> rule) throws Exception { 75 ActivityLauncher activityLauncher = new ActivityLauncher() { 76 @Override 77 Activity launchActivity() { 78 return rule.launchActivity(null); 79 } 80 }; 81 return launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(instrumentation, 82 uiAutomation, activityLauncher, Display.DEFAULT_DISPLAY); 83 } 84 85 /** 86 * If this activity would be launched at virtual display, please finishes this activity before 87 * this test ended. Otherwise it will be displayed on default display and impacts the next test. 88 */ launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, Class<T> clazz, int displayId)89 public static <T extends Activity> T launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( 90 Instrumentation instrumentation, UiAutomation uiAutomation, Class<T> clazz, 91 int displayId) throws Exception { 92 final ActivityOptions options = ActivityOptions.makeBasic(); 93 options.setLaunchDisplayId(displayId); 94 final Intent intent = new Intent(instrumentation.getTargetContext(), clazz); 95 // Add clear task because this activity may on other display. 96 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK|Intent.FLAG_ACTIVITY_NEW_TASK); 97 98 ActivityLauncher activityLauncher = new ActivityLauncher() { 99 @Override 100 Activity launchActivity() { 101 uiAutomation.adoptShellPermissionIdentity(); 102 try { 103 return instrumentation.startActivitySync(intent, options.toBundle()); 104 } finally { 105 uiAutomation.dropShellPermissionIdentity(); 106 } 107 } 108 }; 109 return launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(instrumentation, 110 uiAutomation, activityLauncher, displayId); 111 } 112 getActivityTitle( Instrumentation instrumentation, Activity activity)113 public static CharSequence getActivityTitle( 114 Instrumentation instrumentation, Activity activity) { 115 final StringBuilder titleBuilder = new StringBuilder(); 116 instrumentation.runOnMainSync(() -> titleBuilder.append(activity.getTitle())); 117 return titleBuilder; 118 } 119 findWindowByTitle( UiAutomation uiAutomation, CharSequence title)120 public static AccessibilityWindowInfo findWindowByTitle( 121 UiAutomation uiAutomation, CharSequence title) { 122 final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows(); 123 return findWindowByTitleWithList(title, windows); 124 } 125 findWindowByTitleAndDisplay( UiAutomation uiAutomation, CharSequence title, int displayId)126 public static AccessibilityWindowInfo findWindowByTitleAndDisplay( 127 UiAutomation uiAutomation, CharSequence title, int displayId) { 128 final SparseArray<List<AccessibilityWindowInfo>> allWindows = 129 uiAutomation.getWindowsOnAllDisplays(); 130 final List<AccessibilityWindowInfo> windowsOfDisplay = allWindows.get(displayId); 131 return findWindowByTitleWithList(title, windowsOfDisplay); 132 } 133 homeScreenOrBust(Context context, UiAutomation uiAutomation)134 public static void homeScreenOrBust(Context context, UiAutomation uiAutomation) { 135 wakeUpOrBust(context, uiAutomation); 136 if (context.getPackageManager().isInstantApp()) return; 137 if (isHomeScreenShowing(context, uiAutomation)) return; 138 try { 139 executeAndWaitOn( 140 uiAutomation, 141 () -> { 142 execShellCommand(uiAutomation, AM_START_HOME_ACTIVITY_COMMAND); 143 execShellCommand(uiAutomation, AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND); 144 }, 145 () -> isHomeScreenShowing(context, uiAutomation), 146 DEFAULT_TIMEOUT_MS, 147 "home screen"); 148 } catch (AssertionError error) { 149 Log.e(LOG_TAG, "Timed out looking for home screen. Dumping window list"); 150 final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows(); 151 if (windows == null) { 152 Log.e(LOG_TAG, "Window list is null"); 153 } else if (windows.isEmpty()) { 154 Log.e(LOG_TAG, "Window list is empty"); 155 } else { 156 for (AccessibilityWindowInfo window : windows) { 157 Log.e(LOG_TAG, window.toString()); 158 } 159 } 160 161 fail("Unable to reach home screen"); 162 } 163 } 164 supportsMultiDisplay(Context context)165 public static boolean supportsMultiDisplay(Context context) { 166 return context.getPackageManager().hasSystemFeature( 167 FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS); 168 } 169 isHomeScreenShowing(Context context, UiAutomation uiAutomation)170 private static boolean isHomeScreenShowing(Context context, UiAutomation uiAutomation) { 171 final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows(); 172 final PackageManager packageManager = context.getPackageManager(); 173 final List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities( 174 new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME), 175 PackageManager.MATCH_DEFAULT_ONLY); 176 177 // Look for a window with a package name that matches the default home screen 178 for (AccessibilityWindowInfo window : windows) { 179 final AccessibilityNodeInfo root = window.getRoot(); 180 if (root != null) { 181 final CharSequence packageName = root.getPackageName(); 182 if (packageName != null) { 183 for (ResolveInfo resolveInfo : resolveInfos) { 184 if ((resolveInfo.activityInfo != null) 185 && packageName.equals(resolveInfo.activityInfo.packageName)) { 186 return true; 187 } 188 } 189 } 190 } 191 } 192 // List unexpected package names of default home screen that invoking ResolverActivity 193 final CharSequence homePackageNames = resolveInfos.stream() 194 .map(r -> r.activityInfo).filter(Objects::nonNull) 195 .map(a -> a.packageName).collect(Collectors.joining(", ")); 196 Log.v(LOG_TAG, "No window matched with package names of home screen: " + homePackageNames); 197 return false; 198 } 199 wakeUpOrBust(Context context, UiAutomation uiAutomation)200 private static void wakeUpOrBust(Context context, UiAutomation uiAutomation) { 201 final long deadlineUptimeMillis = SystemClock.uptimeMillis() + DEFAULT_TIMEOUT_MS; 202 final PowerManager powerManager = context.getSystemService(PowerManager.class); 203 do { 204 if (powerManager.isInteractive()) { 205 Log.d(LOG_TAG, "Device is interactive"); 206 return; 207 } 208 209 Log.d(LOG_TAG, "Sending wakeup keycode"); 210 final long eventTime = SystemClock.uptimeMillis(); 211 uiAutomation.injectInputEvent( 212 new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, 213 KeyEvent.KEYCODE_WAKEUP, 0 /* repeat */, 0 /* metastate */, 214 KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 0 /* flags */, 215 InputDevice.SOURCE_KEYBOARD), true /* sync */); 216 uiAutomation.injectInputEvent( 217 new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, 218 KeyEvent.KEYCODE_WAKEUP, 0 /* repeat */, 0 /* metastate */, 219 KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 0 /* flags */, 220 InputDevice.SOURCE_KEYBOARD), true /* sync */); 221 try { 222 Thread.sleep(50); 223 } catch (InterruptedException e) {} 224 } while (SystemClock.uptimeMillis() < deadlineUptimeMillis); 225 fail("Unable to wake up screen"); 226 } 227 228 /** 229 * Executes a command and waits for a specified condition up to a given wait timeout. It checks 230 * condition result each time when events delivered, and throws exception if the condition 231 * result is not {@code true} within the given timeout. 232 */ executeAndWaitOn(UiAutomation uiAutomation, Runnable command, BooleanSupplier condition, long timeoutMillis, String conditionName)233 private static void executeAndWaitOn(UiAutomation uiAutomation, Runnable command, 234 BooleanSupplier condition, long timeoutMillis, String conditionName) { 235 final Object waitObject = new Object(); 236 final long executionStartTimeMillis = SystemClock.uptimeMillis(); 237 try { 238 uiAutomation.setOnAccessibilityEventListener((event) -> { 239 if (event.getEventTime() < executionStartTimeMillis) { 240 return; 241 } 242 synchronized (waitObject) { 243 waitObject.notifyAll(); 244 } 245 }); 246 command.run(); 247 TestUtils.waitOn(waitObject, condition, timeoutMillis, conditionName); 248 } finally { 249 uiAutomation.setOnAccessibilityEventListener(null); 250 } 251 } 252 launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, ActivityLauncher activityLauncher, int displayId)253 private static <T extends Activity> T launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( 254 Instrumentation instrumentation, UiAutomation uiAutomation, 255 ActivityLauncher activityLauncher, int displayId) throws Exception { 256 final int[] location = new int[2]; 257 final StringBuilder activityPackage = new StringBuilder(); 258 final Rect bounds = new Rect(); 259 final StringBuilder activityTitle = new StringBuilder(); 260 final StringBuilder timeoutExceptionRecords = new StringBuilder(); 261 // Make sure we get window events, so we'll know when the window appears 262 AccessibilityServiceInfo info = uiAutomation.getServiceInfo(); 263 info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; 264 uiAutomation.setServiceInfo(info); 265 // There is no any window on virtual display even doing GLOBAL_ACTION_HOME, so only 266 // checking the home screen for default display. 267 if (displayId == Display.DEFAULT_DISPLAY) { 268 homeScreenOrBust(instrumentation.getContext(), uiAutomation); 269 } 270 271 try { 272 final AccessibilityEvent awaitedEvent = uiAutomation.executeAndWaitForEvent( 273 () -> { 274 mTempActivity = activityLauncher.launchActivity(); 275 instrumentation.runOnMainSync(() -> { 276 mTempActivity.getWindow().getDecorView().getLocationOnScreen(location); 277 activityPackage.append(mTempActivity.getPackageName()); 278 }); 279 instrumentation.waitForIdleSync(); 280 activityTitle.append(getActivityTitle(instrumentation, mTempActivity)); 281 }, 282 (event) -> { 283 final AccessibilityWindowInfo window = 284 findWindowByTitleAndDisplay(uiAutomation, activityTitle, displayId); 285 if (window == null) return false; 286 if (window.getRoot() == null) return false; 287 288 window.getBoundsInScreen(bounds); 289 mTempActivity.getWindow().getDecorView().getLocationOnScreen(location); 290 291 // Stores the related information including event, location and window 292 // as a timeout exception record. 293 timeoutExceptionRecords.append(String.format("{Received event: %s \n" 294 + "Window location: %s \nA11y window: %s}\n", 295 event, Arrays.toString(location), window)); 296 297 return (!bounds.isEmpty()) 298 && (bounds.left == location[0]) && (bounds.top == location[1]); 299 }, DEFAULT_TIMEOUT_MS); 300 assertNotNull(awaitedEvent); 301 } catch (TimeoutException timeout) { 302 throw new TimeoutException(timeout.getMessage() + "\n\nTimeout exception records : \n" 303 + timeoutExceptionRecords); 304 } 305 return (T) mTempActivity; 306 } 307 findWindowByTitleWithList(CharSequence title, List<AccessibilityWindowInfo> windows)308 private static AccessibilityWindowInfo findWindowByTitleWithList(CharSequence title, 309 List<AccessibilityWindowInfo> windows) { 310 AccessibilityWindowInfo returnValue = null; 311 if (windows != null && windows.size() > 0) { 312 for (int i = 0; i < windows.size(); i++) { 313 final AccessibilityWindowInfo window = windows.get(i); 314 if (TextUtils.equals(title, window.getTitle())) { 315 returnValue = window; 316 } else { 317 window.recycle(); 318 } 319 } 320 } 321 return returnValue; 322 } 323 324 private static abstract class ActivityLauncher { launchActivity()325 abstract Activity launchActivity(); 326 } 327 } 328