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