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 java.util.stream.Collectors.toList;
20 
21 import android.content.ComponentName;
22 import android.content.Intent;
23 import android.net.Uri;
24 import android.server.wm.WindowManagerState;
25 
26 import com.google.common.collect.Lists;
27 
28 import org.json.JSONArray;
29 import org.json.JSONException;
30 import org.json.JSONObject;
31 
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.Collections;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Objects;
38 import java.util.stream.Collectors;
39 import java.util.stream.Stream;
40 
41 /**
42  * The intent tests are generated by running a series of intents and then recording the end state
43  * of the system. This class contains all the models needed to store the intents that were used to
44  * create the test case and the end states so that they can be asserted on.
45  *
46  * All test cases are serialized to JSON and stored in a single file per testcase.
47  */
48 public class Persistence {
49 
50     /**
51      * The highest level entity in the JSON file
52      */
53     public static class TestCase {
54         private static final String SETUP_KEY = "setup";
55         private static final String INITIAL_STATE_KEY = "initialState";
56         private static final String END_STATE_KEY = "endState";
57 
58         /**
59          * Contains the {@link android.content.Intent}-s that will be launched in this test case.
60          */
61         private final Setup mSetup;
62 
63         /**
64          * The state of the system after the {@link Setup#mInitialIntents} have been launched.
65          */
66         private final StateDump mInitialState;
67 
68         /**
69          * The state of the system after the {@link Setup#mAct} have been launched
70          */
71         private final StateDump mEndState;
72 
73         /**
74          * The name of the testCase, usually the file name it is stored in.
75          * Not actually persisted to json, since it is only used for presentation purposes.
76          */
77         private final String mName;
78 
TestCase(Setup setup, StateDump initialState, StateDump endState, String name)79         public TestCase(Setup setup, StateDump initialState,
80                 StateDump endState, String name) {
81             mSetup = setup;
82             mInitialState = initialState;
83             mEndState = endState;
84             mName = name;
85         }
86 
toJson()87         public JSONObject toJson() throws JSONException {
88             return new JSONObject()
89                     .put(SETUP_KEY, mSetup.toJson())
90                     .put(INITIAL_STATE_KEY, mInitialState.toJson())
91                     .put(END_STATE_KEY, mEndState.toJson());
92         }
93 
fromJson(JSONObject object, Map<String, IntentFlag> table, String name)94         public static TestCase fromJson(JSONObject object,
95                 Map<String, IntentFlag> table, String name) throws JSONException {
96             return new TestCase(Setup.fromJson(object.getJSONObject(SETUP_KEY), table),
97                     StateDump.fromJson(object.getJSONObject(INITIAL_STATE_KEY)),
98                     StateDump.fromJson(object.getJSONObject(END_STATE_KEY)), name);
99         }
100 
getSetup()101         public Setup getSetup() {
102             return mSetup;
103         }
104 
getInitialState()105         public StateDump getInitialState() {
106             return mInitialState;
107         }
108 
getName()109         public String getName() {
110             return mName;
111         }
112 
getEndState()113         public StateDump getEndState() {
114             return mEndState;
115         }
116     }
117 
118     /**
119      * Setup consists of two stages. Firstly a list of intents to bring the system in the state we
120      * want to test something in. Secondly a list of intents to bring the system to the final state.
121      */
122     public static class Setup {
123         private static final String INITIAL_INTENT_KEY = "initialIntents";
124         private static final String ACT_KEY = "act";
125         /**
126          * The intent(s) used to bring the system to the initial state.
127          */
128         private final List<GenerationIntent> mInitialIntents;
129 
130         /**
131          * The intent(s) that we actually want to test.
132          */
133         private final List<GenerationIntent> mAct;
134 
Setup(List<GenerationIntent> initialIntents, List<GenerationIntent> act)135         public Setup(List<GenerationIntent> initialIntents, List<GenerationIntent> act) {
136             mInitialIntents = initialIntents;
137             mAct = act;
138         }
139 
componentsInCase()140         public List<ComponentName> componentsInCase() {
141             return Stream.concat(mInitialIntents.stream(), mAct.stream())
142                     .map(GenerationIntent::getActualIntent)
143                     .map(Intent::getComponent)
144                     .collect(Collectors.toList());
145         }
146 
toJson()147         public JSONObject toJson() throws JSONException {
148             return new JSONObject()
149                     .put(INITIAL_INTENT_KEY, intentsToJson(mInitialIntents))
150                     .put(ACT_KEY, intentsToJson(mAct));
151         }
152 
fromJson(JSONObject object, Map<String, IntentFlag> table)153         public static Setup fromJson(JSONObject object,
154                 Map<String, IntentFlag> table) throws JSONException {
155             List<GenerationIntent> initialState = intentsFromJson(
156                     object.getJSONArray(INITIAL_INTENT_KEY), table);
157             List<GenerationIntent> act = intentsFromJson(object.getJSONArray(ACT_KEY), table);
158 
159             return new Setup(initialState, act);
160         }
161 
162 
intentsToJson(List<GenerationIntent> intents)163         public static JSONArray intentsToJson(List<GenerationIntent> intents)
164                 throws JSONException {
165 
166             JSONArray intentArray = new JSONArray();
167             for (GenerationIntent intent : intents) {
168                 intentArray.put(intent.toJson());
169             }
170             return intentArray;
171         }
172 
intentsFromJson(JSONArray intentArray, Map<String, IntentFlag> table)173         public static List<GenerationIntent> intentsFromJson(JSONArray intentArray,
174                 Map<String, IntentFlag> table) throws JSONException {
175             List<GenerationIntent> intents = new ArrayList<>();
176 
177             for (int i = 0; i < intentArray.length(); i++) {
178                 JSONObject object = (JSONObject) intentArray.get(i);
179                 GenerationIntent intent = GenerationIntent.fromJson(object, table);
180 
181                 intents.add(intent);
182             }
183 
184             return intents;
185         }
186 
getInitialIntents()187         public List<GenerationIntent> getInitialIntents() {
188             return mInitialIntents;
189         }
190 
getAct()191         public List<GenerationIntent> getAct() {
192             return mAct;
193         }
194     }
195 
196     /**
197      * An representation of an {@link android.content.Intent} that can be (de)serialized to / from
198      * JSON. It abstracts whether the context it should be started from is implicitly or explicitly
199      * specified.
200      */
201     interface GenerationIntent {
getActualIntent()202         Intent getActualIntent();
203 
toJson()204         JSONObject toJson() throws JSONException;
205 
getLaunchFromIndex(int currentPosition)206         int getLaunchFromIndex(int currentPosition);
207 
startForResult()208         boolean startForResult();
209 
fromJson(JSONObject object, Map<String, IntentFlag> table)210         static GenerationIntent fromJson(JSONObject object, Map<String, IntentFlag> table)
211                 throws JSONException {
212             if (object.has(LaunchFromIntent.LAUNCH_FROM_KEY)) {
213                 return LaunchFromIntent.fromJson(object, table);
214             } else {
215                 return LaunchIntent.fromJson(object, table);
216             }
217         }
218     }
219 
220     /**
221      * Representation of {@link android.content.Intent} used by the {@link LaunchSequence} api.
222      * It be can used to normally start activities, to start activities for result and Intent Flags
223      * can be added using {@link LaunchIntent#withFlags(IntentFlag...)}
224      */
225     static class LaunchIntent implements GenerationIntent {
226         private static final String FLAGS_KEY = "flags";
227         private static final String PACKAGE_KEY = "package";
228         private static final String CLASS_KEY = "class";
229         private static final String DATA_KEY = "data";
230         private static final String START_FOR_RESULT_KEY = "startForResult";
231 
232         private final List<IntentFlag> mIntentFlags;
233         private final ComponentName mComponentName;
234         private final String mData;
235         private final boolean mStartForResult;
236 
LaunchIntent(List<IntentFlag> intentFlags, ComponentName componentName, String data, boolean startForResult)237         public LaunchIntent(List<IntentFlag> intentFlags, ComponentName componentName, String data,
238                 boolean startForResult) {
239             mIntentFlags = intentFlags;
240             mComponentName = componentName;
241             mData = data;
242             mStartForResult = startForResult;
243         }
244 
245         @Override
getActualIntent()246         public Intent getActualIntent() {
247             final Intent intent = new Intent().setComponent(mComponentName).setFlags(buildFlag());
248             if (mData != null && !mData.isEmpty()) {
249                 intent.setData(Uri.parse(mData));
250             }
251             return intent;
252         }
253 
254         @Override
getLaunchFromIndex(int currentPosition)255         public int getLaunchFromIndex(int currentPosition) {
256             return currentPosition - 1;
257         }
258 
259         @Override
startForResult()260         public boolean startForResult() {
261             return mStartForResult;
262         }
263 
buildFlag()264         public int buildFlag() {
265             int flag = 0;
266             for (IntentFlag intentFlag : mIntentFlags) {
267                 flag |= intentFlag.flag;
268             }
269 
270             return flag;
271         }
272 
humanReadableFlags()273         public String humanReadableFlags() {
274             return mIntentFlags.stream().map(IntentFlag::toString).collect(
275                     Collectors.joining(" | "));
276         }
277 
fromJson(JSONObject fakeIntent, Map<String, IntentFlag> table)278         public static LaunchIntent fromJson(JSONObject fakeIntent, Map<String, IntentFlag> table)
279                 throws JSONException {
280             List<IntentFlag> flags = IntentFlag.parse(table, fakeIntent.getString(FLAGS_KEY));
281 
282             boolean startForResult = fakeIntent.optBoolean(START_FOR_RESULT_KEY, false);
283             String uri = fakeIntent.optString(DATA_KEY);
284             return new LaunchIntent(flags,
285                     new ComponentName(
286                             fakeIntent.getString(PACKAGE_KEY),
287                             fakeIntent.getString(CLASS_KEY)),
288                             uri,
289                             startForResult);
290         }
291 
292         @Override
toJson()293         public JSONObject toJson() throws JSONException {
294             return new JSONObject().put(FLAGS_KEY, this.humanReadableFlags())
295                     .put(CLASS_KEY, this.mComponentName.getClassName())
296                     .put(PACKAGE_KEY, this.mComponentName.getPackageName())
297                     .put(START_FOR_RESULT_KEY, mStartForResult);
298         }
299 
withFlags(IntentFlag... flags)300         public LaunchIntent withFlags(IntentFlag... flags) {
301             List<IntentFlag> intentFlags = Lists.newArrayList(mIntentFlags);
302             Collections.addAll(intentFlags, flags);
303             return new LaunchIntent(intentFlags, mComponentName, mData, mStartForResult);
304         }
305 
getIntentFlags()306         public List<IntentFlag> getIntentFlags() {
307             return mIntentFlags;
308         }
309 
getComponentName()310         public ComponentName getComponentName() {
311             return mComponentName;
312         }
313     }
314 
315     /**
316      * Representation of {@link android.content.Intent} used by the {@link LaunchSequence} api.
317      * It can used to normally start activities, to start activities for result and Intent Flags
318      * can
319      * be added using {@link LaunchIntent#withFlags(IntentFlag...)} just like {@link LaunchIntent}
320      *
321      * However {@link LaunchFromIntent}  also supports launching from a activity earlier in the
322      * launch sequence. This can be done using {@link LaunchSequence#act} and related methods.
323      */
324     static class LaunchFromIntent implements GenerationIntent {
325         static final String LAUNCH_FROM_KEY = "launchFrom";
326 
327         /**
328          * The underlying {@link LaunchIntent} that we are wrapping with the launch point behaviour.
329          */
330         private final LaunchIntent mLaunchIntent;
331 
332         /**
333          * The index in the activityLog maintained by {@link LaunchRunner}, used to retrieve the
334          * activity from the log to start this {@link LaunchIntent} from.
335          */
336         private final int mLaunchFrom;
337 
LaunchFromIntent(LaunchIntent fakeIntent, int launchFrom)338         LaunchFromIntent(LaunchIntent fakeIntent, int launchFrom) {
339             mLaunchIntent = fakeIntent;
340             mLaunchFrom = launchFrom;
341         }
342 
343 
344         @Override
getActualIntent()345         public Intent getActualIntent() {
346             return mLaunchIntent.getActualIntent();
347         }
348 
349         @Override
getLaunchFromIndex(int currentPosition)350         public int getLaunchFromIndex(int currentPosition) {
351             return mLaunchFrom;
352         }
353 
354         @Override
startForResult()355         public boolean startForResult() {
356             return mLaunchIntent.mStartForResult;
357         }
358 
359         @Override
toJson()360         public JSONObject toJson() throws JSONException {
361             return mLaunchIntent.toJson()
362                     .put(LAUNCH_FROM_KEY, mLaunchFrom);
363         }
364 
fromJson(JSONObject object, Map<String, IntentFlag> table)365         public static LaunchFromIntent fromJson(JSONObject object, Map<String, IntentFlag> table)
366                 throws JSONException {
367             LaunchIntent fakeIntent = LaunchIntent.fromJson(object, table);
368             int launchFrom = object.optInt(LAUNCH_FROM_KEY, -1);
369 
370             return new LaunchFromIntent(fakeIntent, launchFrom);
371         }
372 
prepareSerialisation(List<LaunchFromIntent> intents)373         static List<GenerationIntent> prepareSerialisation(List<LaunchFromIntent> intents) {
374             return prepareSerialisation(intents, 0);
375         }
376 
377         // In serialized form we only want to store the launch from index if it deviates from the
378         // default, the default being the previous activity.
prepareSerialisation(List<LaunchFromIntent> intents, int base)379         static List<GenerationIntent> prepareSerialisation(List<LaunchFromIntent> intents,
380                 int base) {
381             List<GenerationIntent> serializeIntents = Lists.newArrayList();
382             for (int i = 0; i < intents.size(); i++) {
383                 LaunchFromIntent launchFromIntent = intents.get(i);
384                 serializeIntents.add(launchFromIntent.forget(base + i));
385             }
386 
387             return serializeIntents;
388         }
389 
forget(int currentIndex)390         public GenerationIntent forget(int currentIndex) {
391             if (mLaunchFrom == currentIndex - 1) {
392                 return this.mLaunchIntent;
393             } else {
394                 return this;
395             }
396         }
397 
getLaunchFrom()398         public int getLaunchFrom() {
399             return mLaunchFrom;
400         }
401     }
402 
403     /**
404      * An intent flag that also stores the name of the flag.
405      * It is used to be able to put the flags in human readable form in the JSON file.
406      */
407     static class IntentFlag {
408         /**
409          * The underlying flag, should be a value from Intent.FLAG_ACTIVITY_*.
410          */
411         public final int flag;
412         /**
413          * The name of the flag.
414          */
415         public final String name;
416 
IntentFlag(int flag, String name)417         public IntentFlag(int flag, String name) {
418             this.flag = flag;
419             this.name = name;
420         }
421 
getFlag()422         public int getFlag() {
423             return flag;
424         }
425 
getName()426         public String getName() {
427             return name;
428         }
429 
combine(IntentFlag other)430         public int combine(IntentFlag other) {
431             return other.flag | flag;
432         }
433 
parse(Map<String, IntentFlag> names, String flagsToParse)434         public static List<IntentFlag> parse(Map<String, IntentFlag> names, String flagsToParse) {
435             String[] split = flagsToParse.replaceAll("\\s", "").split("\\|");
436             return Arrays.stream(split).map(names::get).collect(toList());
437         }
438 
toString()439         public String toString() {
440             return name;
441         }
442     }
443 
flag(int flag, String name)444     static IntentFlag flag(int flag, String name) {
445         return new IntentFlag(flag, name);
446     }
447 
448     public static class StateDump {
449         private static final String TASKS_KEY = "tasks";
450 
451         /**
452          * The Tasks in this stack ordered from most recent to least recent.
453          */
454         private final List<TaskState> mTasks;
455 
fromTasks(List<WindowManagerState.ActivityTask> activityTasks, List<WindowManagerState.ActivityTask> baseStacks)456         public static StateDump fromTasks(List<WindowManagerState.ActivityTask> activityTasks,
457                 List<WindowManagerState.ActivityTask> baseStacks) {
458             List<TaskState> tasks = new ArrayList<>();
459             for (WindowManagerState.ActivityTask task : trimTasks(activityTasks, baseStacks)) {
460                 tasks.add(new TaskState(task));
461             }
462             return new StateDump(tasks);
463         }
464 
StateDump(List<TaskState> tasks)465         private StateDump(List<TaskState> tasks) {
466             mTasks = tasks;
467         }
468 
toJson()469         JSONObject toJson() throws JSONException {
470             JSONArray tasks = new JSONArray();
471             for (TaskState task : mTasks) {
472                 tasks.put(task.toJson());
473             }
474 
475             return new JSONObject().put(TASKS_KEY, tasks);
476         }
477 
fromJson(JSONObject object)478         static StateDump fromJson(JSONObject object) throws JSONException {
479             JSONArray jsonTasks = object.getJSONArray(TASKS_KEY);
480             List<TaskState> tasks = new ArrayList<>();
481 
482             for (int i = 0; i < jsonTasks.length(); i++) {
483                 tasks.add(TaskState.fromJson((JSONObject) jsonTasks.get(i)));
484             }
485 
486             return new StateDump(tasks);
487         }
488 
489         /**
490          * To make the state dump non device specific we remove every task that was present
491          * in the system before recording, by their ID. For example a task containing the launcher
492          * activity.
493          */
trimTasks( List<WindowManagerState.ActivityTask> toTrim, List<WindowManagerState.ActivityTask> trimFrom)494         public static List<WindowManagerState.ActivityTask> trimTasks(
495                 List<WindowManagerState.ActivityTask> toTrim,
496                 List<WindowManagerState.ActivityTask> trimFrom) {
497 
498             for (WindowManagerState.ActivityTask task : trimFrom) {
499                 toTrim.removeIf(t -> t.getRootTaskId() == task.getRootTaskId());
500             }
501 
502             return toTrim;
503         }
504 
505         @Override
equals(Object o)506         public boolean equals(Object o) {
507             if (this == o) return true;
508             if (o == null || getClass() != o.getClass()) return false;
509             StateDump stateDump = (StateDump) o;
510             return Objects.equals(mTasks, stateDump.mTasks);
511         }
512 
513         @Override
hashCode()514         public int hashCode() {
515             return Objects.hash(mTasks);
516         }
517     }
518 
519     public static class TaskState {
520 
521         private static final String STATE_RESUMED = "RESUMED";
522         private static final String ACTIVITIES_KEY = "activities";
523 
524         /**
525          * The component name of the resumedActivity in this state, empty string if there is none.
526          */
527         private final String mResumedActivity;
528 
529         /**
530          * The activities in this task ordered from most recent to least recent.
531          */
532         private final List<ActivityState> mActivities = new ArrayList<>();
533 
TaskState(JSONArray jsonActivities)534         private TaskState(JSONArray jsonActivities) throws JSONException {
535             String resumedActivity = "";
536             for (int i = 0; i < jsonActivities.length(); i++) {
537                 final ActivityState activity =
538                         ActivityState.fromJson((JSONObject) jsonActivities.get(i));
539                 // The json file shouldn't define multiple resumed activities, but it is fine that
540                 // the test will fail when comparing to the real state.
541                 if (STATE_RESUMED.equals(activity.getState())) {
542                     resumedActivity = activity.getName();
543                 }
544                 mActivities.add(activity);
545             }
546 
547             mResumedActivity = resumedActivity;
548         }
549 
TaskState(WindowManagerState.ActivityTask state)550         public TaskState(WindowManagerState.ActivityTask state) {
551             final String resumedActivity = state.getResumedActivity();
552             mResumedActivity = resumedActivity != null ? resumedActivity : "";
553             for (WindowManagerState.Activity activity : state.getActivities()) {
554                 this.mActivities.add(new ActivityState(activity));
555             }
556         }
557 
toJson()558         JSONObject toJson() throws JSONException {
559             JSONArray jsonActivities = new JSONArray();
560 
561             for (ActivityState activity : mActivities) {
562                 jsonActivities.put(activity.toJson());
563             }
564 
565             return new JSONObject()
566                     .put(ACTIVITIES_KEY, jsonActivities);
567         }
568 
fromJson(JSONObject object)569         static TaskState fromJson(JSONObject object) throws JSONException {
570             return new TaskState(object.getJSONArray(ACTIVITIES_KEY));
571         }
572 
getActivities()573         public List<ActivityState> getActivities() {
574             return mActivities;
575         }
576 
577         @Override
equals(Object o)578         public boolean equals(Object o) {
579             if (this == o) return true;
580             if (o == null || getClass() != o.getClass()) return false;
581             TaskState task = (TaskState) o;
582             return Objects.equals(mResumedActivity, task.mResumedActivity)
583                     && Objects.equals(mActivities, task.mActivities);
584         }
585 
586         @Override
hashCode()587         public int hashCode() {
588             return Objects.hash(mResumedActivity, mActivities);
589         }
590     }
591 
592     public static class ActivityState {
593         private static final String NAME_KEY = "name";
594         private static final String STATE_KEY = "state";
595         /**
596          * The componentName of this activity.
597          */
598         private final String mComponentName;
599 
600         /**
601          * The lifecycle state this activity is in.
602          */
603         private final String mLifeCycleState;
604 
ActivityState(String name, String state)605         public ActivityState(String name, String state) {
606             mComponentName = name;
607             mLifeCycleState = state;
608         }
609 
ActivityState(WindowManagerState.Activity activity)610         public ActivityState(WindowManagerState.Activity activity) {
611             mComponentName = activity.getName();
612             mLifeCycleState = activity.getState();
613         }
614 
615 
toJson()616         JSONObject toJson() throws JSONException {
617             return new JSONObject().put(NAME_KEY, mComponentName).put(STATE_KEY, mLifeCycleState);
618         }
619 
fromJson(JSONObject object)620         static ActivityState fromJson(JSONObject object) throws JSONException {
621             return new ActivityState(object.getString(NAME_KEY), object.getString(STATE_KEY));
622         }
623 
624         @Override
equals(Object o)625         public boolean equals(Object o) {
626             if (this == o) return true;
627             if (o == null || getClass() != o.getClass()) return false;
628             ActivityState activity = (ActivityState) o;
629             return Objects.equals(mComponentName, activity.mComponentName) &&
630                     Objects.equals(mLifeCycleState, activity.mLifeCycleState);
631         }
632 
633         @Override
hashCode()634         public int hashCode() {
635             return Objects.hash(mComponentName, mLifeCycleState);
636         }
637 
getName()638         public String getName() {
639             return mComponentName;
640         }
641 
getState()642         public String getState() {
643             return mLifeCycleState;
644         }
645     }
646 }
647