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