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