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