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 androidx.test.InstrumentationRegistry.getInstrumentation;
20 
21 import android.content.ComponentName;
22 import android.server.wm.intent.Persistence.LaunchFromIntent;
23 import android.server.wm.intent.Persistence.LaunchIntent;
24 
25 import com.google.common.collect.Lists;
26 
27 import java.util.List;
28 import java.util.Optional;
29 
30 /**
31  * <pre>
32  * The main API entry point for specifying intent tests.
33  * A {@code Launch} object is an immutable command object to specify sequences of intents to
34  * launch.
35  *
36  * They can be run by a {@link LaunchRunner}
37  * Most tests using this api are defined in {@link Cases}.
38  * The two test classes actually using the tests specified there are:
39  *
40  * 1. {@link IntentGenerationTests}, which runs the cases, records the results and writes them
41  * out to device storage
42  * 2. {@link IntentTests}, which reads the recorded test files and verifies them again.
43  *
44  *
45  * Features supported by this API are:
46  * 1. Starting activities normally or for result.
47  * {@code
48  *  LaunchSequence.start(intentForResult(RegularActivity.class)) // starting an intent for result.
49  *                .append(intent(RegularActivity.class));         // starting an intent normally.
50  * }
51  * 2. Specifying Intent Flags
52  * {@code
53  *   LaunchSequence.start(intent(RegularActivity.class).withFlags(Cases.NEW_TASK));
54  * }
55  *
56  * 3. Launching an intent from any point earlier in the launch sequence.
57  * {@code
58  *   LaunchIntent multipleTask = intent(RegularActivity.class)
59  *                                     .withFlags(Cases.NEW_TASK,Cases.MULTIPLE_TASK);
60  *   LaunchSequence firstTask = LaunchSequence.start(multipleTask);
61  *   firstTask.append(multipleTask).append(intent(SingleTopActivity.class), firstTask);
62  * }
63  *
64  * The above will result in: the first task having two activities in it and the second task having
65  * one activity, instead of the normal behaviour adding only one task.
66  *
67  * It is completely immutable and can therefore be reused without hesitation.
68  * Note that making a launch object doesn't start anything yet until it is ran by a
69  * {@link LaunchRunner}
70  * </pre>
71  */
72 public interface LaunchSequence {
73     /**
74      * @return the amount of intents that will launch before this {@link LaunchSequence} object.
75      */
depth()76     int depth();
77 
78     /**
79      * Extract all the information that has been built up in this {@link LaunchSequence} object, so
80      * that {@link LaunchRunner} can run the described sequence of intents.
81      */
fold()82     LaunchSequenceExecutionInfo fold();
83 
84     /**
85      * @return the {@link LaunchIntent} inside of this {@link LaunchSequence}.
86      */
getIntent()87     LaunchIntent getIntent();
88 
89     /**
90      * Create a {@link LaunchSequence} object this always has
91      * {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK} set because without it we can not
92      * launch from the application context.
93      *
94      * @param intent the first intent to be Launched.
95      * @return an {@link LaunchSequence} object launching just this intent.
96      */
create(LaunchIntent intent)97     static LaunchSequence create(LaunchIntent intent) {
98         return new RootLaunch(intent.withFlags(Cases.NEW_TASK));
99     }
100 
101     /**
102      * @param intent the next intent that should be launched
103      * @return a {@link LaunchSequence} that will launch all the intents in this and then
104      * {@code intent}
105      */
append(LaunchIntent intent)106     default LaunchSequence append(LaunchIntent intent) {
107         return new ConsecutiveLaunch(this, intent, false, Optional.empty());
108     }
109 
110     /**
111      * @param intent     the next intent that should be launched
112      * @param launchFrom a {@link LaunchSequence} to start the intent from, {@code launchFrom}
113      *                   should be a sub sequence of this.
114      * @return a {@link LaunchSequence} that will launch all the intents in this and then
115      * {@code intent}
116      */
append(LaunchIntent intent, LaunchSequence launchFrom)117     default LaunchSequence append(LaunchIntent intent, LaunchSequence launchFrom) {
118         return new ConsecutiveLaunch(this, intent, false, Optional.of(launchFrom));
119     }
120 
121     /**
122      * @param intent the intent to Launch
123      * @return a launch with the {@code intent} added to the Act stage.
124      */
act(LaunchIntent intent)125     default LaunchSequence act(LaunchIntent intent) {
126         return new ConsecutiveLaunch(this, intent, true, Optional.empty());
127     }
128 
129     /**
130      * @param intent     the intent to Launch
131      * @param launchFrom a {@link LaunchSequence} to start the intent from, {@code launchFrom}
132      *                   should be a sub sequence of this.
133      * @return a launch with the {@code intent} added to the Act stage.
134      */
act(LaunchIntent intent, LaunchSequence launchFrom)135     default LaunchSequence act(LaunchIntent intent, LaunchSequence launchFrom) {
136         return new ConsecutiveLaunch(this, intent, true, Optional.of(launchFrom));
137     }
138 
139     /**
140      * @param activity the activity to create an intent for.
141      * @return Creates an {@link LaunchIntent} that will target the {@link android.app.activity}
142      * class passed in. see {@link LaunchIntent#withFlags} to add intent flags to the returned
143      * intent.
144      */
intent(Class<? extends android.app.Activity> activity)145     static LaunchIntent intent(Class<? extends android.app.Activity> activity) {
146         return new LaunchIntent(Lists.newArrayList(), createComponent(activity), null, false);
147     }
148 
149 
150     /**
151      * Creates an {@link LaunchIntent} that will be started with {@link
152      * android.app.Activity#startActivityForResult}
153      *
154      * @param activity the activity to create an intent for.
155      */
intentForResult(Class<? extends android.app.Activity> activity)156     static LaunchIntent intentForResult(Class<? extends android.app.Activity> activity) {
157         return new LaunchIntent(Lists.newArrayList(), createComponent(activity), null, true);
158     }
159 
160     String packageName = getInstrumentation().getTargetContext().getPackageName();
161 
createComponent(Class<? extends android.app.Activity> activity)162     static ComponentName createComponent(Class<? extends android.app.Activity> activity) {
163         return new ComponentName(packageName, activity.getName());
164     }
165 
166     /**
167      * Allows {@link LaunchSequence} objects to form a linkedlist of intents to launch.
168      */
169     class ConsecutiveLaunch implements LaunchSequence {
170         private final LaunchSequence mPrevious;
171         private final LaunchIntent mLaunchIntent;
172         private final boolean mAct;
173 
ConsecutiveLaunch(LaunchSequence previous, LaunchIntent launchIntent, boolean act, Optional<LaunchSequence> launchFrom)174         public ConsecutiveLaunch(LaunchSequence previous, LaunchIntent launchIntent,
175                 boolean act, Optional<LaunchSequence> launchFrom) {
176             mPrevious = previous;
177             mLaunchIntent = launchIntent;
178             mAct = act;
179             mLaunchFrom = launchFrom;
180         }
181 
182         /**
183          * This should always be a {@link LaunchSequence} that is in mPrevious.
184          */
185         @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
186         private Optional<LaunchSequence> mLaunchFrom;
187 
188         @Override
depth()189         public int depth() {
190             return mPrevious.depth() + 1;
191         }
192 
193         @Override
fold()194         public LaunchSequenceExecutionInfo fold() {
195             LaunchSequenceExecutionInfo launchSequenceExecutionInfo = mPrevious.fold();
196             int launchSite = mLaunchFrom.map(LaunchSequence::depth).orElse(this.depth() - 1);
197 
198             LaunchFromIntent intent = new LaunchFromIntent(mLaunchIntent, launchSite);
199             if (mAct) {
200                 launchSequenceExecutionInfo.acts.add(intent);
201             } else {
202                 launchSequenceExecutionInfo.setup.add(intent);
203             }
204             return launchSequenceExecutionInfo;
205         }
206 
207         @Override
getIntent()208         public LaunchIntent getIntent() {
209             return mLaunchIntent;
210         }
211     }
212 
213     /**
214      * The first intent to launch in a {@link LaunchSequence} Object.
215      */
216     class RootLaunch implements LaunchSequence {
217         /**
218          * The intent that should be launched.
219          */
220         private final LaunchIntent mLaunchIntent;
221 
RootLaunch(LaunchIntent launchIntent)222         public RootLaunch(LaunchIntent launchIntent) {
223             mLaunchIntent = launchIntent;
224         }
225 
226         @Override
depth()227         public int depth() {
228             return 0;
229         }
230 
231         @Override
fold()232         public LaunchSequenceExecutionInfo fold() {
233             return new LaunchSequenceExecutionInfo(
234                     Lists.newArrayList(new LaunchFromIntent(mLaunchIntent, -1)),
235                     Lists.newArrayList());
236         }
237 
238         @Override
getIntent()239         public LaunchIntent getIntent() {
240             return mLaunchIntent;
241         }
242     }
243 
244     /**
245      * Information for the {@link LaunchRunner} to run the launch represented by a {@link
246      * LaunchSequence} object. The {@link LaunchSequence} object is "folded" as a list
247      * into the summary of all intents with the corresponding indexes from what context
248      * to launch them.
249      */
250     class LaunchSequenceExecutionInfo {
251         List<LaunchFromIntent> setup;
252         List<LaunchFromIntent> acts;
253 
LaunchSequenceExecutionInfo(List<LaunchFromIntent> setup, List<LaunchFromIntent> acts)254         public LaunchSequenceExecutionInfo(List<LaunchFromIntent> setup,
255                 List<LaunchFromIntent> acts) {
256             this.setup = setup;
257             this.acts = acts;
258         }
259     }
260 }
261 
262