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