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