1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package android.server.am; 18 19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; 20 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; 21 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; 22 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; 23 import static android.server.am.ComponentNameUtils.getActivityName; 24 import static android.server.am.ComponentNameUtils.getWindowName; 25 import static android.server.am.StateLogger.log; 26 import static android.server.am.StateLogger.logAlways; 27 import static android.server.am.StateLogger.logE; 28 import static android.server.am.UiDeviceUtils.pressBackButton; 29 import static android.server.am.UiDeviceUtils.pressEnterButton; 30 import static android.server.am.UiDeviceUtils.pressHomeButton; 31 import static android.server.am.UiDeviceUtils.pressSleepButton; 32 import static android.server.am.UiDeviceUtils.pressUnlockButton; 33 import static android.server.am.UiDeviceUtils.pressWakeupButton; 34 import static android.server.am.UiDeviceUtils.waitForDeviceIdle; 35 36 import android.accessibilityservice.AccessibilityService; 37 import android.app.ActivityManager; 38 import android.app.ActivityTaskManager; 39 import android.content.ComponentName; 40 import android.content.Context; 41 import android.os.SystemClock; 42 43 import androidx.test.InstrumentationRegistry; 44 45 import com.android.compatibility.common.util.SystemUtil; 46 47 import org.junit.After; 48 import org.junit.Before; 49 50 import java.io.IOException; 51 import java.util.UUID; 52 import java.util.concurrent.TimeUnit; 53 import java.util.regex.Matcher; 54 import java.util.regex.Pattern; 55 56 public abstract class ActivityManagerTestBase { 57 protected static final int[] ALL_ACTIVITY_TYPE_BUT_HOME = { 58 ACTIVITY_TYPE_STANDARD, ACTIVITY_TYPE_ASSISTANT, ACTIVITY_TYPE_RECENTS, 59 ACTIVITY_TYPE_UNDEFINED 60 }; 61 62 private static final String TASK_ID_PREFIX = "taskId"; 63 64 private static final String AM_STACK_LIST = "am stack list"; 65 66 private static final String AM_FORCE_STOP_TEST_PACKAGE = "am force-stop android.server.am"; 67 private static final String AM_FORCE_STOP_SECOND_TEST_PACKAGE 68 = "am force-stop android.server.am.second"; 69 private static final String AM_FORCE_STOP_THIRD_TEST_PACKAGE 70 = "am force-stop android.server.am.third"; 71 72 protected static final String AM_START_HOME_ACTIVITY_COMMAND = 73 "am start -a android.intent.action.MAIN -c android.intent.category.HOME"; 74 75 private static final String LOCK_CREDENTIAL = "1234"; 76 77 private static final int UI_MODE_TYPE_MASK = 0x0f; 78 private static final int UI_MODE_TYPE_VR_HEADSET = 0x07; 79 80 protected Context mContext; 81 protected ActivityManager mAm; 82 protected ActivityTaskManager mAtm; 83 84 /** 85 * @return the am command to start the given activity with the following extra key/value pairs. 86 * {@param keyValuePairs} must be a list of arguments defining each key/value extra. 87 */ 88 // TODO: Make this more generic, for instance accepting flags or extras of other types. getAmStartCmd(final ComponentName activityName, final String... keyValuePairs)89 protected static String getAmStartCmd(final ComponentName activityName, 90 final String... keyValuePairs) { 91 return getAmStartCmdInternal(getActivityName(activityName), keyValuePairs); 92 } 93 getAmStartCmdInternal(final String activityName, final String... keyValuePairs)94 private static String getAmStartCmdInternal(final String activityName, 95 final String... keyValuePairs) { 96 return appendKeyValuePairs( 97 new StringBuilder("am start -n ").append(activityName), 98 keyValuePairs); 99 } 100 appendKeyValuePairs( final StringBuilder cmd, final String... keyValuePairs)101 private static String appendKeyValuePairs( 102 final StringBuilder cmd, final String... keyValuePairs) { 103 if (keyValuePairs.length % 2 != 0) { 104 throw new RuntimeException("keyValuePairs must be pairs of key/value arguments"); 105 } 106 for (int i = 0; i < keyValuePairs.length; i += 2) { 107 final String key = keyValuePairs[i]; 108 final String value = keyValuePairs[i + 1]; 109 cmd.append(" --es ") 110 .append(key) 111 .append(" ") 112 .append(value); 113 } 114 return cmd.toString(); 115 } 116 117 protected ActivityAndWindowManagersState mAmWmState = new ActivityAndWindowManagersState(); 118 119 @Before setUp()120 public void setUp() throws Exception { 121 mContext = InstrumentationRegistry.getContext(); 122 mAm = mContext.getSystemService(ActivityManager.class); 123 mAtm = mContext.getSystemService(ActivityTaskManager.class); 124 125 pressWakeupButton(); 126 pressUnlockButton(); 127 pressHomeButton(); 128 } 129 130 @After tearDown()131 public void tearDown() throws Exception { 132 // Synchronous execution of removeStacksWithActivityTypes() ensures that all activities but 133 // home are cleaned up from the stack at the end of each test. Am force stop shell commands 134 // might be asynchronous and could interrupt the stack cleanup process if executed first. 135 executeShellCommand(AM_FORCE_STOP_TEST_PACKAGE); 136 executeShellCommand(AM_FORCE_STOP_SECOND_TEST_PACKAGE); 137 executeShellCommand(AM_FORCE_STOP_THIRD_TEST_PACKAGE); 138 pressHomeButton(); 139 } 140 executeShellCommand(String command)141 public static String executeShellCommand(String command) { 142 log("Shell command: " + command); 143 try { 144 return SystemUtil 145 .runShellCommand(InstrumentationRegistry.getInstrumentation(), command); 146 } catch (IOException e) { 147 //bubble it up 148 logE("Error running shell command: " + command); 149 throw new RuntimeException(e); 150 } 151 } 152 launchActivity(final ComponentName activityName, final String... keyValuePairs)153 protected void launchActivity(final ComponentName activityName, final String... keyValuePairs) { 154 executeShellCommand(getAmStartCmd(activityName, keyValuePairs)); 155 mAmWmState.waitForValidState(new WaitForValidActivityState(activityName)); 156 } 157 waitForIdle()158 private static void waitForIdle() { 159 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 160 } 161 launchHomeActivity()162 protected void launchHomeActivity() { 163 executeShellCommand(AM_START_HOME_ACTIVITY_COMMAND); 164 mAmWmState.waitForHomeActivityVisible(); 165 } 166 launchActivity(ComponentName activityName, int windowingMode, final String... keyValuePairs)167 protected void launchActivity(ComponentName activityName, int windowingMode, 168 final String... keyValuePairs) { 169 executeShellCommand(getAmStartCmd(activityName, keyValuePairs) 170 + " --windowingMode " + windowingMode); 171 mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName) 172 .setWindowingMode(windowingMode) 173 .build()); 174 } 175 setActivityTaskWindowingMode(ComponentName activityName, int windowingMode)176 protected void setActivityTaskWindowingMode(ComponentName activityName, int windowingMode) { 177 final int taskId = getActivityTaskId(activityName); 178 mAtm.setTaskWindowingMode(taskId, windowingMode, true /* toTop */); 179 mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName) 180 .setActivityType(ACTIVITY_TYPE_STANDARD) 181 .setWindowingMode(windowingMode) 182 .build()); 183 } 184 185 @Deprecated getActivityTaskId(final ComponentName activityName)186 protected int getActivityTaskId(final ComponentName activityName) { 187 final String windowName = getWindowName(activityName); 188 final String output = executeShellCommand(AM_STACK_LIST); 189 final Pattern activityPattern = Pattern.compile("(.*) " + windowName + " (.*)"); 190 for (final String line : output.split("\\n")) { 191 final Matcher matcher = activityPattern.matcher(line); 192 if (matcher.matches()) { 193 for (String word : line.split("\\s+")) { 194 if (word.startsWith(TASK_ID_PREFIX)) { 195 final String withColon = word.split("=")[1]; 196 return Integer.parseInt(withColon.substring(0, withColon.length() - 1)); 197 } 198 } 199 } 200 } 201 return -1; 202 } 203 isTablet()204 protected boolean isTablet() { 205 // Larger than approx 7" tablets 206 return mContext.getResources().getConfiguration().smallestScreenWidthDp >= 600; 207 } 208 209 // TODO: Switch to using a feature flag, when available. isUiModeLockedToVrHeadset()210 protected boolean isUiModeLockedToVrHeadset() { 211 final String output = runCommandAndPrintOutput("dumpsys uimode"); 212 213 Integer curUiMode = null; 214 Boolean uiModeLocked = null; 215 for (String line : output.split("\\n")) { 216 line = line.trim(); 217 Matcher matcher = sCurrentUiModePattern.matcher(line); 218 if (matcher.find()) { 219 curUiMode = Integer.parseInt(matcher.group(1), 16); 220 } 221 matcher = sUiModeLockedPattern.matcher(line); 222 if (matcher.find()) { 223 uiModeLocked = matcher.group(1).equals("true"); 224 } 225 } 226 227 boolean uiModeLockedToVrHeadset = (curUiMode != null) && (uiModeLocked != null) 228 && ((curUiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_VR_HEADSET) && uiModeLocked; 229 230 if (uiModeLockedToVrHeadset) { 231 log("UI mode is locked to VR headset"); 232 } 233 234 return uiModeLockedToVrHeadset; 235 } 236 supportsSplitScreenMultiWindow()237 protected boolean supportsSplitScreenMultiWindow() { 238 return ActivityTaskManager.supportsSplitScreenMultiWindow(mContext); 239 } 240 hasDeviceFeature(final String requiredFeature)241 protected boolean hasDeviceFeature(final String requiredFeature) { 242 return InstrumentationRegistry.getContext() 243 .getPackageManager() 244 .hasSystemFeature(requiredFeature); 245 } 246 isDisplayOn()247 protected boolean isDisplayOn() { 248 final String output = executeShellCommand("dumpsys power"); 249 final Matcher matcher = sDisplayStatePattern.matcher(output); 250 if (matcher.find()) { 251 final String state = matcher.group(1); 252 log("power state=" + state); 253 return "ON".equals(state); 254 } 255 logAlways("power state :("); 256 return false; 257 } 258 259 protected class LockScreenSession implements AutoCloseable { 260 private static final boolean DEBUG = false; 261 262 private final boolean mIsLockDisabled; 263 private boolean mLockCredentialSet; 264 LockScreenSession()265 public LockScreenSession() { 266 mIsLockDisabled = isLockDisabled(); 267 mLockCredentialSet = false; 268 // Enable lock screen (swipe) by default. 269 setLockDisabled(false); 270 } 271 setLockCredential()272 public LockScreenSession setLockCredential() { 273 mLockCredentialSet = true; 274 runCommandAndPrintOutput("locksettings set-pin " + LOCK_CREDENTIAL); 275 return this; 276 } 277 enterAndConfirmLockCredential()278 public LockScreenSession enterAndConfirmLockCredential() { 279 waitForDeviceIdle(3000); 280 281 runCommandAndPrintOutput("input text " + LOCK_CREDENTIAL); 282 pressEnterButton(); 283 return this; 284 } 285 removeLockCredential()286 private void removeLockCredential() { 287 runCommandAndPrintOutput("locksettings clear --old " + LOCK_CREDENTIAL); 288 mLockCredentialSet = false; 289 } 290 disableLockScreen()291 LockScreenSession disableLockScreen() { 292 setLockDisabled(true); 293 return this; 294 } 295 sleepDevice()296 public LockScreenSession sleepDevice() { 297 pressSleepButton(); 298 // Not all device variants lock when we go to sleep, so we need to explicitly lock the 299 // device. Note that pressSleepButton() above is redundant because the action also 300 // puts the device to sleep, but kept around for clarity. 301 InstrumentationRegistry.getInstrumentation().getUiAutomation().performGlobalAction( 302 AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN); 303 waitForDisplayStateWithRetry(false /* displayOn */ ); 304 return this; 305 } 306 wakeUpDevice()307 protected LockScreenSession wakeUpDevice() { 308 pressWakeupButton(); 309 waitForDisplayStateWithRetry(true /* displayOn */ ); 310 return this; 311 } 312 313 /** 314 * If the device has a PIN or password set, this doesn't immediately unlock the device. 315 * Instead, it shows the keyguard and brings the entry field into focus, ready for a 316 * call to enterAndConfirmLockCredential(). 317 */ unlockDevice()318 protected LockScreenSession unlockDevice() { 319 pressUnlockButton(); 320 SystemClock.sleep(200); 321 return this; 322 } 323 gotoKeyguard()324 public LockScreenSession gotoKeyguard() { 325 if (DEBUG && isLockDisabled()) { 326 logE("LockScreenSession.gotoKeyguard() is called without lock enabled."); 327 } 328 sleepDevice(); 329 wakeUpDevice(); 330 unlockDevice(); 331 mAmWmState.waitForKeyguardShowingAndNotOccluded(); 332 return this; 333 } 334 335 @Override close()336 public void close() throws Exception { 337 setLockDisabled(mIsLockDisabled); 338 if (mLockCredentialSet) { 339 removeLockCredential(); 340 } 341 342 // Dismiss active keyguard after credential is cleared, so keyguard doesn't ask for 343 // the stale credential. 344 pressBackButton(); 345 sleepDevice(); 346 wakeUpDevice(); 347 unlockDevice(); 348 } 349 350 /** 351 * Returns whether the lock screen is disabled. 352 * 353 * @return true if the lock screen is disabled, false otherwise. 354 */ isLockDisabled()355 private boolean isLockDisabled() { 356 final String isLockDisabled = runCommandAndPrintOutput( 357 "locksettings get-disabled").trim(); 358 return !"null".equals(isLockDisabled) && Boolean.parseBoolean(isLockDisabled); 359 } 360 361 /** 362 * Disable the lock screen. 363 * 364 * @param lockDisabled true if should disable, false otherwise. 365 */ setLockDisabled(boolean lockDisabled)366 protected void setLockDisabled(boolean lockDisabled) { 367 runCommandAndPrintOutput("locksettings set-disabled " + lockDisabled); 368 } 369 waitForDisplayStateWithRetry(boolean displayOn)370 protected void waitForDisplayStateWithRetry(boolean displayOn) { 371 for (int retry = 1; isDisplayOn() != displayOn && retry <= 5; retry++) { 372 logAlways("***Waiting for display state... retry " + retry); 373 SystemClock.sleep(TimeUnit.SECONDS.toMillis(1)); 374 } 375 } 376 } 377 runCommandAndPrintOutput(String command)378 protected static String runCommandAndPrintOutput(String command) { 379 final String output = executeShellCommand(command); 380 log(output); 381 return output; 382 } 383 384 protected static class LogSeparator { 385 private final String mUniqueString; 386 LogSeparator()387 private LogSeparator() { 388 mUniqueString = UUID.randomUUID().toString(); 389 } 390 391 @Override toString()392 public String toString() { 393 return mUniqueString; 394 } 395 } 396 397 // TODO: Now that our test are device side, we can convert these to a more direct communication 398 // channel vs. depending on logs. 399 private static final Pattern sDisplayStatePattern = 400 Pattern.compile("Display Power: state=(.+)"); 401 private static final Pattern sCurrentUiModePattern = Pattern.compile("mCurUiMode=0x(\\d+)"); 402 private static final Pattern sUiModeLockedPattern = 403 Pattern.compile("mUiModeLocked=(true|false)"); 404 stopTestPackage(final ComponentName activityName)405 protected void stopTestPackage(final ComponentName activityName) { 406 executeShellCommand("am force-stop " + activityName.getPackageName()); 407 } 408 } 409