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