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