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