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