| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.server.wm.intent; |
| |
| import static android.server.wm.intent.Persistence.LaunchFromIntent.prepareSerialisation; |
| import static android.server.wm.intent.StateComparisonException.assertEndStatesEqual; |
| import static android.server.wm.intent.StateComparisonException.assertInitialStateEqual; |
| |
| import static androidx.test.InstrumentationRegistry.getInstrumentation; |
| |
| import static com.google.common.collect.Iterables.getLast; |
| |
| import static org.junit.Assert.assertNotNull; |
| |
| import android.app.Activity; |
| import android.app.Instrumentation; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.os.SystemClock; |
| import android.server.wm.WindowManagerStateHelper; |
| import android.server.wm.WindowManagerState; |
| import android.server.wm.intent.LaunchSequence.LaunchSequenceExecutionInfo; |
| import android.server.wm.intent.Persistence.GenerationIntent; |
| import android.server.wm.intent.Persistence.LaunchFromIntent; |
| import android.server.wm.intent.Persistence.StateDump; |
| import android.view.Display; |
| |
| import com.google.common.collect.Lists; |
| |
| import java.util.List; |
| |
| /** |
| * Launch runner is an interpreter for a {@link LaunchSequence} command object. |
| * It supports three main modes of operation. |
| * |
| * 1. The {@link LaunchRunner#runAndWrite} method to run a launch object and write out the |
| * resulting {@link Persistence.TestCase} to device storage |
| * |
| * 2. The {@link LaunchRunner#verify} method to rerun a previously recorded |
| * {@link Persistence.TestCase} and verify that the recorded states match the states resulting from |
| * the rerun. |
| * |
| * 3. The {@link LaunchRunner#run} method to run a launch object and return an {@link LaunchRecord} |
| * that can be used to do assertions directly in the same test. |
| */ |
| public class LaunchRunner { |
| private static final int ACTIVITY_LAUNCH_TIMEOUT = 10000; |
| private static final int BEFORE_DUMP_TIMEOUT = 3000; |
| |
| /** |
| * Used for the waiting utilities. |
| */ |
| private IntentTestBase mTestBase; |
| |
| /** |
| * The activities that were already present in the system when the test started. |
| * So they can be removed form the outputs, otherwise our tests would be system dependent. |
| */ |
| private List<WindowManagerState.ActivityTask> mBaseStacks; |
| |
| public LaunchRunner(IntentTestBase testBase) { |
| mTestBase = testBase; |
| mBaseStacks = getBaseStacks(); |
| } |
| |
| /** |
| * Re-run a previously recorded {@link Persistence.TestCase} and verify that the recorded |
| * states match the states resulting from the rerun. |
| * |
| * @param initialContext the context to launch the first Activity from. |
| * @param testCase the {@link Persistence.TestCase} we are verifying. |
| */ |
| void verify(Context initialContext, Persistence.TestCase testCase) { |
| List<GenerationIntent> initialState = testCase.getSetup().getInitialIntents(); |
| List<GenerationIntent> act = testCase.getSetup().getAct(); |
| |
| List<Activity> activityLog = Lists.newArrayList(); |
| |
| // Launch the first activity from the start context |
| GenerationIntent firstIntent = initialState.get(0); |
| activityLog.add(launchFromContext(initialContext, firstIntent.getActualIntent())); |
| |
| // launch the rest from the initial intents |
| for (int i = 1; i < initialState.size(); i++) { |
| GenerationIntent generationIntent = initialState.get(i); |
| Activity activityToLaunchFrom = activityLog.get(generationIntent.getLaunchFromIndex(i)); |
| Activity result = launch(activityToLaunchFrom, generationIntent.getActualIntent(), |
| generationIntent.startForResult()); |
| activityLog.add(result); |
| } |
| |
| // assert that the state after setup is the same this time as the recorded state. |
| StateDump setupStateDump = waitDumpAndTrimForVerification(getLast(activityLog), |
| testCase.getEndState()); |
| assertInitialStateEqual(testCase.getInitialState(), setupStateDump); |
| |
| // apply all the intents in the act stage |
| for (int i = 0; i < act.size(); i++) { |
| GenerationIntent generationIntent = act.get(i); |
| Activity activityToLaunchFrom = activityLog.get( |
| generationIntent.getLaunchFromIndex(initialState.size() + i)); |
| Activity result = launch(activityToLaunchFrom, generationIntent.getActualIntent(), |
| generationIntent.startForResult()); |
| activityLog.add(result); |
| } |
| |
| // assert that the endStates are the same. |
| StateDump endStateDump = waitDumpAndTrimForVerification(getLast(activityLog), |
| testCase.getEndState()); |
| assertEndStatesEqual(testCase.getEndState(), endStateDump); |
| } |
| |
| /** |
| * Runs a launch object and writes out the resulting {@link Persistence.TestCase} to |
| * device storage |
| * |
| * @param startContext the context to launch the first Activity from. |
| * @param name the name of the directory to store the json files in. |
| * @param launches a list of launches to run and record. |
| */ |
| public void runAndWrite(Context startContext, String name, List<LaunchSequence> launches) |
| throws Exception { |
| for (int i = 0; i < launches.size(); i++) { |
| Persistence.TestCase testCase = this.runAndSerialize(launches.get(i), startContext, |
| Integer.toString(i)); |
| IntentTests.writeToDocumentsStorage(testCase, i + 1, name); |
| // Cleanup all the activities of this testCase before going to the next |
| // to preserve isolation across test cases. |
| mTestBase.cleanUp(testCase.getSetup().componentsInCase()); |
| } |
| } |
| |
| private Persistence.TestCase runAndSerialize(LaunchSequence launchSequence, |
| Context startContext, String name) { |
| LaunchRecord launchRecord = run(launchSequence, startContext); |
| |
| LaunchSequenceExecutionInfo executionInfo = launchSequence.fold(); |
| List<GenerationIntent> setupIntents = prepareSerialisation(executionInfo.setup); |
| List<GenerationIntent> actIntents = prepareSerialisation(executionInfo.acts, |
| setupIntents.size()); |
| |
| Persistence.Setup setup = new Persistence.Setup(setupIntents, actIntents); |
| |
| return new Persistence.TestCase(setup, launchRecord.initialDump, launchRecord.endDump, |
| name); |
| } |
| |
| /** |
| * Runs a launch object and returns a {@link LaunchRecord} that can be used to do assertions |
| * directly in the same test. |
| * |
| * @param launch the {@link LaunchSequence}we want to run |
| * @param startContext the {@link android.content.Context} to launch the first Activity from. |
| * @return {@link LaunchRecord} that can be used to do assertions. |
| */ |
| LaunchRecord run(LaunchSequence launch, Context startContext) { |
| LaunchSequence.LaunchSequenceExecutionInfo work = launch.fold(); |
| List<Activity> activityLog = Lists.newArrayList(); |
| |
| if (work.setup.isEmpty() || work.acts.isEmpty()) { |
| throw new IllegalArgumentException("no intents to start"); |
| } |
| |
| // Launch the first activity from the start context. |
| LaunchFromIntent firstIntent = work.setup.get(0); |
| Activity firstActivity = this.launchFromContext(startContext, |
| firstIntent.getActualIntent()); |
| |
| activityLog.add(firstActivity); |
| |
| // launch the rest from the initial intents. |
| for (int i = 1; i < work.setup.size(); i++) { |
| LaunchFromIntent launchFromIntent = work.setup.get(i); |
| Intent actualIntent = launchFromIntent.getActualIntent(); |
| Activity activity = launch(activityLog.get(launchFromIntent.getLaunchFrom()), |
| actualIntent, launchFromIntent.startForResult()); |
| activityLog.add(activity); |
| } |
| |
| // record the state after the initial intents. |
| StateDump initialDump = waitDumpAndTrim(getLast(activityLog)); |
| |
| // apply all the intents in the act stage |
| for (LaunchFromIntent launchFromIntent : work.acts) { |
| Intent actualIntent = launchFromIntent.getActualIntent(); |
| Activity activity = launch(activityLog.get(launchFromIntent.getLaunchFrom()), |
| actualIntent, launchFromIntent.startForResult()); |
| |
| activityLog.add(activity); |
| } |
| |
| //record the end state after all intents are launched. |
| StateDump endDump = waitDumpAndTrim(getLast(activityLog)); |
| |
| return new LaunchRecord(initialDump, endDump, activityLog); |
| } |
| |
| /** |
| * Results from the running of an {@link LaunchSequence} so the user can assert on the results |
| * directly. |
| */ |
| class LaunchRecord { |
| |
| /** |
| * The end state after the setup intents. |
| */ |
| public final StateDump initialDump; |
| |
| /** |
| * The end state after the setup and act intents. |
| */ |
| public final StateDump endDump; |
| |
| /** |
| * The activities that were started by every intent in the {@link LaunchSequence}. |
| */ |
| public final List<Activity> mActivitiesLog; |
| |
| public LaunchRecord(StateDump initialDump, StateDump endDump, |
| List<Activity> activitiesLog) { |
| this.initialDump = initialDump; |
| this.endDump = endDump; |
| mActivitiesLog = activitiesLog; |
| } |
| } |
| |
| |
| public Activity launchFromContext(Context context, Intent intent) { |
| Instrumentation.ActivityMonitor monitor = getInstrumentation() |
| .addMonitor((String) null, null, false); |
| |
| context.startActivity(intent); |
| Activity activity = monitor.waitForActivityWithTimeout(ACTIVITY_LAUNCH_TIMEOUT); |
| waitAndAssertActivityLaunched(activity, intent); |
| |
| return activity; |
| } |
| |
| public Activity launch(Activity activityContext, Intent intent, boolean startForResult) { |
| Instrumentation.ActivityMonitor monitor = getInstrumentation() |
| .addMonitor((String) null, null, false); |
| |
| if (startForResult) { |
| activityContext.startActivityForResult(intent, 1); |
| } else { |
| activityContext.startActivity(intent); |
| } |
| Activity activity = monitor.waitForActivityWithTimeout(ACTIVITY_LAUNCH_TIMEOUT); |
| |
| if (activity == null) { |
| return activityContext; |
| } else if (startForResult && activityContext == activity) { |
| // The result might have send back to caller activity and forced the caller activity |
| // to resumed again, before the started activity actually resumed. Just wait for idle |
| // for that case. |
| getInstrumentation().waitForIdleSync(); |
| } else { |
| waitAndAssertActivityLaunched(activity, intent); |
| } |
| |
| return activity; |
| } |
| |
| private void waitAndAssertActivityLaunched(Activity activity, Intent intent) { |
| assertNotNull("Intent: " + intent.toString(), activity); |
| |
| final ComponentName testActivityName = activity.getComponentName(); |
| mTestBase.waitAndAssertTopResumedActivity(testActivityName, |
| Display.DEFAULT_DISPLAY, "Activity must be resumed"); |
| } |
| |
| /** |
| * After the last activity has been launched we wait for a valid state + an extra three seconds |
| * so have a stable state of the system. Also all previously known stacks in |
| * {@link LaunchRunner#mBaseStacks} is excluded from the output. |
| * |
| * @param activity The last activity to be launched before dumping the state. |
| * @return A stable {@link StateDump}, meaning no more {@link android.app.Activity} is in a |
| * life cycle transition. |
| */ |
| public StateDump waitDumpAndTrim(Activity activity) { |
| mTestBase.getWmState().waitForValidState(activity.getComponentName()); |
| // The last activity that was launched before the dump could still be in an intermediate |
| // lifecycle state. wait an extra 3 seconds for it to settle |
| SystemClock.sleep(BEFORE_DUMP_TIMEOUT); |
| mTestBase.getWmState().computeState(activity.getComponentName()); |
| List<WindowManagerState.ActivityTask> endStateStacks = |
| mTestBase.getWmState().getRootTasks(); |
| return StateDump.fromStacks(endStateStacks, mBaseStacks); |
| } |
| |
| /** |
| * Like {@link LaunchRunner#waitDumpAndTrim(Activity)} but also waits until the state becomes |
| * equal to the state we expect. It is therefore only used when verifying a recorded testcase. |
| * |
| * If we take a dump of an unstable state we allow it to settle into the expected state. |
| * |
| * @param activity The last activity to be launched before dumping the state. |
| * @param expected The state that was previously recorded for this testCase. |
| * @return A stable {@link StateDump}, meaning no more {@link android.app.Activity} is in a |
| * life cycle transition. |
| */ |
| public StateDump waitDumpAndTrimForVerification(Activity activity, StateDump expected) { |
| mTestBase.getWmState().waitForValidState(activity.getComponentName()); |
| // The last activity that was launched before the dump could still be in an intermediate |
| // lifecycle state. wait an extra 3 seconds for it to settle |
| SystemClock.sleep(BEFORE_DUMP_TIMEOUT); |
| mTestBase.getWmState().waitForWithAmState( |
| am -> StateDump.fromStacks(am.getRootTasks(), mBaseStacks).equals(expected), |
| "the activity states match up with what we recorded"); |
| mTestBase.getWmState().computeState(activity.getComponentName()); |
| |
| List<WindowManagerState.ActivityTask> endStateStacks = |
| mTestBase.getWmState().getRootTasks(); |
| |
| return StateDump.fromStacks(endStateStacks, mBaseStacks); |
| } |
| |
| private List<WindowManagerState.ActivityTask> getBaseStacks() { |
| WindowManagerStateHelper amWmState = mTestBase.getWmState(); |
| amWmState.computeState(new ComponentName[]{}); |
| return amWmState.getRootTasks(); |
| } |
| } |