1 /* 2 * Copyright (C) 2019 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.wm.intent; 18 19 import static android.server.wm.intent.Persistence.LaunchFromIntent.prepareSerialisation; 20 import static android.server.wm.intent.StateComparisonException.assertEndStatesEqual; 21 import static android.server.wm.intent.StateComparisonException.assertInitialStateEqual; 22 import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED; 23 24 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 25 26 import static com.google.common.collect.Iterables.getLast; 27 28 import static org.junit.Assert.assertNotNull; 29 30 import android.app.Activity; 31 import android.app.ActivityOptions; 32 import android.app.Instrumentation; 33 import android.app.WindowConfiguration; 34 import android.content.ComponentName; 35 import android.content.Context; 36 import android.content.Intent; 37 import android.os.Bundle; 38 import android.os.SystemClock; 39 import android.server.wm.WindowManagerStateHelper; 40 import android.server.wm.WindowManagerState; 41 import android.server.wm.intent.LaunchSequence.LaunchSequenceExecutionInfo; 42 import android.server.wm.intent.Persistence.GenerationIntent; 43 import android.server.wm.intent.Persistence.LaunchFromIntent; 44 import android.server.wm.intent.Persistence.StateDump; 45 import android.view.Display; 46 import android.window.DisplayAreaOrganizer; 47 48 import com.google.common.collect.Lists; 49 50 import java.util.List; 51 import java.util.stream.Collectors; 52 53 /** 54 * Launch runner is an interpreter for a {@link LaunchSequence} command object. 55 * It supports three main modes of operation. 56 * 57 * 1. The {@link LaunchRunner#runAndWrite} method to run a launch object and write out the 58 * resulting {@link Persistence.TestCase} to device storage 59 * 60 * 2. The {@link LaunchRunner#verify} method to rerun a previously recorded 61 * {@link Persistence.TestCase} and verify that the recorded states match the states resulting from 62 * the rerun. 63 * 64 * 3. The {@link LaunchRunner#run} method to run a launch object and return an {@link LaunchRecord} 65 * that can be used to do assertions directly in the same test. 66 */ 67 public class LaunchRunner { 68 private static final int ACTIVITY_LAUNCH_TIMEOUT = 10000; 69 private static final int BEFORE_DUMP_TIMEOUT = 3000; 70 71 /** 72 * Used for the waiting utilities. 73 */ 74 private IntentTestBase mTestBase; 75 76 /** 77 * The activities that were already present in the system when the test started. 78 * So they can be removed form the outputs, otherwise our tests would be system dependent. 79 */ 80 private List<WindowManagerState.Task> mBaseTasks; 81 LaunchRunner(IntentTestBase testBase)82 public LaunchRunner(IntentTestBase testBase) { 83 mTestBase = testBase; 84 mBaseTasks = getBaseTasks(); 85 } 86 87 /** 88 * Re-run a previously recorded {@link Persistence.TestCase} and verify that the recorded 89 * states match the states resulting from the rerun. 90 * 91 * @param initialContext the context to launch the first Activity from. 92 * @param testCase the {@link Persistence.TestCase} we are verifying. 93 */ verify(Context initialContext, Persistence.TestCase testCase)94 void verify(Context initialContext, Persistence.TestCase testCase) { 95 List<GenerationIntent> initialState = testCase.getSetup().getInitialIntents(); 96 List<GenerationIntent> act = testCase.getSetup().getAct(); 97 98 List<Activity> activityLog = Lists.newArrayList(); 99 100 // Launch the first activity from the start context 101 GenerationIntent firstIntent = initialState.get(0); 102 Activity firstActivity = launchFromContext(initialContext, firstIntent.getActualIntent()); 103 // Launch all tasks in the same task display area. CTS tests using multiple tasks assume 104 // they will be started in the same task display area. 105 int firstActivityDisplayAreaFeatureId = mTestBase.getWmState() 106 .getTaskDisplayAreaFeatureId(firstActivity.getComponentName()); 107 activityLog.add(firstActivity); 108 109 // launch the rest from the initial intents 110 for (int i = 1; i < initialState.size(); i++) { 111 GenerationIntent generationIntent = initialState.get(i); 112 Activity activityToLaunchFrom = activityLog.get(generationIntent.getLaunchFromIndex(i)); 113 Activity result = launch(activityToLaunchFrom, generationIntent.getActualIntent(), 114 generationIntent.startForResult(), firstActivityDisplayAreaFeatureId); 115 activityLog.add(result); 116 } 117 118 // assert that the state after setup is the same this time as the recorded state. 119 StateDump setupStateDump = waitDumpAndTrimForVerification(getLast(activityLog), 120 testCase.getInitialState()); 121 assertInitialStateEqual(testCase.getInitialState(), setupStateDump); 122 123 // apply all the intents in the act stage 124 for (int i = 0; i < act.size(); i++) { 125 GenerationIntent generationIntent = act.get(i); 126 Activity activityToLaunchFrom = activityLog.get( 127 generationIntent.getLaunchFromIndex(initialState.size() + i)); 128 Activity result = launch(activityToLaunchFrom, generationIntent.getActualIntent(), 129 generationIntent.startForResult(), firstActivityDisplayAreaFeatureId); 130 activityLog.add(result); 131 } 132 133 // assert that the endStates are the same. 134 StateDump endStateDump = waitDumpAndTrimForVerification(getLast(activityLog), 135 testCase.getEndState()); 136 assertEndStatesEqual(testCase.getEndState(), endStateDump); 137 } 138 139 /** 140 * Runs a launch object and writes out the resulting {@link Persistence.TestCase} to 141 * device storage 142 * 143 * @param startContext the context to launch the first Activity from. 144 * @param name the name of the directory to store the json files in. 145 * @param launches a list of launches to run and record. 146 */ runAndWrite(Context startContext, String name, List<LaunchSequence> launches)147 public void runAndWrite(Context startContext, String name, List<LaunchSequence> launches) 148 throws Exception { 149 for (int i = 0; i < launches.size(); i++) { 150 Persistence.TestCase testCase = this.runAndSerialize(launches.get(i), startContext, 151 Integer.toString(i)); 152 IoUtils.writeToDocumentsStorage(testCase, i + 1, name); 153 // Cleanup all the activities of this testCase before going to the next 154 // to preserve isolation across test cases. 155 mTestBase.cleanUp(testCase.getSetup().componentsInCase()); 156 } 157 } 158 runAndSerialize(LaunchSequence launchSequence, Context startContext, String name)159 private Persistence.TestCase runAndSerialize(LaunchSequence launchSequence, 160 Context startContext, String name) { 161 LaunchRecord launchRecord = run(launchSequence, startContext); 162 163 LaunchSequenceExecutionInfo executionInfo = launchSequence.fold(); 164 List<GenerationIntent> setupIntents = prepareSerialisation(executionInfo.setup); 165 List<GenerationIntent> actIntents = prepareSerialisation(executionInfo.acts, 166 setupIntents.size()); 167 168 Persistence.Setup setup = new Persistence.Setup(setupIntents, actIntents); 169 170 return new Persistence.TestCase(setup, launchRecord.initialDump, launchRecord.endDump, 171 name); 172 } 173 174 /** 175 * Runs a launch object and returns a {@link LaunchRecord} that can be used to do assertions 176 * directly in the same test. 177 * 178 * @param launch the {@link LaunchSequence}we want to run 179 * @param startContext the {@link android.content.Context} to launch the first Activity from. 180 * @return {@link LaunchRecord} that can be used to do assertions. 181 */ run(LaunchSequence launch, Context startContext)182 LaunchRecord run(LaunchSequence launch, Context startContext) { 183 LaunchSequence.LaunchSequenceExecutionInfo work = launch.fold(); 184 List<Activity> activityLog = Lists.newArrayList(); 185 186 if (work.setup.isEmpty() || work.acts.isEmpty()) { 187 throw new IllegalArgumentException("no intents to start"); 188 } 189 190 // Launch the first activity from the start context. 191 LaunchFromIntent firstIntent = work.setup.get(0); 192 Activity firstActivity = this.launchFromContext(startContext, 193 firstIntent.getActualIntent()); 194 195 activityLog.add(firstActivity); 196 197 // launch the rest from the initial intents. 198 for (int i = 1; i < work.setup.size(); i++) { 199 LaunchFromIntent launchFromIntent = work.setup.get(i); 200 Intent actualIntent = launchFromIntent.getActualIntent(); 201 Activity activity = launch(activityLog.get(launchFromIntent.getLaunchFrom()), 202 actualIntent, launchFromIntent.startForResult()); 203 activityLog.add(activity); 204 } 205 206 // record the state after the initial intents. 207 StateDump initialDump = waitDumpAndTrim(getLast(activityLog)); 208 209 // apply all the intents in the act stage 210 for (LaunchFromIntent launchFromIntent : work.acts) { 211 Intent actualIntent = launchFromIntent.getActualIntent(); 212 Activity activity = launch(activityLog.get(launchFromIntent.getLaunchFrom()), 213 actualIntent, launchFromIntent.startForResult()); 214 215 activityLog.add(activity); 216 } 217 218 //record the end state after all intents are launched. 219 StateDump endDump = waitDumpAndTrim(getLast(activityLog)); 220 221 return new LaunchRecord(initialDump, endDump, activityLog); 222 } 223 224 /** 225 * Results from the running of an {@link LaunchSequence} so the user can assert on the results 226 * directly. 227 */ 228 class LaunchRecord { 229 230 /** 231 * The end state after the setup intents. 232 */ 233 public final StateDump initialDump; 234 235 /** 236 * The end state after the setup and act intents. 237 */ 238 public final StateDump endDump; 239 240 /** 241 * The activities that were started by every intent in the {@link LaunchSequence}. 242 */ 243 public final List<Activity> mActivitiesLog; 244 LaunchRecord(StateDump initialDump, StateDump endDump, List<Activity> activitiesLog)245 public LaunchRecord(StateDump initialDump, StateDump endDump, 246 List<Activity> activitiesLog) { 247 this.initialDump = initialDump; 248 this.endDump = endDump; 249 mActivitiesLog = activitiesLog; 250 } 251 } 252 253 launchFromContext(Context context, Intent intent)254 public Activity launchFromContext(Context context, Intent intent) { 255 return launchFromContext(context, intent, FEATURE_UNDEFINED); 256 } 257 258 launchFromContext(Context context, Intent intent, int launchTaskDisplayAreaFeatureId)259 public Activity launchFromContext(Context context, Intent intent, 260 int launchTaskDisplayAreaFeatureId) { 261 Instrumentation.ActivityMonitor monitor = getInstrumentation() 262 .addMonitor((String) null, null, false); 263 264 context.startActivity(intent, getLaunchOptions(launchTaskDisplayAreaFeatureId)); 265 Activity activity = monitor.waitForActivityWithTimeout(ACTIVITY_LAUNCH_TIMEOUT); 266 waitAndAssertActivityLaunched(activity, intent); 267 268 return activity; 269 } 270 launch(Activity activityContext, Intent intent, boolean startForResult)271 public Activity launch(Activity activityContext, Intent intent, boolean startForResult) { 272 return launch(activityContext, intent, startForResult, FEATURE_UNDEFINED); 273 } 274 launch(Activity activityContext, Intent intent, boolean startForResult, int launchTaskDisplayAreaFeatureId)275 public Activity launch(Activity activityContext, Intent intent, boolean startForResult, 276 int launchTaskDisplayAreaFeatureId) { 277 Instrumentation.ActivityMonitor monitor = getInstrumentation() 278 .addMonitor((String) null, null, false); 279 280 if (startForResult) { 281 activityContext.startActivityForResult(intent, 1, 282 getLaunchOptions(launchTaskDisplayAreaFeatureId)); 283 } else { 284 activityContext.startActivity(intent, getLaunchOptions(launchTaskDisplayAreaFeatureId)); 285 } 286 Activity activity = monitor.waitForActivityWithTimeout(ACTIVITY_LAUNCH_TIMEOUT); 287 288 if (activity == null) { 289 return activityContext; 290 } else if (startForResult && activityContext == activity) { 291 // The result may have been sent back to caller activity and forced the caller activity 292 // to be resumed again, before the started activity actually resumed. Just wait for idle 293 // for that case. 294 getInstrumentation().waitForIdleSync(); 295 } else { 296 waitAndAssertActivityLaunched(activity, intent); 297 } 298 299 return activity; 300 } 301 waitAndAssertActivityLaunched(Activity activity, Intent intent)302 private void waitAndAssertActivityLaunched(Activity activity, Intent intent) { 303 assertNotNull("Intent: " + intent.toString(), activity); 304 305 final ComponentName testActivityName = activity.getComponentName(); 306 mTestBase.waitAndAssertTopResumedActivity(testActivityName, 307 Display.DEFAULT_DISPLAY, "Activity must be resumed"); 308 } 309 310 /** 311 * After the last activity has been launched we wait for a valid state + an extra three seconds 312 * so have a stable state of the system. Also all previously known tasks in 313 * {@link LaunchRunner#mBaseTasks} is excluded from the output. 314 * 315 * @param activity The last activity to be launched before dumping the state. 316 * @return A stable {@link StateDump}, meaning no more {@link android.app.Activity} is in a 317 * life cycle transition. 318 */ waitDumpAndTrim(Activity activity)319 public StateDump waitDumpAndTrim(Activity activity) { 320 mTestBase.getWmState().waitForValidState(activity.getComponentName()); 321 // The last activity that was launched before the dump could still be in an intermediate 322 // lifecycle state. wait an extra 3 seconds for it to settle 323 SystemClock.sleep(BEFORE_DUMP_TIMEOUT); 324 mTestBase.getWmState().computeState(activity.getComponentName()); 325 List<WindowManagerState.Task> endStateTasks = 326 mTestBase.getWmState().getRootTasks(); 327 return StateDump.fromTasks(endStateTasks, mBaseTasks); 328 } 329 330 /** 331 * Like {@link LaunchRunner#waitDumpAndTrim(Activity)} but also waits until the state becomes 332 * equal to the state we expect. It is therefore only used when verifying a recorded testcase. 333 * 334 * If we take a dump of an unstable state we allow it to settle into the expected state. 335 * 336 * @param activity The last activity to be launched before dumping the state. 337 * @param expected The state that was previously recorded for this testCase. 338 * @return A stable {@link StateDump}, meaning no more {@link android.app.Activity} is in a 339 * life cycle transition. 340 */ waitDumpAndTrimForVerification(Activity activity, StateDump expected)341 public StateDump waitDumpAndTrimForVerification(Activity activity, StateDump expected) { 342 mTestBase.getWmState().waitForValidState(activity.getComponentName()); 343 mTestBase.getWmState().waitForWithAmState( 344 am -> StateDump.fromTasks(am.getRootTasks(), mBaseTasks).equals(expected), 345 "the activity states match up with what we recorded"); 346 mTestBase.getWmState().computeState(activity.getComponentName()); 347 348 List<WindowManagerState.Task> endStateTasks = 349 mTestBase.getWmState().getRootTasks(); 350 351 endStateTasks = endStateTasks.stream() 352 .filter(task -> activity.getPackageName().equals(task.getPackageName())) 353 .collect(Collectors.toList()); 354 355 return StateDump.fromTasks(endStateTasks, mBaseTasks); 356 } 357 getBaseTasks()358 private List<WindowManagerState.Task> getBaseTasks() { 359 WindowManagerStateHelper amWmState = mTestBase.getWmState(); 360 amWmState.computeState(new ComponentName[]{}); 361 return amWmState.getRootTasks(); 362 } 363 getLaunchOptions()364 private static Bundle getLaunchOptions() { 365 return getLaunchOptions(FEATURE_UNDEFINED); 366 } 367 getLaunchOptions(int launchTaskDisplayAreaFeatureId)368 private static Bundle getLaunchOptions(int launchTaskDisplayAreaFeatureId) { 369 ActivityOptions options = ActivityOptions.makeBasic(); 370 options.setLaunchWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN); 371 if (launchTaskDisplayAreaFeatureId != DisplayAreaOrganizer.FEATURE_UNDEFINED) { 372 options.setLaunchTaskDisplayAreaFeatureId(launchTaskDisplayAreaFeatureId); 373 } 374 return options.toBundle(); 375 } 376 } 377