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 } 408 409 /** Start the activity by the given intent and wait it becomes idle. */ 410 public ActivitySession startActivity(Intent intent) { 411 return startActivity(intent, null /* options */, true /* waitIdle */); 412 } 413 414 /** 415 * Launch the activity and establish a new session. 416 * 417 * @param intent The description of the activity to start. 418 * @param options Additional options for how the Activity should be started. 419 * @param waitIdle Block in this method until the target activity is idle. 420 * @return The session to communicate with the started activity. 421 */ 422 public ActivitySession startActivity(Intent intent, Bundle options, boolean waitIdle) { 423 ensureNotClosed(); 424 final ActivitySession session = new ActivitySession(this, waitIdle); 425 mSessions.put(session.mHostId, session); 426 setupLaunchIntent(intent, waitIdle, session); 427 428 if (!(mContext instanceof Activity)) { 429 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 430 } 431 mContext.startActivity(intent, options); 432 if (waitIdle) { 433 session.waitReply(); 434 } 435 return session; 436 } 437 438 /** Launch activity via proxy that allows to inject session parameters. */ 439 public ActivitySession startActivity(LaunchProxy proxy) { 440 ensureNotClosed(); 441 final boolean waitIdle = proxy.shouldWaitForLaunched(); 442 final ActivitySession session = new ActivitySession(this, waitIdle); 443 mSessions.put(session.mHostId, session); 444 445 proxy.setLaunchInjector(new LaunchInjector() { 446 @Override 447 public void setupIntent(Intent intent) { 448 final Bundle bundle = proxy.getExtras(); 449 if (bundle != null) { 450 intent.putExtras(bundle); 451 } 452 setupLaunchIntent(intent, waitIdle, session); 453 } 454 455 @Override 456 public void setupShellCommand(StringBuilder commandBuilder) { 457 commandBuilder.append(" --es " + KEY_HOST_ID + " " + session.mHostId); 458 commandBuilder.append(" --es " + KEY_CLIENT_ID + " " + mClientId); 459 if (waitIdle) { 460 commandBuilder.append( 461 " --el " + KEY_REQUEST_TOKEN + " " + session.mPendingRequestToken); 462 commandBuilder.append(" --es " + KEY_COMMAND + " " + COMMAND_WAIT_IDLE); 463 } 464 } 465 }); 466 467 proxy.execute(); 468 if (waitIdle) { 469 session.waitReply(); 470 } 471 return session; 472 } 473 474 private void setupLaunchIntent(Intent intent, boolean waitIdle, ActivitySession session) { 475 intent.putExtra(KEY_HOST_ID, session.mHostId); 476 intent.putExtra(KEY_CLIENT_ID, mClientId); 477 if (waitIdle) { 478 intent.putExtra(KEY_REQUEST_TOKEN, session.mPendingRequestToken); 479 intent.putExtra(KEY_COMMAND, COMMAND_WAIT_IDLE); 480 } 481 session.mOriginalLaunchIntent = intent; 482 } 483 484 public ActivitySession getLastStartedSession() { 485 if (mSessions.isEmpty()) { 486 throw new IllegalStateException("No started sessions"); 487 } 488 return mSessions.valueAt(mSessions.size() - 1); 489 } 490 491 private void ensureNotClosed() { 492 if (mClosed) { 493 throw new IllegalStateException("This session client is closed."); 494 } 495 } 496 497 @Override 498 public void onReceive(Context context, Intent intent) { 499 final ActivitySession session = mSessions.get(intent.getStringExtra(KEY_HOST_ID)); 500 if (DEBUG) Log.i(TAG, mClientId + " receives " + commandIntentToString(intent)); 501 if (session != null) { 502 session.receiveReply(intent.getExtras()); 503 } else { 504 Log.w(TAG, "No available session for " + commandIntentToString(intent)); 505 } 506 } 507 508 /** Complete cleanup with finishing all associated activities. */ 509 @Override 510 public void close() { 511 close(true /* finishSession */); 512 } 513 514 /** Cleanup except finish associated activities. */ 515 public void closeAndKeepSession() { 516 close(false /* finishSession */); 517 } 518 519 /** 520 * Closes this client. Once a client is closed, all methods on it will throw an 521 * IllegalStateException and all responses from host are ignored. 522 * 523 * @param finishSession Whether to finish activities launched from this client. 524 */ 525 public void close(boolean finishSession) { 526 ensureNotClosed(); 527 mClosed = true; 528 if (finishSession) { 529 for (int i = mSessions.size() - 1; i >= 0; i--) { 530 mSessions.valueAt(i).finish(); 531 } 532 } 533 mContext.unregisterReceiver(this); 534 mThread.quit(); 535 } 536 } 537 538 /** 539 * Interface definition for session host to process command from {@link ActivitySessionClient}. 540 */ 541 interface CommandReceiver { 542 /** Called when the session host is receiving command. */ 543 void receiveCommand(String command, Bundle data); 544 } 545 546 /** The host receives command from the test client. */ 547 public static class ActivitySessionHost extends BroadcastReceiver { 548 private final Context mContext; 549 private final String mClientId; 550 private final String mHostId; 551 private CommandReceiver mCallback; 552 /** The intents received when the host activity is relaunching. */ 553 private ArrayList<Intent> mPendingIntents; 554 555 ActivitySessionHost(Context context, String hostId, String clientId, 556 CommandReceiver callback) { 557 mContext = context; 558 mHostId = hostId; 559 mClientId = clientId; 560 mCallback = callback; 561 context.registerReceiver(this, new IntentFilter(hostId)); 562 } 563 564 @Override 565 public void onReceive(Context context, Intent intent) { 566 if (DEBUG) { 567 Log.i(TAG, mHostId + "(" 568 + (mCallback != null 569 ? mCallback.getClass().getName() 570 : mContext.getClass().getName()) 571 + ") receives " + commandIntentToString(intent)); 572 } 573 if (mCallback == null) { 574 if (mPendingIntents == null) { 575 mPendingIntents = new ArrayList<>(); 576 } 577 mPendingIntents.add(intent); 578 return; 579 } 580 dispatchCommand(mCallback, intent); 581 } 582 583 private static void dispatchCommand(CommandReceiver callback, Intent intent) { 584 callback.receiveCommand(intent.getStringExtra(KEY_COMMAND), intent.getExtras()); 585 } 586 587 void reply(String command, Bundle data) { 588 final Intent intent = new Intent(mClientId); 589 intent.putExtras(data); 590 intent.putExtra(KEY_COMMAND, command); 591 intent.putExtra(KEY_HOST_ID, mHostId); 592 mContext.sendBroadcast(intent); 593 if (DEBUG) { 594 Log.i(TAG, mHostId + "(" + mContext.getClass().getSimpleName() 595 + ") replies " + commandIntentToString(intent) + " to " + mClientId); 596 } 597 } 598 599 void setCallback(CommandReceiver callback) { 600 if (mPendingIntents != null && mCallback == null && callback != null) { 601 for (Intent intent : mPendingIntents) { 602 dispatchCommand(callback, intent); 603 } 604 mPendingIntents = null; 605 } 606 mCallback = callback; 607 } 608 609 void destroy() { 610 mContext.unregisterReceiver(this); 611 } 612 } 613 614 /** 615 * A map to store data by host id. The usage should be declared as static that is able to keep 616 * data after activity is relaunched. 617 */ 618 private static class StaticHostStorage<T> { 619 final ArrayMap<String, ArrayList<T>> mStorage = new ArrayMap<>(); 620 621 void add(String hostId, T data) { 622 ArrayList<T> commands = mStorage.get(hostId); 623 if (commands == null) { 624 commands = new ArrayList<>(); 625 mStorage.put(hostId, commands); 626 } 627 commands.add(data); 628 } 629 630 ArrayList<T> get(String hostId) { 631 return mStorage.get(hostId); 632 } 633 634 void clear(String hostId) { 635 mStorage.remove(hostId); 636 } 637 } 638 639 /** Store the commands which have not been handled. */ 640 private static class CommandStorage extends StaticHostStorage<Bundle> { 641 642 /** Remove the oldest matched command and return its request token. */ 643 long consume(String hostId, String command) { 644 final ArrayList<Bundle> commands = mStorage.get(hostId); 645 if (commands != null) { 646 final Iterator<Bundle> iterator = commands.iterator(); 647 while (iterator.hasNext()) { 648 final Bundle data = iterator.next(); 649 if (command.equals(data.getString(KEY_COMMAND))) { 650 iterator.remove(); 651 return data.getLong(KEY_REQUEST_TOKEN); 652 } 653 } 654 if (commands.isEmpty()) { 655 clear(hostId); 656 } 657 } 658 return INVALID_REQUEST_TOKEN; 659 } 660 661 boolean containsCommand(String receiverId, String command) { 662 final ArrayList<Bundle> dataList = mStorage.get(receiverId); 663 if (dataList != null) { 664 for (Bundle data : dataList) { 665 if (command.equals(data.getString(KEY_COMMAND))) { 666 return true; 667 } 668 } 669 } 670 return false; 671 } 672 } 673 674 /** 675 * The base activity which supports the session protocol. If the caller does not use 676 * {@link ActivitySessionClient}, it behaves as a normal activity. 677 */ 678 public static class CommandSessionActivity extends Activity implements CommandReceiver { 679 /** Static command storage for across relaunch. */ 680 private static CommandStorage sCommandStorage; 681 private ActivitySessionHost mReceiver; 682 683 /** The subclasses can disable the test journal client if its information is not used. */ 684 protected boolean mUseTestJournal = true; 685 protected TestJournalClient mTestJournalClient; 686 687 @Override 688 protected void onCreate(Bundle savedInstanceState) { 689 super.onCreate(savedInstanceState); 690 if (mUseTestJournal) { 691 mTestJournalClient = TestJournalClient.create(this /* context */, 692 getComponentName()); 693 } 694 695 final String hostId = getIntent().getStringExtra(KEY_HOST_ID); 696 final String clientId = getIntent().getStringExtra(KEY_CLIENT_ID); 697 if (hostId != null && clientId != null) { 698 if (sCommandStorage == null) { 699 sCommandStorage = new CommandStorage(); 700 } 701 final Object receiver = getLastNonConfigurationInstance(); 702 if (receiver instanceof ActivitySessionHost) { 703 mReceiver = (ActivitySessionHost) receiver; 704 mReceiver.setCallback(this); 705 } else { 706 mReceiver = new ActivitySessionHost(getApplicationContext(), hostId, clientId, 707 this /* callback */); 708 } 709 } 710 } 711 712 @Override 713 protected void onDestroy() { 714 super.onDestroy(); 715 if (isChangingConfigurations()) { 716 // Detach the callback if the activity is relaunching. The callback will be 717 // associated again in onCreate. 718 if (mReceiver != null) { 719 mReceiver.setCallback(null); 720 } 721 } else if (mReceiver != null) { 722 // Clean up for real removal. 723 sCommandStorage.clear(getHostId()); 724 mReceiver.destroy(); 725 mReceiver = null; 726 } 727 if (mTestJournalClient != null) { 728 mTestJournalClient.close(); 729 } 730 } 731 732 @Override 733 public Object onRetainNonConfigurationInstance() { 734 return mReceiver; 735 } 736 737 @Override 738 public final void receiveCommand(String command, Bundle data) { 739 if (mReceiver == null) { 740 throw new IllegalStateException("The receiver is not created"); 741 } 742 sCommandStorage.add(getHostId(), data); 743 handleCommand(command, data); 744 } 745 746 /** Handle the incoming command from client. */ 747 protected void handleCommand(String command, Bundle data) { 748 } 749 750 protected final void reply(String command) { 751 reply(command, null /* data */); 752 } 753 754 /** Reply data to client for the command. */ 755 protected final void reply(String command, Bundle data) { 756 if (mReceiver == null) { 757 throw new IllegalStateException("The receiver is not created"); 758 } 759 final long requestToke = sCommandStorage.consume(getHostId(), command); 760 if (requestToke == INVALID_REQUEST_TOKEN) { 761 throw new IllegalStateException("There is no pending command " + command); 762 } 763 if (data == null) { 764 data = new Bundle(); 765 } 766 data.putLong(KEY_REQUEST_TOKEN, requestToke); 767 mReceiver.reply(command, data); 768 } 769 770 protected boolean hasPendingCommand(String command) { 771 return mReceiver != null && sCommandStorage.containsCommand(getHostId(), command); 772 } 773 774 /** Returns null means this activity does support the session protocol. */ 775 final String getHostId() { 776 return mReceiver != null ? mReceiver.mHostId : null; 777 } 778 } 779 780 /** The default implementation that supports basic commands to interact with activity. */ 781 public static class BasicTestActivity extends CommandSessionActivity { 782 /** Static callback history for across relaunch. */ 783 private static final StaticHostStorage<ActivityCallback> sCallbackStorage = 784 new StaticHostStorage<>(); 785 786 private final String mTag = getClass().getSimpleName(); 787 protected boolean mPrintCallbackLog; 788 789 @Override 790 protected void onCreate(Bundle savedInstanceState) { 791 super.onCreate(savedInstanceState); 792 onCallback(ActivityCallback.ON_CREATE); 793 794 if (getHostId() != null) { 795 final int orientation = getIntent().getIntExtra(KEY_ORIENTATION, Integer.MIN_VALUE); 796 if (orientation != Integer.MIN_VALUE) { 797 setRequestedOrientation(orientation); 798 } 799 if (COMMAND_WAIT_IDLE.equals(getIntent().getStringExtra(KEY_COMMAND))) { 800 receiveCommand(COMMAND_WAIT_IDLE, getIntent().getExtras()); 801 // No need to execute again if the activity is relaunched. 802 getIntent().removeExtra(KEY_COMMAND); 803 } 804 } 805 } 806 807 @Override 808 public void handleCommand(String command, Bundle data) { 809 switch (command) { 810 case COMMAND_ORIENTATION: 811 clearCallbackHistory(); 812 setRequestedOrientation(data.getInt(KEY_ORIENTATION)); 813 getWindow().getDecorView().postDelayed(() -> { 814 if (reportConfigIfNeeded()) { 815 Log.w(getTag(), "Fallback report. The orientation may not change."); 816 } 817 }, ActivitySession.Response.TIMEOUT_MILLIS / 2); 818 break; 819 820 case COMMAND_GET_CONFIG: 821 runWhenIdle(() -> { 822 final Bundle replyData = new Bundle(); 823 replyData.putParcelable(KEY_CONFIG_INFO, getConfigInfo()); 824 reply(COMMAND_GET_CONFIG, replyData); 825 }); 826 break; 827 828 case COMMAND_GET_APP_CONFIG: 829 runWhenIdle(() -> { 830 final Bundle replyData = new Bundle(); 831 replyData.putParcelable(KEY_APP_CONFIG_INFO, getAppConfigInfo()); 832 reply(COMMAND_GET_APP_CONFIG, replyData); 833 }); 834 break; 835 836 case COMMAND_FINISH: 837 if (!isFinishing()) { 838 finish(); 839 } 840 break; 841 842 case COMMAND_TAKE_CALLBACK_HISTORY: 843 final Bundle replyData = new Bundle(); 844 replyData.putParcelableArrayList(KEY_CALLBACK_HISTORY, getCallbackHistory()); 845 reply(command, replyData); 846 clearCallbackHistory(); 847 break; 848 849 case COMMAND_WAIT_IDLE: 850 runWhenIdle(() -> reply(command)); 851 break; 852 853 case COMMAND_GET_NAME: { 854 final Bundle result = new Bundle(); 855 result.putParcelable(COMMAND_GET_NAME, getComponentName()); 856 reply(COMMAND_GET_NAME, result); 857 break; 858 } 859 860 case COMMAND_DISPLAY_ACCESS_CHECK: 861 final Bundle result = new Bundle(); 862 final boolean displayHasAccess = getDisplay().hasAccess(Process.myUid()); 863 result.putBoolean(KEY_UID_HAS_ACCESS_ON_DISPLAY, displayHasAccess); 864 reply(command, result); 865 break; 866 867 default: 868 break; 869 } 870 } 871 872 protected final void clearCallbackHistory() { 873 sCallbackStorage.clear(getHostId()); 874 } 875 876 protected final ArrayList<ActivityCallback> getCallbackHistory() { 877 return sCallbackStorage.get(getHostId()); 878 } 879 880 protected void runWhenIdle(Runnable r) { 881 Looper.getMainLooper().getQueue().addIdleHandler(() -> { 882 r.run(); 883 return false; 884 }); 885 } 886 887 protected boolean reportConfigIfNeeded() { 888 if (!hasPendingCommand(COMMAND_ORIENTATION)) { 889 return false; 890 } 891 runWhenIdle(() -> { 892 final Bundle replyData = new Bundle(); 893 replyData.putParcelable(KEY_CONFIG_INFO, getConfigInfo()); 894 replyData.putParcelableArrayList(KEY_CALLBACK_HISTORY, getCallbackHistory()); 895 reply(COMMAND_ORIENTATION, replyData); 896 clearCallbackHistory(); 897 }); 898 return true; 899 } 900 901 @Override 902 protected void onStart() { 903 super.onStart(); 904 onCallback(ActivityCallback.ON_START); 905 } 906 907 @Override 908 protected void onRestart() { 909 super.onRestart(); 910 onCallback(ActivityCallback.ON_RESTART); 911 } 912 913 @Override 914 protected void onResume() { 915 super.onResume(); 916 onCallback(ActivityCallback.ON_RESUME); 917 reportConfigIfNeeded(); 918 } 919 920 @Override 921 protected void onPause() { 922 super.onPause(); 923 onCallback(ActivityCallback.ON_PAUSE); 924 } 925 926 @Override 927 protected void onStop() { 928 super.onStop(); 929 onCallback(ActivityCallback.ON_STOP); 930 } 931 932 @Override 933 protected void onDestroy() { 934 super.onDestroy(); 935 onCallback(ActivityCallback.ON_DESTROY); 936 } 937 938 @Override 939 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 940 super.onActivityResult(requestCode, resultCode, data); 941 onCallback(ActivityCallback.ON_ACTIVITY_RESULT); 942 } 943 944 @Override 945 protected void onUserLeaveHint() { 946 super.onUserLeaveHint(); 947 onCallback(ActivityCallback.ON_USER_LEAVE_HINT); 948 } 949 950 @Override 951 protected void onNewIntent(Intent intent) { 952 super.onNewIntent(intent); 953 onCallback(ActivityCallback.ON_NEW_INTENT); 954 } 955 956 @Override 957 public void onConfigurationChanged(Configuration newConfig) { 958 super.onConfigurationChanged(newConfig); 959 onCallback(ActivityCallback.ON_CONFIGURATION_CHANGED); 960 reportConfigIfNeeded(); 961 } 962 963 @Override 964 public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) { 965 super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig); 966 onCallback(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED); 967 } 968 969 @Override 970 public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, 971 Configuration newConfig) { 972 super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig); 973 onCallback(ActivityCallback.ON_PICTURE_IN_PICTURE_MODE_CHANGED); 974 } 975 976 @Override 977 public void onMovedToDisplay(int displayId, Configuration config) { 978 super.onMovedToDisplay(displayId, config); 979 onCallback(ActivityCallback.ON_MOVED_TO_DISPLAY); 980 } 981 982 public void onCallback(ActivityCallback callback) { 983 if (mPrintCallbackLog) { 984 Log.i(getTag(), callback + " @ " 985 + Integer.toHexString(System.identityHashCode(this))); 986 } 987 final String hostId = getHostId(); 988 if (hostId != null) { 989 sCallbackStorage.add(hostId, callback); 990 } 991 if (mTestJournalClient != null) { 992 mTestJournalClient.addCallback(callback); 993 } 994 } 995 996 protected void withTestJournalClient(Consumer<TestJournalClient> client) { 997 if (mTestJournalClient != null) { 998 client.accept(mTestJournalClient); 999 } 1000 } 1001 1002 protected String getTag() { 1003 return mTag; 1004 } 1005 1006 /** Get configuration and display info. It should be called only after resumed. */ 1007 protected ConfigInfo getConfigInfo() { 1008 final View view = getWindow().getDecorView(); 1009 if (!view.isAttachedToWindow()) { 1010 Log.w(getTag(), "Decor view has not attached"); 1011 } 1012 return new ConfigInfo(view.getContext(), view.getDisplay()); 1013 } 1014 1015 /** Same as {@link #getConfigInfo()}, but for Application. */ 1016 private ConfigInfo getAppConfigInfo() { 1017 final Application application = (Application) getApplicationContext(); 1018 return new ConfigInfo(application, getDisplay()); 1019 } 1020 } 1021 1022 public enum ActivityCallback implements Parcelable { 1023 ON_CREATE, 1024 ON_START, 1025 ON_RESUME, 1026 ON_PAUSE, 1027 ON_STOP, 1028 ON_RESTART, 1029 ON_DESTROY, 1030 ON_ACTIVITY_RESULT, 1031 ON_USER_LEAVE_HINT, 1032 ON_NEW_INTENT, 1033 ON_CONFIGURATION_CHANGED, 1034 ON_MULTI_WINDOW_MODE_CHANGED, 1035 ON_PICTURE_IN_PICTURE_MODE_CHANGED, 1036 ON_MOVED_TO_DISPLAY, 1037 ON_PICTURE_IN_PICTURE_REQUESTED; 1038 1039 private static final ActivityCallback[] sValues = ActivityCallback.values(); 1040 public static final int SIZE = sValues.length; 1041 1042 @Override 1043 public int describeContents() { 1044 return 0; 1045 } 1046 1047 @Override 1048 public void writeToParcel(final Parcel dest, final int flags) { 1049 dest.writeInt(ordinal()); 1050 } 1051 1052 public static final Creator<ActivityCallback> CREATOR = new Creator<ActivityCallback>() { 1053 @Override 1054 public ActivityCallback createFromParcel(final Parcel source) { 1055 return sValues[source.readInt()]; 1056 } 1057 1058 @Override 1059 public ActivityCallback[] newArray(final int size) { 1060 return new ActivityCallback[size]; 1061 } 1062 }; 1063 } 1064 1065 public static class ConfigInfo implements Parcelable { 1066 public int displayId = Display.INVALID_DISPLAY; 1067 public int rotation; 1068 public SizeInfo sizeInfo; 1069 1070 ConfigInfo() { 1071 } 1072 1073 public ConfigInfo(Context context, Display display) { 1074 final Resources res = context.getResources(); 1075 final DisplayMetrics metrics = res.getDisplayMetrics(); 1076 final Configuration config = res.getConfiguration(); 1077 1078 if (display != null) { 1079 displayId = display.getDisplayId(); 1080 rotation = display.getRotation(); 1081 } 1082 sizeInfo = new SizeInfo(display, metrics, config); 1083 } 1084 1085 public ConfigInfo(Resources res) { 1086 final DisplayMetrics metrics = res.getDisplayMetrics(); 1087 final Configuration config = res.getConfiguration(); 1088 sizeInfo = new SizeInfo(null /* display */, metrics, config); 1089 } 1090 1091 @Override 1092 public String toString() { 1093 return "ConfigInfo: {displayId=" + displayId + " rotation=" + rotation 1094 + " " + sizeInfo + "}"; 1095 } 1096 1097 @Override 1098 public int describeContents() { 1099 return 0; 1100 } 1101 1102 @Override 1103 public void writeToParcel(Parcel dest, int flags) { 1104 dest.writeInt(displayId); 1105 dest.writeInt(rotation); 1106 dest.writeParcelable(sizeInfo, 0 /* parcelableFlags */); 1107 } 1108 1109 public void readFromParcel(Parcel in) { 1110 displayId = in.readInt(); 1111 rotation = in.readInt(); 1112 sizeInfo = in.readParcelable(SizeInfo.class.getClassLoader()); 1113 } 1114 1115 public static final Creator<ConfigInfo> CREATOR = new Creator<ConfigInfo>() { 1116 @Override 1117 public ConfigInfo createFromParcel(Parcel source) { 1118 final ConfigInfo sizeInfo = new ConfigInfo(); 1119 sizeInfo.readFromParcel(source); 1120 return sizeInfo; 1121 } 1122 1123 @Override 1124 public ConfigInfo[] newArray(int size) { 1125 return new ConfigInfo[size]; 1126 } 1127 }; 1128 } 1129 1130 public static class SizeInfo implements Parcelable { 1131 public int widthDp; 1132 public int heightDp; 1133 public int displayWidth; 1134 public int displayHeight; 1135 public int metricsWidth; 1136 public int metricsHeight; 1137 public int smallestWidthDp; 1138 public int densityDpi; 1139 public int orientation; 1140 public int windowWidth; 1141 public int windowHeight; 1142 public int windowAppWidth; 1143 public int windowAppHeight; 1144 1145 SizeInfo() { 1146 } 1147 1148 public SizeInfo(Display display, DisplayMetrics metrics, Configuration config) { 1149 if (display != null) { 1150 final Point displaySize = new Point(); 1151 display.getSize(displaySize); 1152 displayWidth = displaySize.x; 1153 displayHeight = displaySize.y; 1154 } 1155 1156 widthDp = config.screenWidthDp; 1157 heightDp = config.screenHeightDp; 1158 metricsWidth = metrics.widthPixels; 1159 metricsHeight = metrics.heightPixels; 1160 smallestWidthDp = config.smallestScreenWidthDp; 1161 densityDpi = config.densityDpi; 1162 orientation = config.orientation; 1163 windowWidth = config.windowConfiguration.getBounds().width(); 1164 windowHeight = config.windowConfiguration.getBounds().height(); 1165 windowAppWidth = config.windowConfiguration.getAppBounds().width(); 1166 windowAppHeight = config.windowConfiguration.getAppBounds().height(); 1167 } 1168 1169 @Override 1170 public String toString() { 1171 return "SizeInfo: {widthDp=" + widthDp + " heightDp=" + heightDp 1172 + " displayWidth=" + displayWidth + " displayHeight=" + displayHeight 1173 + " metricsWidth=" + metricsWidth + " metricsHeight=" + metricsHeight 1174 + " smallestWidthDp=" + smallestWidthDp + " densityDpi=" + densityDpi 1175 + " windowWidth=" + windowWidth + " windowHeight=" + windowHeight 1176 + " windowAppWidth=" + windowAppWidth + " windowAppHeight=" + windowAppHeight 1177 + " orientation=" + orientation + "}"; 1178 } 1179 1180 @Override 1181 public boolean equals(Object obj) { 1182 if (obj == this) { 1183 return true; 1184 } 1185 if (!(obj instanceof SizeInfo)) { 1186 return false; 1187 } 1188 final SizeInfo that = (SizeInfo) obj; 1189 return widthDp == that.widthDp 1190 && heightDp == that.heightDp 1191 && displayWidth == that.displayWidth 1192 && displayHeight == that.displayHeight 1193 && metricsWidth == that.metricsWidth 1194 && metricsHeight == that.metricsHeight 1195 && smallestWidthDp == that.smallestWidthDp 1196 && densityDpi == that.densityDpi 1197 && orientation == that.orientation 1198 && windowWidth == that.windowWidth 1199 && windowHeight == that.windowHeight 1200 && windowAppWidth == that.windowAppWidth 1201 && windowAppHeight == that.windowAppHeight; 1202 } 1203 1204 @Override 1205 public int hashCode() { 1206 int result = 0; 1207 result = 31 * result + widthDp; 1208 result = 31 * result + heightDp; 1209 result = 31 * result + displayWidth; 1210 result = 31 * result + displayHeight; 1211 result = 31 * result + metricsWidth; 1212 result = 31 * result + metricsHeight; 1213 result = 31 * result + smallestWidthDp; 1214 result = 31 * result + densityDpi; 1215 result = 31 * result + orientation; 1216 result = 31 * result + windowWidth; 1217 result = 31 * result + windowHeight; 1218 result = 31 * result + windowAppWidth; 1219 result = 31 * result + windowAppHeight; 1220 return result; 1221 } 1222 1223 @Override 1224 public int describeContents() { 1225 return 0; 1226 } 1227 1228 @Override 1229 public void writeToParcel(Parcel dest, int flags) { 1230 dest.writeInt(widthDp); 1231 dest.writeInt(heightDp); 1232 dest.writeInt(displayWidth); 1233 dest.writeInt(displayHeight); 1234 dest.writeInt(metricsWidth); 1235 dest.writeInt(metricsHeight); 1236 dest.writeInt(smallestWidthDp); 1237 dest.writeInt(densityDpi); 1238 dest.writeInt(orientation); 1239 dest.writeInt(windowWidth); 1240 dest.writeInt(windowHeight); 1241 dest.writeInt(windowAppWidth); 1242 dest.writeInt(windowAppHeight); 1243 } 1244 1245 public void readFromParcel(Parcel in) { 1246 widthDp = in.readInt(); 1247 heightDp = in.readInt(); 1248 displayWidth = in.readInt(); 1249 displayHeight = in.readInt(); 1250 metricsWidth = in.readInt(); 1251 metricsHeight = in.readInt(); 1252 smallestWidthDp = in.readInt(); 1253 densityDpi = in.readInt(); 1254 orientation = in.readInt(); 1255 windowWidth = in.readInt(); 1256 windowHeight = in.readInt(); 1257 windowAppWidth = in.readInt(); 1258 windowAppHeight = in.readInt(); 1259 } 1260 1261 public static final Creator<SizeInfo> CREATOR = new Creator<SizeInfo>() { 1262 @Override 1263 public SizeInfo createFromParcel(Parcel source) { 1264 final SizeInfo sizeInfo = new SizeInfo(); 1265 sizeInfo.readFromParcel(source); 1266 return sizeInfo; 1267 } 1268 1269 @Override 1270 public SizeInfo[] newArray(int size) { 1271 return new SizeInfo[size]; 1272 } 1273 }; 1274 } 1275 } 1276