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