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