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