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