• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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