1 /* 2 * Copyright (C) 2018 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; 18 19 import android.app.Activity; 20 import android.app.Application; 21 import android.content.BroadcastReceiver; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentFilter; 26 import android.content.res.Configuration; 27 import android.content.res.Resources; 28 import android.graphics.Point; 29 import android.os.Build; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.os.HandlerThread; 33 import android.os.Looper; 34 import android.os.Parcel; 35 import android.os.Parcelable; 36 import android.os.Process; 37 import android.os.SystemClock; 38 import android.server.wm.TestJournalProvider.TestJournalClient; 39 import android.util.ArrayMap; 40 import android.util.DisplayMetrics; 41 import android.util.Log; 42 import android.view.Display; 43 import android.view.View; 44 45 import java.util.ArrayList; 46 import java.util.Iterator; 47 import java.util.concurrent.TimeoutException; 48 import java.util.function.Consumer; 49 50 /** 51 * A mechanism for communication between the started activity and its caller in different package or 52 * process. Generally, a test case is the client, and the testing activity is the host. The client 53 * can control whether to send an async or sync command with response data. 54 * <p>Sample:</p> 55 * <pre> 56 * try (ActivitySessionClient client = new ActivitySessionClient(context)) { 57 * final ActivitySession session = client.startActivity( 58 * new Intent(context, TestActivity.class)); 59 * final Bundle response = session.requestOrientation( 60 * ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 61 * Log.i("Test", "Config: " + CommandSession.getConfigInfo(response)); 62 * Log.i("Test", "Callbacks: " + CommandSession.getCallbackHistory(response)); 63 * 64 * session.startActivity(session.getOriginalLaunchIntent()); 65 * Log.i("Test", "New intent callbacks: " + session.takeCallbackHistory()); 66 * } 67 * </pre> 68 * <p>To perform custom command, use sendCommand* in {@link ActivitySession} to send the request, 69 * and the receiving side (activity) can extend {@link BasicTestActivity} or 70 * {@link CommandSessionActivity} with overriding handleCommand to do the corresponding action.</p> 71 */ 72 public final class CommandSession { 73 private static final boolean DEBUG = "eng".equals(Build.TYPE); 74 private static final String TAG = "CommandSession"; 75 76 private static final String EXTRA_PREFIX = "s_"; 77 78 static final String KEY_FORWARD = EXTRA_PREFIX + "key_forward"; 79 80 private static final String KEY_CALLBACK_HISTORY = EXTRA_PREFIX + "key_callback_history"; 81 private static final String KEY_CLIENT_ID = EXTRA_PREFIX + "key_client_id"; 82 private static final String KEY_COMMAND = EXTRA_PREFIX + "key_command"; 83 private static final String KEY_CONFIG_INFO = EXTRA_PREFIX + "key_config_info"; 84 private static final String KEY_APP_CONFIG_INFO = EXTRA_PREFIX + "key_app_config_info"; 85 private static final String KEY_HOST_ID = EXTRA_PREFIX + "key_host_id"; 86 private static final String KEY_ORIENTATION = EXTRA_PREFIX + "key_orientation"; 87 private static final String KEY_REQUEST_TOKEN = EXTRA_PREFIX + "key_request_id"; 88 private static final String KEY_UID_HAS_ACCESS_ON_DISPLAY = 89 EXTRA_PREFIX + "uid_has_access_on_display"; 90 91 private static final String COMMAND_FINISH = EXTRA_PREFIX + "command_finish"; 92 private static final String COMMAND_GET_CONFIG = EXTRA_PREFIX + "command_get_config"; 93 private static final String COMMAND_GET_APP_CONFIG = EXTRA_PREFIX + "command_get_app_config"; 94 private static final String COMMAND_ORIENTATION = EXTRA_PREFIX + "command_orientation"; 95 private static final String COMMAND_TAKE_CALLBACK_HISTORY = EXTRA_PREFIX 96 + "command_take_callback_history"; 97 private static final String COMMAND_WAIT_IDLE = EXTRA_PREFIX + "command_wait_idle"; 98 private static final String COMMAND_GET_NAME = EXTRA_PREFIX + "command_get_name"; 99 private static final String COMMAND_DISPLAY_ACCESS_CHECK = 100 EXTRA_PREFIX + "display_access_check"; 101 102 private static final long INVALID_REQUEST_TOKEN = -1; 103 CommandSession()104 private CommandSession() { 105 } 106 107 /** Get {@link ConfigInfo} from bundle. */ getConfigInfo(Bundle data)108 public static ConfigInfo getConfigInfo(Bundle data) { 109 return data.getParcelable(KEY_CONFIG_INFO); 110 } 111 112 /** Get application {@link ConfigInfo} from bundle. */ getAppConfigInfo(Bundle data)113 public static ConfigInfo getAppConfigInfo(Bundle data) { 114 return data.getParcelable(KEY_APP_CONFIG_INFO); 115 } 116 117 /** Get list of {@link ActivityCallback} from bundle. */ getCallbackHistory(Bundle data)118 public static ArrayList<ActivityCallback> getCallbackHistory(Bundle data) { 119 return data.getParcelableArrayList(KEY_CALLBACK_HISTORY); 120 } 121 122 /** Return non-null if the session info should forward to launch target. */ handleForward(Bundle data)123 public static LaunchInjector handleForward(Bundle data) { 124 if (data == null || !data.getBoolean(KEY_FORWARD)) { 125 return null; 126 } 127 128 // Only keep the necessary data which relates to session. 129 final Bundle sessionInfo = new Bundle(data); 130 sessionInfo.remove(KEY_FORWARD); 131 for (String key : sessionInfo.keySet()) { 132 if (key != null && !key.startsWith(EXTRA_PREFIX)) { 133 sessionInfo.remove(key); 134 } 135 } 136 137 return new LaunchInjector() { 138 @Override 139 public void setupIntent(Intent intent) { 140 intent.putExtras(sessionInfo); 141 } 142 143 @Override 144 public void setupShellCommand(StringBuilder shellCommand) { 145 // Currently there is no use case from shell. 146 throw new UnsupportedOperationException(); 147 } 148 }; 149 } 150 151 private static String generateId(String prefix, Object obj) { 152 return prefix + "_" + Integer.toHexString(System.identityHashCode(obj)); 153 } 154 155 private static String commandIntentToString(Intent intent) { 156 return intent.getStringExtra(KEY_COMMAND) 157 + "@" + intent.getLongExtra(KEY_REQUEST_TOKEN, INVALID_REQUEST_TOKEN); 158 } 159 160 /** Get an unique token to match the request and reply. */ 161 private static long generateRequestToken() { 162 return SystemClock.elapsedRealtimeNanos(); 163 } 164 165 /** 166 * As a controller associated with the testing activity. It can only process one sync command 167 * (require response) at a time. 168 */ 169 public static class ActivitySession { 170 private final ActivitySessionClient mClient; 171 private final String mHostId; 172 private final Response mPendingResponse = new Response(); 173 // Only set when requiring response. 174 private long mPendingRequestToken = INVALID_REQUEST_TOKEN; 175 private String mPendingCommand; 176 private boolean mFinished; 177 private Intent mOriginalLaunchIntent; 178 179 ActivitySession(ActivitySessionClient client, boolean requireReply) { 180 mClient = client; 181 mHostId = generateId("activity", this); 182 if (requireReply) { 183 mPendingRequestToken = generateRequestToken(); 184 mPendingCommand = COMMAND_WAIT_IDLE; 185 } 186 } 187 188 /** Start the activity again. The intent must have the same filter as original one. */ 189 public void startActivity(Intent intent) { 190 if (!intent.filterEquals(mOriginalLaunchIntent)) { 191 throw new IllegalArgumentException("The intent filter is different " + intent); 192 } 193 mClient.mContext.startActivity(intent); 194 mFinished = false; 195 } 196 197 /** 198 * Request the activity to set the given orientation. The returned bundle contains the 199 * changed config info and activity lifecycles during the change. 200 * 201 * @param orientation An orientation constant as used in 202 * {@link android.content.pm.ActivityInfo#screenOrientation}. 203 */ 204 public Bundle requestOrientation(int orientation) { 205 final Bundle data = new Bundle(); 206 data.putInt(KEY_ORIENTATION, orientation); 207 return sendCommandAndWaitReply(COMMAND_ORIENTATION, data); 208 } 209 210 /** Get {@link ConfigInfo} of the associated activity. */ 211 public ConfigInfo getConfigInfo() { 212 return CommandSession.getConfigInfo(sendCommandAndWaitReply(COMMAND_GET_CONFIG)); 213 } 214 215 /** Get {@link ConfigInfo} of the Application of the associated activity. */ 216 public ConfigInfo getAppConfigInfo() { 217 return CommandSession.getAppConfigInfo(sendCommandAndWaitReply(COMMAND_GET_APP_CONFIG)); 218 } 219 220 /** 221 * Get executed callbacks of the activity since the last command. The current callback 222 * history will also be cleared. 223 */ 224 public ArrayList<ActivityCallback> takeCallbackHistory() { 225 return getCallbackHistory(sendCommandAndWaitReply(COMMAND_TAKE_CALLBACK_HISTORY, 226 null /* data */)); 227 } 228 229 /** Get the intent that launches the activity. Null if launch from shell command. */ 230 public Intent getOriginalLaunchIntent() { 231 return mOriginalLaunchIntent; 232 } 233 234 /** Get a name to represent this session by the original launch intent if possible. */ 235 public ComponentName getName() { 236 if (mOriginalLaunchIntent != null) { 237 final ComponentName componentName = mOriginalLaunchIntent.getComponent(); 238 if (componentName != null) { 239 return componentName; 240 } 241 } 242 return sendCommandAndWaitReply(COMMAND_GET_NAME, null /* data */) 243 .getParcelable(COMMAND_GET_NAME); 244 } 245 246 public boolean isUidAccesibleOnDisplay() { 247 return sendCommandAndWaitReply(COMMAND_DISPLAY_ACCESS_CHECK, null).getBoolean( 248 KEY_UID_HAS_ACCESS_ON_DISPLAY); 249 } 250 251 /** Send command to the associated activity. */ 252 public void sendCommand(String command) { 253 sendCommand(command, null /* data */); 254 } 255 256 /** Send command with extra parameters to the associated activity. */ 257 public void sendCommand(String command, Bundle data) { 258 if (mFinished) { 259 throw new IllegalStateException("The session is finished"); 260 } 261 262 final Intent intent = new Intent(mHostId); 263 if (data != null) { 264 intent.putExtras(data); 265 } 266 intent.putExtra(KEY_COMMAND, command); 267 mClient.mContext.sendBroadcast(intent); 268 if (DEBUG) { 269 Log.i(TAG, mClient.mClientId + " sends " + commandIntentToString(intent) 270 + " to " + mHostId); 271 } 272 } 273 274 public Bundle sendCommandAndWaitReply(String command) { 275 return sendCommandAndWaitReply(command, null /* data */); 276 } 277 278 /** Returns the reply data by the given command. */ 279 public Bundle sendCommandAndWaitReply(String command, Bundle data) { 280 if (data == null) { 281 data = new Bundle(); 282 } 283 284 if (mPendingRequestToken != INVALID_REQUEST_TOKEN) { 285 throw new IllegalStateException("The previous pending request " 286 + mPendingCommand + " has not replied"); 287 } 288 mPendingRequestToken = generateRequestToken(); 289 mPendingCommand = command; 290 data.putLong(KEY_REQUEST_TOKEN, mPendingRequestToken); 291 292 sendCommand(command, data); 293 return waitReply(); 294 } 295 296 private Bundle waitReply() { 297 if (mPendingRequestToken == INVALID_REQUEST_TOKEN) { 298 throw new IllegalStateException("No pending request to wait"); 299 } 300 301 if (DEBUG) Log.i(TAG, "Waiting for request " + mPendingRequestToken); 302 try { 303 return mPendingResponse.takeResult(); 304 } catch (TimeoutException e) { 305 throw new RuntimeException("Timeout on command " 306 + mPendingCommand + " with token " + mPendingRequestToken, e); 307 } finally { 308 mPendingRequestToken = INVALID_REQUEST_TOKEN; 309 mPendingCommand = null; 310 } 311 } 312 313 // This method should run on an independent thread. 314 void receiveReply(Bundle reply) { 315 final long incomingToken = reply.getLong(KEY_REQUEST_TOKEN); 316 if (incomingToken == mPendingRequestToken) { 317 mPendingResponse.setResult(reply); 318 } else { 319 throw new IllegalStateException("Mismatched token: incoming=" + incomingToken 320 + " pending=" + mPendingRequestToken); 321 } 322 } 323 324 /** Finish the activity that associates with this session. */ 325 public void finish() { 326 if (!mFinished) { 327 sendCommand(COMMAND_FINISH); 328 mClient.mSessions.remove(mHostId); 329 mFinished = true; 330 } 331 } 332 333 private static class Response { 334 static final int TIMEOUT_MILLIS = 5000; 335 private volatile boolean mHasResult; 336 private Bundle mResult; 337 338 synchronized void setResult(Bundle result) { 339 mHasResult = true; 340 mResult = result; 341 notifyAll(); 342 } 343 344 synchronized Bundle takeResult() throws TimeoutException { 345 final long startTime = SystemClock.uptimeMillis(); 346 while (!mHasResult) { 347 try { 348 wait(TIMEOUT_MILLIS); 349 } catch (InterruptedException ignored) { 350 } 351 if (!mHasResult && (SystemClock.uptimeMillis() - startTime > TIMEOUT_MILLIS)) { 352 throw new TimeoutException("No response over " + TIMEOUT_MILLIS + "ms"); 353 } 354 } 355 356 final Bundle result = mResult; 357 mHasResult = false; 358 mResult = null; 359 return result; 360 } 361 } 362 } 363 364 /** For LaunchProxy to setup launch parameter that establishes session. */ 365 interface LaunchInjector { 366 void setupIntent(Intent intent); 367 void setupShellCommand(StringBuilder shellCommand); 368 } 369 370 /** A proxy to launch activity by intent or shell command. */ 371 interface LaunchProxy { 372 void setLaunchInjector(LaunchInjector injector); 373 default Bundle getExtras() { return null; } 374 void execute(); 375 boolean shouldWaitForLaunched(); 376 } 377 378 abstract static class DefaultLaunchProxy implements LaunchProxy { 379 LaunchInjector mLaunchInjector; 380 381 @Override 382 public boolean shouldWaitForLaunched() { 383 return true; 384 } 385 386 @Override 387 public void setLaunchInjector(LaunchInjector injector) { 388 mLaunchInjector = injector; 389 } 390 } 391 392 /** Created by test case to control testing activity that implements the session protocol. */ 393 public static class ActivitySessionClient extends BroadcastReceiver implements AutoCloseable { 394 private final Context mContext; 395 private final String mClientId; 396 private final HandlerThread mThread; 397 private final ArrayMap<String, ActivitySession> mSessions = new ArrayMap<>(); 398 private boolean mClosed; 399 400 public ActivitySessionClient(Context context) { 401 mContext = context; 402 mClientId = generateId("testcase", this); 403 mThread = new HandlerThread(mClientId); 404 mThread.start(); 405 context.registerReceiver(this, new IntentFilter(mClientId), 406 null /* broadcastPermission */, new Handler(mThread.getLooper()), 407 Context.RECEIVER_EXPORTED); 408 } 409 410 /** Start the activity by the given intent and wait it becomes idle. */ 411 public ActivitySession startActivity(Intent intent) { 412 return startActivity(intent, null /* options */, true /* waitIdle */); 413 } 414 415 /** 416 * Launch the activity and establish a new session. 417 * 418 * @param intent The description of the activity to start. 419 * @param options Additional options for how the Activity should be started. 420 * @param waitIdle Block in this method until the target activity is idle. 421 * @return The session to communicate with the started activity. 422 */ 423 public ActivitySession startActivity(Intent intent, Bundle options, boolean waitIdle) { 424 ensureNotClosed(); 425 final ActivitySession session = new ActivitySession(this, waitIdle); 426 mSessions.put(session.mHostId, session); 427 setupLaunchIntent(intent, waitIdle, session); 428 429 if (!(mContext instanceof Activity)) { 430 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 431 } 432 mContext.startActivity(intent, options); 433 if (waitIdle) { 434 session.waitReply(); 435 } 436 return session; 437 } 438 439 /** Launch activity via proxy that allows to inject session parameters. */ 440 public ActivitySession startActivity(LaunchProxy proxy) { 441 ensureNotClosed(); 442 final boolean waitIdle = proxy.shouldWaitForLaunched(); 443 final ActivitySession session = new ActivitySession(this, waitIdle); 444 mSessions.put(session.mHostId, session); 445 446 proxy.setLaunchInjector(new LaunchInjector() { 447 @Override 448 public void setupIntent(Intent intent) { 449 final Bundle bundle = proxy.getExtras(); 450 if (bundle != null) { 451 intent.putExtras(bundle); 452 } 453 setupLaunchIntent(intent, waitIdle, session); 454 } 455 456 @Override 457 public void setupShellCommand(StringBuilder commandBuilder) { 458 commandBuilder.append(" --es " + KEY_HOST_ID + " " + session.mHostId); 459 commandBuilder.append(" --es " + KEY_CLIENT_ID + " " + mClientId); 460 if (waitIdle) { 461 commandBuilder.append( 462 " --el " + KEY_REQUEST_TOKEN + " " + session.mPendingRequestToken); 463 commandBuilder.append(" --es " + KEY_COMMAND + " " + COMMAND_WAIT_IDLE); 464 } 465 } 466 }); 467 468 proxy.execute(); 469 if (waitIdle) { 470 session.waitReply(); 471 } 472 return session; 473 } 474 475 private void setupLaunchIntent(Intent intent, boolean waitIdle, ActivitySession session) { 476 intent.putExtra(KEY_HOST_ID, session.mHostId); 477 intent.putExtra(KEY_CLIENT_ID, mClientId); 478 if (waitIdle) { 479 intent.putExtra(KEY_REQUEST_TOKEN, session.mPendingRequestToken); 480 intent.putExtra(KEY_COMMAND, COMMAND_WAIT_IDLE); 481 } 482 session.mOriginalLaunchIntent = intent; 483 } 484 485 public ActivitySession getLastStartedSession() { 486 if (mSessions.isEmpty()) { 487 throw new IllegalStateException("No started sessions"); 488 } 489 return mSessions.valueAt(mSessions.size() - 1); 490 } 491 492 private void ensureNotClosed() { 493 if (mClosed) { 494 throw new IllegalStateException("This session client is closed."); 495 } 496 } 497 498 @Override 499 public void onReceive(Context context, Intent intent) { 500 final ActivitySession session = mSessions.get(intent.getStringExtra(KEY_HOST_ID)); 501 if (DEBUG) Log.i(TAG, mClientId + " receives " + commandIntentToString(intent)); 502 if (session != null) { 503 session.receiveReply(intent.getExtras()); 504 } else { 505 Log.w(TAG, "No available session for " + commandIntentToString(intent)); 506 } 507 } 508 509 /** Complete cleanup with finishing all associated activities. */ 510 @Override 511 public void close() { 512 close(true /* finishSession */); 513 } 514 515 /** Cleanup except finish associated activities. */ 516 public void closeAndKeepSession() { 517 close(false /* finishSession */); 518 } 519 520 /** 521 * Closes this client. Once a client is closed, all methods on it will throw an 522 * IllegalStateException and all responses from host are ignored. 523 * 524 * @param finishSession Whether to finish activities launched from this client. 525 */ 526 public void close(boolean finishSession) { 527 ensureNotClosed(); 528 mClosed = true; 529 if (finishSession) { 530 for (int i = mSessions.size() - 1; i >= 0; i--) { 531 mSessions.valueAt(i).finish(); 532 } 533 } 534 mContext.unregisterReceiver(this); 535 mThread.quit(); 536 } 537 } 538 539 /** 540 * Interface definition for session host to process command from {@link ActivitySessionClient}. 541 */ 542 interface CommandReceiver { 543 /** Called when the session host is receiving command. */ 544 void receiveCommand(String command, Bundle data); 545 } 546 547 /** The host receives command from the test client. */ 548 public static class ActivitySessionHost extends BroadcastReceiver { 549 private final Context mContext; 550 private final String mClientId; 551 private final String mHostId; 552 private CommandReceiver mCallback; 553 /** The intents received when the host activity is relaunching. */ 554 private ArrayList<Intent> mPendingIntents; 555 556 ActivitySessionHost(Context context, String hostId, String clientId, 557 CommandReceiver callback) { 558 mContext = context; 559 mHostId = hostId; 560 mClientId = clientId; 561 mCallback = callback; 562 context.registerReceiver(this, new IntentFilter(hostId), Context.RECEIVER_EXPORTED); 563 } 564 565 @Override 566 public void onReceive(Context context, Intent intent) { 567 if (DEBUG) { 568 Log.i(TAG, mHostId + "(" 569 + (mCallback != null 570 ? mCallback.getClass().getName() 571 : mContext.getClass().getName()) 572 + ") receives " + commandIntentToString(intent)); 573 } 574 if (mCallback == null) { 575 if (mPendingIntents == null) { 576 mPendingIntents = new ArrayList<>(); 577 } 578 mPendingIntents.add(intent); 579 return; 580 } 581 dispatchCommand(mCallback, intent); 582 } 583 584 private static void dispatchCommand(CommandReceiver callback, Intent intent) { 585 callback.receiveCommand(intent.getStringExtra(KEY_COMMAND), intent.getExtras()); 586 } 587 588 void reply(String command, Bundle data) { 589 final Intent intent = new Intent(mClientId); 590 intent.putExtras(data); 591 intent.putExtra(KEY_COMMAND, command); 592 intent.putExtra(KEY_HOST_ID, mHostId); 593 mContext.sendBroadcast(intent); 594 if (DEBUG) { 595 Log.i(TAG, mHostId + "(" + mContext.getClass().getSimpleName() 596 + ") replies " + commandIntentToString(intent) + " to " + mClientId); 597 } 598 } 599 600 void setCallback(CommandReceiver callback) { 601 if (mPendingIntents != null && mCallback == null && callback != null) { 602 for (Intent intent : mPendingIntents) { 603 dispatchCommand(callback, intent); 604 } 605 mPendingIntents = null; 606 } 607 mCallback = callback; 608 } 609 610 void destroy() { 611 mContext.unregisterReceiver(this); 612 } 613 } 614 615 /** 616 * A map to store data by host id. The usage should be declared as static that is able to keep 617 * data after activity is relaunched. 618 */ 619 private static class StaticHostStorage<T> { 620 final ArrayMap<String, ArrayList<T>> mStorage = new ArrayMap<>(); 621 622 void add(String hostId, T data) { 623 ArrayList<T> commands = mStorage.get(hostId); 624 if (commands == null) { 625 commands = new ArrayList<>(); 626 mStorage.put(hostId, commands); 627 } 628 commands.add(data); 629 } 630 631 ArrayList<T> get(String hostId) { 632 return mStorage.get(hostId); 633 } 634 635 void clear(String hostId) { 636 mStorage.remove(hostId); 637 } 638 } 639 640 /** Store the commands which have not been handled. */ 641 private static class CommandStorage extends StaticHostStorage<Bundle> { 642 643 /** Remove the oldest matched command and return its request token. */ 644 long consume(String hostId, String command) { 645 final ArrayList<Bundle> commands = mStorage.get(hostId); 646 if (commands != null) { 647 final Iterator<Bundle> iterator = commands.iterator(); 648 while (iterator.hasNext()) { 649 final Bundle data = iterator.next(); 650 if (command.equals(data.getString(KEY_COMMAND))) { 651 iterator.remove(); 652 return data.getLong(KEY_REQUEST_TOKEN); 653 } 654 } 655 if (commands.isEmpty()) { 656 clear(hostId); 657 } 658 } 659 return INVALID_REQUEST_TOKEN; 660 } 661 662 boolean containsCommand(String receiverId, String command) { 663 final ArrayList<Bundle> dataList = mStorage.get(receiverId); 664 if (dataList != null) { 665 for (Bundle data : dataList) { 666 if (command.equals(data.getString(KEY_COMMAND))) { 667 return true; 668 } 669 } 670 } 671 return false; 672 } 673 } 674 675 /** 676 * The base activity which supports the session protocol. If the caller does not use 677 * {@link ActivitySessionClient}, it behaves as a normal activity. 678 */ 679 public static class CommandSessionActivity extends Activity implements CommandReceiver { 680 /** Static command storage for across relaunch. */ 681 private static CommandStorage sCommandStorage; 682 private ActivitySessionHost mReceiver; 683 684 /** The subclasses can disable the test journal client if its information is not used. */ 685 protected boolean mUseTestJournal = true; 686 protected TestJournalClient mTestJournalClient; 687 688 @Override 689 protected void onCreate(Bundle savedInstanceState) { 690 super.onCreate(savedInstanceState); 691 if (mUseTestJournal) { 692 mTestJournalClient = TestJournalClient.create(this /* context */, 693 getComponentName()); 694 } 695 696 final String hostId = getIntent().getStringExtra(KEY_HOST_ID); 697 final String clientId = getIntent().getStringExtra(KEY_CLIENT_ID); 698 if (hostId != null && clientId != null) { 699 if (sCommandStorage == null) { 700 sCommandStorage = new CommandStorage(); 701 } 702 final Object receiver = getLastNonConfigurationInstance(); 703 if (receiver instanceof ActivitySessionHost) { 704 mReceiver = (ActivitySessionHost) receiver; 705 mReceiver.setCallback(this); 706 } else { 707 mReceiver = new ActivitySessionHost(getApplicationContext(), hostId, clientId, 708 this /* callback */); 709 } 710 } 711 } 712 713 @Override 714 protected void onDestroy() { 715 super.onDestroy(); 716 if (isChangingConfigurations()) { 717 // Detach the callback if the activity is relaunching. The callback will be 718 // associated again in onCreate. 719 if (mReceiver != null) { 720 mReceiver.setCallback(null); 721 } 722 } else if (mReceiver != null) { 723 // Clean up for real removal. 724 sCommandStorage.clear(getHostId()); 725 mReceiver.destroy(); 726 mReceiver = null; 727 } 728 if (mTestJournalClient != null) { 729 mTestJournalClient.close(); 730 } 731 } 732 733 @Override 734 public Object onRetainNonConfigurationInstance() { 735 return mReceiver; 736 } 737 738 @Override 739 public final void receiveCommand(String command, Bundle data) { 740 if (mReceiver == null) { 741 throw new IllegalStateException("The receiver is not created"); 742 } 743 sCommandStorage.add(getHostId(), data); 744 handleCommand(command, data); 745 } 746 747 /** Handle the incoming command from client. */ 748 protected void handleCommand(String command, Bundle data) { 749 } 750 751 protected final void reply(String command) { 752 reply(command, null /* data */); 753 } 754 755 /** Reply data to client for the command. */ 756 protected final void reply(String command, Bundle data) { 757 if (mReceiver == null) { 758 throw new IllegalStateException("The receiver is not created"); 759 } 760 final long requestToke = sCommandStorage.consume(getHostId(), command); 761 if (requestToke == INVALID_REQUEST_TOKEN) { 762 throw new IllegalStateException("There is no pending command " + command); 763 } 764 if (data == null) { 765 data = new Bundle(); 766 } 767 data.putLong(KEY_REQUEST_TOKEN, requestToke); 768 mReceiver.reply(command, data); 769 } 770 771 protected boolean hasPendingCommand(String command) { 772 return mReceiver != null && sCommandStorage.containsCommand(getHostId(), command); 773 } 774 775 /** Returns null means this activity does support the session protocol. */ 776 final String getHostId() { 777 return mReceiver != null ? mReceiver.mHostId : null; 778 } 779 } 780 781 /** The default implementation that supports basic commands to interact with activity. */ 782 public static class BasicTestActivity extends CommandSessionActivity { 783 /** Static callback history for across relaunch. */ 784 private static final StaticHostStorage<ActivityCallback> sCallbackStorage = 785 new StaticHostStorage<>(); 786 787 private final String mTag = getClass().getSimpleName(); 788 protected boolean mPrintCallbackLog; 789 790 @Override 791 protected void onCreate(Bundle savedInstanceState) { 792 super.onCreate(savedInstanceState); 793 onCallback(ActivityCallback.ON_CREATE); 794 795 if (getHostId() != null) { 796 final int orientation = getIntent().getIntExtra(KEY_ORIENTATION, Integer.MIN_VALUE); 797 if (orientation != Integer.MIN_VALUE) { 798 setRequestedOrientation(orientation); 799 } 800 if (COMMAND_WAIT_IDLE.equals(getIntent().getStringExtra(KEY_COMMAND))) { 801 receiveCommand(COMMAND_WAIT_IDLE, getIntent().getExtras()); 802 // No need to execute again if the activity is relaunched. 803 getIntent().removeExtra(KEY_COMMAND); 804 } 805 } 806 } 807 808 @Override 809 public void handleCommand(String command, Bundle data) { 810 switch (command) { 811 case COMMAND_ORIENTATION: 812 clearCallbackHistory(); 813 setRequestedOrientation(data.getInt(KEY_ORIENTATION)); 814 getWindow().getDecorView().postDelayed(() -> { 815 if (reportConfigIfNeeded()) { 816 Log.w(getTag(), "Fallback report. The orientation may not change."); 817 } 818 }, ActivitySession.Response.TIMEOUT_MILLIS / 2); 819 break; 820 821 case COMMAND_GET_CONFIG: 822 runWhenIdle(() -> { 823 final Bundle replyData = new Bundle(); 824 replyData.putParcelable(KEY_CONFIG_INFO, getConfigInfo()); 825 reply(COMMAND_GET_CONFIG, replyData); 826 }); 827 break; 828 829 case COMMAND_GET_APP_CONFIG: 830 runWhenIdle(() -> { 831 final Bundle replyData = new Bundle(); 832 replyData.putParcelable(KEY_APP_CONFIG_INFO, getAppConfigInfo()); 833 reply(COMMAND_GET_APP_CONFIG, replyData); 834 }); 835 break; 836 837 case COMMAND_FINISH: 838 if (!isFinishing()) { 839 finish(); 840 } 841 break; 842 843 case COMMAND_TAKE_CALLBACK_HISTORY: 844 final Bundle replyData = new Bundle(); 845 replyData.putParcelableArrayList(KEY_CALLBACK_HISTORY, getCallbackHistory()); 846 reply(command, replyData); 847 clearCallbackHistory(); 848 break; 849 850 case COMMAND_WAIT_IDLE: 851 runWhenIdle(() -> reply(command)); 852 break; 853 854 case COMMAND_GET_NAME: { 855 final Bundle result = new Bundle(); 856 result.putParcelable(COMMAND_GET_NAME, getComponentName()); 857 reply(COMMAND_GET_NAME, result); 858 break; 859 } 860 861 case COMMAND_DISPLAY_ACCESS_CHECK: 862 final Bundle result = new Bundle(); 863 final boolean displayHasAccess = getDisplay().hasAccess(Process.myUid()); 864 result.putBoolean(KEY_UID_HAS_ACCESS_ON_DISPLAY, displayHasAccess); 865 reply(command, result); 866 break; 867 868 default: 869 break; 870 } 871 } 872 873 protected final void clearCallbackHistory() { 874 sCallbackStorage.clear(getHostId()); 875 } 876 877 protected final ArrayList<ActivityCallback> getCallbackHistory() { 878 return sCallbackStorage.get(getHostId()); 879 } 880 881 protected void runWhenIdle(Runnable r) { 882 Looper.getMainLooper().getQueue().addIdleHandler(() -> { 883 r.run(); 884 return false; 885 }); 886 } 887 888 protected boolean reportConfigIfNeeded() { 889 if (!hasPendingCommand(COMMAND_ORIENTATION)) { 890 return false; 891 } 892 runWhenIdle(() -> { 893 final Bundle replyData = new Bundle(); 894 replyData.putParcelable(KEY_CONFIG_INFO, getConfigInfo()); 895 replyData.putParcelableArrayList(KEY_CALLBACK_HISTORY, getCallbackHistory()); 896 reply(COMMAND_ORIENTATION, replyData); 897 clearCallbackHistory(); 898 }); 899 return true; 900 } 901 902 @Override 903 protected void onStart() { 904 super.onStart(); 905 onCallback(ActivityCallback.ON_START); 906 } 907 908 @Override 909 protected void onRestart() { 910 super.onRestart(); 911 onCallback(ActivityCallback.ON_RESTART); 912 } 913 914 @Override 915 protected void onResume() { 916 super.onResume(); 917 onCallback(ActivityCallback.ON_RESUME); 918 reportConfigIfNeeded(); 919 } 920 921 @Override 922 protected void onPause() { 923 super.onPause(); 924 onCallback(ActivityCallback.ON_PAUSE); 925 } 926 927 @Override 928 protected void onStop() { 929 super.onStop(); 930 onCallback(ActivityCallback.ON_STOP); 931 } 932 933 @Override 934 protected void onDestroy() { 935 super.onDestroy(); 936 onCallback(ActivityCallback.ON_DESTROY); 937 } 938 939 @Override 940 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 941 super.onActivityResult(requestCode, resultCode, data); 942 onCallback(ActivityCallback.ON_ACTIVITY_RESULT); 943 } 944 945 @Override 946 protected void onUserLeaveHint() { 947 super.onUserLeaveHint(); 948 onCallback(ActivityCallback.ON_USER_LEAVE_HINT); 949 } 950 951 @Override 952 protected void onNewIntent(Intent intent) { 953 super.onNewIntent(intent); 954 onCallback(ActivityCallback.ON_NEW_INTENT); 955 } 956 957 @Override 958 public void onConfigurationChanged(Configuration newConfig) { 959 super.onConfigurationChanged(newConfig); 960 onCallback(ActivityCallback.ON_CONFIGURATION_CHANGED); 961 reportConfigIfNeeded(); 962 } 963 964 @Override 965 public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) { 966 super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig); 967 onCallback(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED); 968 } 969 970 @Override 971 public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, 972 Configuration newConfig) { 973 super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig); 974 onCallback(ActivityCallback.ON_PICTURE_IN_PICTURE_MODE_CHANGED); 975 } 976 977 @Override 978 public void onMovedToDisplay(int displayId, Configuration config) { 979 super.onMovedToDisplay(displayId, config); 980 onCallback(ActivityCallback.ON_MOVED_TO_DISPLAY); 981 } 982 983 public void onCallback(ActivityCallback callback) { 984 if (mPrintCallbackLog) { 985 Log.i(getTag(), callback + " @ " 986 + Integer.toHexString(System.identityHashCode(this))); 987 } 988 final String hostId = getHostId(); 989 if (hostId != null) { 990 sCallbackStorage.add(hostId, callback); 991 } 992 if (mTestJournalClient != null) { 993 mTestJournalClient.addCallback(callback); 994 } 995 } 996 997 protected void withTestJournalClient(Consumer<TestJournalClient> client) { 998 if (mTestJournalClient != null) { 999 client.accept(mTestJournalClient); 1000 } 1001 } 1002 1003 protected String getTag() { 1004 return mTag; 1005 } 1006 1007 /** Get configuration and display info. It should be called only after resumed. */ 1008 protected ConfigInfo getConfigInfo() { 1009 final View view = getWindow().getDecorView(); 1010 if (!view.isAttachedToWindow()) { 1011 Log.w(getTag(), "Decor view has not attached"); 1012 } 1013 return new ConfigInfo(view.getContext(), view.getDisplay()); 1014 } 1015 1016 /** Same as {@link #getConfigInfo()}, but for Application. */ 1017 private ConfigInfo getAppConfigInfo() { 1018 final Application application = (Application) getApplicationContext(); 1019 return new ConfigInfo(application, getDisplay()); 1020 } 1021 } 1022 1023 public enum ActivityCallback implements Parcelable { 1024 ON_CREATE, 1025 ON_START, 1026 ON_RESUME, 1027 ON_PAUSE, 1028 ON_STOP, 1029 ON_RESTART, 1030 ON_DESTROY, 1031 ON_ACTIVITY_RESULT, 1032 ON_USER_LEAVE_HINT, 1033 ON_NEW_INTENT, 1034 ON_CONFIGURATION_CHANGED, 1035 ON_MULTI_WINDOW_MODE_CHANGED, 1036 ON_PICTURE_IN_PICTURE_MODE_CHANGED, 1037 ON_MOVED_TO_DISPLAY, 1038 ON_PICTURE_IN_PICTURE_REQUESTED; 1039 1040 private static final ActivityCallback[] sValues = ActivityCallback.values(); 1041 public static final int SIZE = sValues.length; 1042 1043 @Override 1044 public int describeContents() { 1045 return 0; 1046 } 1047 1048 @Override 1049 public void writeToParcel(final Parcel dest, final int flags) { 1050 dest.writeInt(ordinal()); 1051 } 1052 1053 public static final Creator<ActivityCallback> CREATOR = new Creator<ActivityCallback>() { 1054 @Override 1055 public ActivityCallback createFromParcel(final Parcel source) { 1056 return sValues[source.readInt()]; 1057 } 1058 1059 @Override 1060 public ActivityCallback[] newArray(final int size) { 1061 return new ActivityCallback[size]; 1062 } 1063 }; 1064 } 1065 1066 public static class ConfigInfo implements Parcelable { 1067 public int displayId = Display.INVALID_DISPLAY; 1068 public int rotation; 1069 public SizeInfo sizeInfo; 1070 1071 ConfigInfo() { 1072 } 1073 1074 public ConfigInfo(Context context, Display display) { 1075 final Resources res = context.getResources(); 1076 final DisplayMetrics metrics = res.getDisplayMetrics(); 1077 final Configuration config = res.getConfiguration(); 1078 1079 if (display != null) { 1080 displayId = display.getDisplayId(); 1081 rotation = display.getRotation(); 1082 } 1083 sizeInfo = new SizeInfo(display, metrics, config); 1084 } 1085 1086 public ConfigInfo(Resources res) { 1087 final DisplayMetrics metrics = res.getDisplayMetrics(); 1088 final Configuration config = res.getConfiguration(); 1089 sizeInfo = new SizeInfo(null /* display */, metrics, config); 1090 } 1091 1092 @Override 1093 public String toString() { 1094 return "ConfigInfo: {displayId=" + displayId + " rotation=" + rotation 1095 + " " + sizeInfo + "}"; 1096 } 1097 1098 @Override 1099 public int describeContents() { 1100 return 0; 1101 } 1102 1103 @Override 1104 public void writeToParcel(Parcel dest, int flags) { 1105 dest.writeInt(displayId); 1106 dest.writeInt(rotation); 1107 dest.writeParcelable(sizeInfo, 0 /* parcelableFlags */); 1108 } 1109 1110 public void readFromParcel(Parcel in) { 1111 displayId = in.readInt(); 1112 rotation = in.readInt(); 1113 sizeInfo = in.readParcelable(SizeInfo.class.getClassLoader()); 1114 } 1115 1116 public static final Creator<ConfigInfo> CREATOR = new Creator<ConfigInfo>() { 1117 @Override 1118 public ConfigInfo createFromParcel(Parcel source) { 1119 final ConfigInfo sizeInfo = new ConfigInfo(); 1120 sizeInfo.readFromParcel(source); 1121 return sizeInfo; 1122 } 1123 1124 @Override 1125 public ConfigInfo[] newArray(int size) { 1126 return new ConfigInfo[size]; 1127 } 1128 }; 1129 } 1130 1131 public static class SizeInfo implements Parcelable { 1132 public int widthDp; 1133 public int heightDp; 1134 public int displayWidth; 1135 public int displayHeight; 1136 public int metricsWidth; 1137 public int metricsHeight; 1138 public int smallestWidthDp; 1139 public int densityDpi; 1140 public int orientation; 1141 public int windowWidth; 1142 public int windowHeight; 1143 public int windowAppWidth; 1144 public int windowAppHeight; 1145 1146 SizeInfo() { 1147 } 1148 1149 public SizeInfo(Display display, DisplayMetrics metrics, Configuration config) { 1150 if (display != null) { 1151 final Point displaySize = new Point(); 1152 display.getSize(displaySize); 1153 displayWidth = displaySize.x; 1154 displayHeight = displaySize.y; 1155 } 1156 1157 widthDp = config.screenWidthDp; 1158 heightDp = config.screenHeightDp; 1159 metricsWidth = metrics.widthPixels; 1160 metricsHeight = metrics.heightPixels; 1161 smallestWidthDp = config.smallestScreenWidthDp; 1162 densityDpi = config.densityDpi; 1163 orientation = config.orientation; 1164 windowWidth = config.windowConfiguration.getBounds().width(); 1165 windowHeight = config.windowConfiguration.getBounds().height(); 1166 windowAppWidth = config.windowConfiguration.getAppBounds().width(); 1167 windowAppHeight = config.windowConfiguration.getAppBounds().height(); 1168 } 1169 1170 @Override 1171 public String toString() { 1172 return "SizeInfo: {widthDp=" + widthDp + " heightDp=" + heightDp 1173 + " displayWidth=" + displayWidth + " displayHeight=" + displayHeight 1174 + " metricsWidth=" + metricsWidth + " metricsHeight=" + metricsHeight 1175 + " smallestWidthDp=" + smallestWidthDp + " densityDpi=" + densityDpi 1176 + " windowWidth=" + windowWidth + " windowHeight=" + windowHeight 1177 + " windowAppWidth=" + windowAppWidth + " windowAppHeight=" + windowAppHeight 1178 + " orientation=" + orientation + "}"; 1179 } 1180 1181 @Override 1182 public boolean equals(Object obj) { 1183 if (obj == this) { 1184 return true; 1185 } 1186 if (!(obj instanceof SizeInfo)) { 1187 return false; 1188 } 1189 final SizeInfo that = (SizeInfo) obj; 1190 return widthDp == that.widthDp 1191 && heightDp == that.heightDp 1192 && displayWidth == that.displayWidth 1193 && displayHeight == that.displayHeight 1194 && metricsWidth == that.metricsWidth 1195 && metricsHeight == that.metricsHeight 1196 && smallestWidthDp == that.smallestWidthDp 1197 && densityDpi == that.densityDpi 1198 && orientation == that.orientation 1199 && windowWidth == that.windowWidth 1200 && windowHeight == that.windowHeight 1201 && windowAppWidth == that.windowAppWidth 1202 && windowAppHeight == that.windowAppHeight; 1203 } 1204 1205 @Override 1206 public int hashCode() { 1207 int result = 0; 1208 result = 31 * result + widthDp; 1209 result = 31 * result + heightDp; 1210 result = 31 * result + displayWidth; 1211 result = 31 * result + displayHeight; 1212 result = 31 * result + metricsWidth; 1213 result = 31 * result + metricsHeight; 1214 result = 31 * result + smallestWidthDp; 1215 result = 31 * result + densityDpi; 1216 result = 31 * result + orientation; 1217 result = 31 * result + windowWidth; 1218 result = 31 * result + windowHeight; 1219 result = 31 * result + windowAppWidth; 1220 result = 31 * result + windowAppHeight; 1221 return result; 1222 } 1223 1224 @Override 1225 public int describeContents() { 1226 return 0; 1227 } 1228 1229 @Override 1230 public void writeToParcel(Parcel dest, int flags) { 1231 dest.writeInt(widthDp); 1232 dest.writeInt(heightDp); 1233 dest.writeInt(displayWidth); 1234 dest.writeInt(displayHeight); 1235 dest.writeInt(metricsWidth); 1236 dest.writeInt(metricsHeight); 1237 dest.writeInt(smallestWidthDp); 1238 dest.writeInt(densityDpi); 1239 dest.writeInt(orientation); 1240 dest.writeInt(windowWidth); 1241 dest.writeInt(windowHeight); 1242 dest.writeInt(windowAppWidth); 1243 dest.writeInt(windowAppHeight); 1244 } 1245 1246 public void readFromParcel(Parcel in) { 1247 widthDp = in.readInt(); 1248 heightDp = in.readInt(); 1249 displayWidth = in.readInt(); 1250 displayHeight = in.readInt(); 1251 metricsWidth = in.readInt(); 1252 metricsHeight = in.readInt(); 1253 smallestWidthDp = in.readInt(); 1254 densityDpi = in.readInt(); 1255 orientation = in.readInt(); 1256 windowWidth = in.readInt(); 1257 windowHeight = in.readInt(); 1258 windowAppWidth = in.readInt(); 1259 windowAppHeight = in.readInt(); 1260 } 1261 1262 public static final Creator<SizeInfo> CREATOR = new Creator<SizeInfo>() { 1263 @Override 1264 public SizeInfo createFromParcel(Parcel source) { 1265 final SizeInfo sizeInfo = new SizeInfo(); 1266 sizeInfo.readFromParcel(source); 1267 return sizeInfo; 1268 } 1269 1270 @Override 1271 public SizeInfo[] newArray(int size) { 1272 return new SizeInfo[size]; 1273 } 1274 }; 1275 } 1276 } 1277