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