/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package androidx.test.internal.runner;
import static androidx.test.internal.util.Checks.checkNotNull;
import static androidx.test.internal.util.Checks.checkState;
import static androidx.test.internal.util.LogUtil.logDebugWithProcess;
import android.app.Instrumentation;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.Parcelable;
import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import androidx.test.annotation.Beta;
import androidx.test.internal.util.ParcelableIBinder;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.MonitoringInstrumentation;
import androidx.test.runner.MonitoringInstrumentation.ActivityFinisher;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
/**
* A singleton class that facilitates the communication between different instrumentation test
* runner instances running on different processes.
*
* <p>Upon creation, a broadcast with an Intent containing a {@link #BROADCAST_FILTER} action, will
* be sent out to notify other runners of this instance. During handshake, both instances will
* exchange their own {@link IBinder}'s for further IPC, along with the list of all known client's
* {@link Messenger}s. Where a client could be any other testing framework that is built on top of
* AndroidJUnitRunner (example: Espresso, UiAutomator, etc.)
*
* <p>To get the instance of this object {@link #getInstance()} should be called. The user of this
* class should then call {@link #init(Instrumentation, ActivityFinisher)} prior to attempting to
* use any functionality of this class. Call {@link #terminate()} after using
* InstrumentationConnection to release any resources. Failure to do so will lead to memory leaks
* and unexpected behavior.
*
* <p><b>This API is currently in beta.</b>
*/
@Beta
public class InstrumentationConnection {
private static final String TAG = "InstrConnection";
private static final InstrumentationConnection DEFAULT_INSTANCE =
new InstrumentationConnection(
InstrumentationRegistry.getInstrumentation().getTargetContext());
private static final String BUNDLE_KEY_CLIENTS = "instr_clients";
private static final String BUNDLE_KEY_CLIENT_TYPE = "instr_client_type";
private static final String BUNDLE_KEY_CLIENT_MESSENGER = "instr_client_msgr";
private static final String BUNDLE_KEY_UUID = "instr_uuid";
@VisibleForTesting static final String BUNDLE_BR_NEW_BINDER = "new_instrumentation_binder";
/** The intent action used to discover other instrumentation instances */
public static final String BROADCAST_FILTER =
"androidx.test.runner.InstrumentationConnection.event";
private static final int MSG_REMOTE_ADD_CLIENT = 0;
private static final int MSG_REMOTE_REMOVE_CLIENT = 1;
private static final int MSG_TERMINATE = 2;
private static final int MSG_HANDLE_INSTRUMENTATION_FROM_BROADCAST = 3;
@VisibleForTesting static final int MSG_ADD_INSTRUMENTATION = 4;
private static final int MSG_REMOVE_INSTRUMENTATION = 5;
@VisibleForTesting static final int MSG_ADD_CLIENTS_IN_BUNDLE = 6;
private static final int MSG_REMOVE_CLIENTS_IN_BUNDLE = 7;
private static final int MSG_REG_CLIENT = 8;
private static final int MSG_UN_REG_CLIENT = 9;
@VisibleForTesting static final int MSG_REMOTE_CLEANUP_REQUEST = 10;
private static final int MSG_PERFORM_CLEANUP = 11;
private static final int MSG_PERFORM_CLEANUP_FINISHED = 12;
private Context targetContext;
private static Instrumentation instrumentation;
private static MonitoringInstrumentation.ActivityFinisher activityFinisher;
/**
* The {@link IncomingHandler} that will handle all the incoming messages via {@link
* IncomingHandler#messengerHandler}
*/
IncomingHandler incomingHandler;
/** Receiver used to discover and establish communication with new instrumentation instances */
@VisibleForTesting final BroadcastReceiver messengerReceiver = new MessengerReceiver();
@VisibleForTesting
InstrumentationConnection(@NonNull Context context) {
targetContext = checkNotNull(context, "Context can't be null");
}
/**
* Returns a {@link InstrumentationConnection} object
*
* @return an instance of {@link InstrumentationConnection} object.
*/
public static InstrumentationConnection getInstance() {
return DEFAULT_INSTANCE;
}
/**
* The initialization consists of:
*
* <ol>
* <li>Sending a broadcast via a well known {@link Intent} that contains a {@link
* IncomingHandler} which others can use the send messages.
* <li>Registering this instance for the same intent broadcasts to enable future discovery of
* newly started instrumentation runners on other processes.
* </ol>
*
* The caller of this method must call {@link #terminate()} to preform the proper clean up which
* includes unregister from the above defined broadcast.
*
* @param instrumentation the {@link Instrumentation} instance this running in
* @param finisher an activity finisher to use when performing cleanup
*/
public synchronized void init(
Instrumentation instrumentation, MonitoringInstrumentation.ActivityFinisher finisher) {
logDebugWithProcess(TAG, "init");
if (null == incomingHandler) {
InstrumentationConnection.instrumentation = instrumentation;
activityFinisher = finisher;
HandlerThread ht = new HandlerThread("InstrumentationConnectionThread");
ht.start();
incomingHandler = new IncomingHandler(ht.getLooper());
// Inform other instances of yourself
Intent intent = new Intent(BROADCAST_FILTER);
Bundle bundle = new Bundle();
bundle.putParcelable(
BUNDLE_BR_NEW_BINDER,
new ParcelableIBinder(incomingHandler.messengerHandler.getBinder()));
intent.putExtra(BUNDLE_BR_NEW_BINDER, bundle);
try {
targetContext.sendBroadcast(intent);
// TODO: Consider enforcing permissions when registering for a receiver
targetContext.registerReceiver(messengerReceiver, new IntentFilter(BROADCAST_FILTER));
} catch (SecurityException isolatedProcess) {
Log.i(TAG, "Could not send broadcast or register receiver (isolatedProcess?)");
}
}
}
/**
* This methods should be called after {@link #init(Instrumentation, ActivityFinisher)} was
* called. The purpose of this method is to preform any required clean up along with unregistering
* from any broadcast receivers.
*/
public synchronized void terminate() {
logDebugWithProcess(TAG, "Terminate is called");
if (incomingHandler != null) {
// post termination message to the handler in case there messages in flight
incomingHandler.runSyncTask(
new Callable<Void>() {
@Override
public Void call() {
incomingHandler.doDie();
return null;
}
});
targetContext.unregisterReceiver(messengerReceiver);
incomingHandler = null;
}
}
/**
* Request all remote instrumentation instances to finish all activities in order to insure a
* clean state before/after each test.
*/
public synchronized void requestRemoteInstancesActivityCleanup() {
checkState(incomingHandler != null, "Instrumentation Connection in not yet initialized");
UUID uuid = UUID.randomUUID();
CountDownLatch latch = new CountDownLatch(1);
incomingHandler.associateLatch(uuid, latch);
Message msg = incomingHandler.obtainMessage(MSG_REMOTE_CLEANUP_REQUEST);
msg.replyTo = incomingHandler.messengerHandler;
Bundle bundle = msg.getData();
bundle.putSerializable(BUNDLE_KEY_UUID, uuid);
msg.setData(bundle);
incomingHandler.sendMessage(msg);
// block until remote clean up is complete, will timeout no reply received within 2 sec
try {
if (!latch.await(2, TimeUnit.SECONDS)) {
Log.w(TAG, "Timed out while attempting to perform activity clean up for " + uuid);
}
} catch (InterruptedException e) {
Log.e(TAG, "Interrupted while waiting for response from message with id: " + uuid, e);
} finally {
incomingHandler.disassociateLatch(uuid);
}
}
/**
* Register a client and notify all other clients of the same type if needed.
*
* @param type the type of the client
* @param messenger a {@link Messenger} to use for future communication with the client
*/
public synchronized void registerClient(String type, Messenger messenger) {
checkState(incomingHandler != null, "Instrumentation Connection in not yet initialized");
Log.i(TAG, "Register client of type: " + type);
Bundle bundle = new Bundle();
bundle.putString(BUNDLE_KEY_CLIENT_TYPE, type);
bundle.putParcelable(BUNDLE_KEY_CLIENT_MESSENGER, messenger);
Message msg = incomingHandler.obtainMessage(MSG_REG_CLIENT);
msg.setData(bundle);
incomingHandler.sendMessage(msg);
}
/**
* Helper method to obtain a set of clients of the same type.
*
* @param type the type of the client
* @return a Set of Messengers of the desired client type, {@code null} is returned if client type
* is unknown
*/
public synchronized Set<Messenger> getClientsForType(final String type) {
return incomingHandler.getClientsForType(type);
}
/**
* Un-register a client and notify all other clients of the same type if needed.
*
* @param type the type of the client
* @param messenger a {@link Messenger} to use for future communication with the client
*/
public synchronized void unregisterClient(String type, Messenger messenger) {
checkState(incomingHandler != null, "Instrumentation Connection in not yet initialized");
Log.i(TAG, "Unregister client of type: " + type);
Bundle bundle = new Bundle();
bundle.putString(BUNDLE_KEY_CLIENT_TYPE, type);
bundle.putParcelable(BUNDLE_KEY_CLIENT_MESSENGER, messenger);
Message msg = incomingHandler.obtainMessage(MSG_UN_REG_CLIENT);
msg.setData(bundle);
incomingHandler.sendMessage(msg);
}
/** Receiver to handle Intent broadcasts with {@link #BROADCAST_FILTER} event. */
@VisibleForTesting
class MessengerReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
logDebugWithProcess(TAG, "Broadcast received");
// Extract data included in the Intent
Bundle extras = intent.getBundleExtra(BUNDLE_BR_NEW_BINDER);
if (null == extras) {
Log.w(TAG, "Broadcast intent doesn't contain any extras, ignoring it..");
return;
}
ParcelableIBinder iBinder = extras.getParcelable(BUNDLE_BR_NEW_BINDER);
if (iBinder != null) {
Messenger msgr = new Messenger(iBinder.getIBinder());
Message msg = incomingHandler.obtainMessage(MSG_HANDLE_INSTRUMENTATION_FROM_BROADCAST);
msg.replyTo = msgr;
incomingHandler.sendMessage(msg);
}
}
}
/**
* Handler of incoming messages from other instrumentation runners.
*
* <p>Addition or removal of instrumentation or clients must always be done on the handler thread.
*/
@VisibleForTesting
static class IncomingHandler extends Handler {
/** Target we publish for clients to send messages to IncomingHandler. */
@VisibleForTesting Messenger messengerHandler = new Messenger(this);
/**
* Keeps track of all currently registered clients. Note: This Set should only be modified via
* the incomingHandler.
*/
@VisibleForTesting Set<Messenger> otherInstrumentations = new HashSet<>();
/**
* Keeps track of all Messengers for each unique client type. Note: This Map should only be
* modified via the incomingHandler.
*/
@VisibleForTesting Map<String, Set<Messenger>> typedClients = new HashMap<>();
/**
* Keeps track of {@link CountDownLatch}s mapped to {@link UUID}s to aid with synchronization.
*/
private final Map<UUID, CountDownLatch> latches = new HashMap<>();
public IncomingHandler(Looper looper) {
super(looper);
if (Looper.getMainLooper() == looper || Looper.myLooper() == looper) {
throw new IllegalStateException(
"This handler should not be using the main thread "
+ "looper nor the instrumentation thread looper.");
}
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_TERMINATE:
logDebugWithProcess(TAG, "handleMessage(MSG_TERMINATE)");
doDie();
break;
case MSG_HANDLE_INSTRUMENTATION_FROM_BROADCAST:
logDebugWithProcess(TAG, "handleMessage(MSG_HANDLE_INSTRUMENTATION_FROM_BROADCAST)");
// This message comes from the local MessengerReceiver#onReceive method to
// register the remote instrumentation instance came in the broadcast and
// reply back with local binder for future IPC.
if (otherInstrumentations.add(msg.replyTo)) {
sendMessageWithReply(msg.replyTo, MSG_ADD_INSTRUMENTATION, null);
} else {
Log.w(TAG, "Broadcast with existing binder was received, ignoring it..");
}
break;
case MSG_ADD_INSTRUMENTATION:
logDebugWithProcess(TAG, "handleMessage(MSG_ADD_INSTRUMENTATION)");
// This message comes from instrumentation instances running in a
// separate process who receive {@link BROADCAST_FILTER} broadcast. The message
// should include: the sender's Messenger in Message#replyTo and a bundle with
// a list of its potential clients.
if (otherInstrumentations.add(msg.replyTo)) {
// Reply back with local list of clients if exist
if (!typedClients.isEmpty()) {
sendMessageWithReply(msg.replyTo, MSG_ADD_CLIENTS_IN_BUNDLE, null);
}
// Save remote clients
clientsRegistrationFromBundle(msg.getData(), true);
} else {
Log.w(TAG, "Message with existing binder was received, ignoring it..");
}
break;
case MSG_REMOVE_INSTRUMENTATION:
logDebugWithProcess(TAG, "handleMessage(MSG_REMOVE_INSTRUMENTATION)");
// A message notifying the termination of a remote instrumentation instance.
// This instance should no longer keep track of the sender's Messenger.
if (!otherInstrumentations.remove(msg.replyTo)) {
Log.w(TAG, "Attempting to remove a non-existent binder!");
}
break;
case MSG_ADD_CLIENTS_IN_BUNDLE:
logDebugWithProcess(TAG, "handleMessage(MSG_ADD_CLIENTS_IN_BUNDLE)");
// A message from a new or an existing remote instrumentation instance that
// provides a bundle containing all clients registered to that instrumentation.
// Add all the remote clients
clientsRegistrationFromBundle(msg.getData(), true);
break;
case MSG_REMOVE_CLIENTS_IN_BUNDLE:
logDebugWithProcess(TAG, "handleMessage(MSG_REMOVE_CLIENTS_IN_BUNDLE)");
// A message from an existing remote instrumentation instance provides a bundle
// containing clients to be removed.
clientsRegistrationFromBundle(msg.getData(), false);
break;
case MSG_REG_CLIENT:
logDebugWithProcess(TAG, "handleMessage(MSG_REG_CLIENT)");
// A new client is registering with this instance of instrumentation. Register
// the client and potentially notify other instrumentation instances if needed.
registerClient(
msg.getData().getString(BUNDLE_KEY_CLIENT_TYPE),
(Messenger) msg.getData().getParcelable(BUNDLE_KEY_CLIENT_MESSENGER));
sendMessageToOtherInstr(MSG_REMOTE_ADD_CLIENT, msg.getData());
break;
case MSG_REMOTE_ADD_CLIENT:
logDebugWithProcess(TAG, "handleMessage(MSG_REMOTE_ADD_CLIENT)");
registerClient(
msg.getData().getString(BUNDLE_KEY_CLIENT_TYPE),
(Messenger) msg.getData().getParcelable(BUNDLE_KEY_CLIENT_MESSENGER));
break;
case MSG_UN_REG_CLIENT:
logDebugWithProcess(TAG, "handleMessage(MSG_UN_REG_CLIENT)");
// A client is un-registering from this instance of instrumentation. Un-register
// the client and notify other instrumentation instances if needed.
unregisterClient(
msg.getData().getString(BUNDLE_KEY_CLIENT_TYPE),
(Messenger) msg.getData().getParcelable(BUNDLE_KEY_CLIENT_MESSENGER));
sendMessageToOtherInstr(MSG_REMOTE_REMOVE_CLIENT, msg.getData());
break;
case MSG_REMOTE_REMOVE_CLIENT:
logDebugWithProcess(TAG, "handleMessage(MSG_REMOTE_REMOVE_CLIENT)");
unregisterClient(msg.getData().getString(BUNDLE_KEY_CLIENT_TYPE), msg.replyTo);
break;
case MSG_REMOTE_CLEANUP_REQUEST:
logDebugWithProcess(TAG, "handleMessage(MSG_REMOTE_CLEANUP_REQUEST)");
if (otherInstrumentations.isEmpty()) {
Message m = obtainMessage(MSG_PERFORM_CLEANUP_FINISHED);
m.setData(msg.getData());
sendMessage(m);
break;
}
sendMessageToOtherInstr(MSG_PERFORM_CLEANUP, msg.getData());
break;
case MSG_PERFORM_CLEANUP:
logDebugWithProcess(TAG, "handleMessage(MSG_PERFORM_CLEANUP)");
instrumentation.runOnMainSync(activityFinisher);
sendMessageWithReply(msg.replyTo, MSG_PERFORM_CLEANUP_FINISHED, msg.getData());
break;
case MSG_PERFORM_CLEANUP_FINISHED:
logDebugWithProcess(TAG, "handleMessage(MSG_PERFORM_CLEANUP_FINISHED)");
notifyLatch((UUID) msg.getData().getSerializable(BUNDLE_KEY_UUID));
break;
default:
Log.w(TAG, "Unknown message code received: " + msg.what);
super.handleMessage(msg);
}
}
private void notifyLatch(UUID uuid) {
if (uuid != null && latches.containsKey(uuid)) {
latches.get(uuid).countDown();
} else {
Log.w(TAG, "Latch not found " + uuid);
}
}
private void associateLatch(final UUID latchId, final CountDownLatch latch) {
runSyncTask(
new Callable<Void>() {
@Override
public Void call() {
latches.put(latchId, latch);
return null;
}
});
}
private void disassociateLatch(final UUID latchId) {
runSyncTask(
new Callable<Void>() {
@Override
public Void call() {
latches.remove(latchId);
return null;
}
});
}
private <T> T runSyncTask(Callable<T> task) {
FutureTask<T> futureTask = new FutureTask<>(task);
post(futureTask);
try {
return futureTask.get();
} catch (InterruptedException e) {
throw new IllegalStateException(e.getCause());
} catch (ExecutionException e) {
throw new IllegalStateException(e.getCause());
}
}
private void doDie() {
Log.i(TAG, "terminating process");
// notify others of self termination
sendMessageToOtherInstr(MSG_REMOVE_INSTRUMENTATION, null);
otherInstrumentations.clear();
typedClients.clear();
logDebugWithProcess(TAG, "quitting looper...");
getLooper().quit();
logDebugWithProcess(TAG, "finishing instrumentation...");
instrumentation.finish(0, null);
instrumentation = null;
activityFinisher = null;
}
private Set<Messenger> getClientsForType(final String type) {
FutureTask<Set<Messenger>> associationTask =
new FutureTask<>(
new Callable<Set<Messenger>>() {
@Override
public Set<Messenger> call() {
return typedClients.get(type);
}
});
post(associationTask);
try {
return associationTask.get();
} catch (InterruptedException e) {
// Shouldn't happen, always waiting for finish
throw new IllegalStateException(e);
} catch (ExecutionException e) {
// Shouldn't happen, just adding (key,value) to a map
throw new IllegalStateException(e.getCause());
}
}
/**
* Helper method to send a message to a given Instrumentation Messenger. The message will have
* the msg.replyTo field set to this {@link IncomingHandler} and it will also include the map of
* uniquely typed client Messenger's it contains.
*
* @param toMessenger who to send the message to.
* @param what the type of message, value to assign to the what member.
* @param data the arbitrary data associated with this message, ignored if {@code null}.
*/
private void sendMessageWithReply(Messenger toMessenger, int what, Bundle data) {
logDebugWithProcess(TAG, "sendMessageWithReply type: " + what + " called");
// Construct a message for a given code and with the local Messenger
Message msg = obtainMessage(what);
msg.replyTo = messengerHandler;
if (data != null) {
msg.setData(data);
}
// The message should include all known clients
if (!typedClients.isEmpty()) {
Bundle clientsBundle = msg.getData();
// Flatten the map of clients to send it as part of a bundle.
ArrayList<String> keyList = new ArrayList<>(typedClients.keySet());
clientsBundle.putStringArrayList(BUNDLE_KEY_CLIENTS, keyList);
for (Map.Entry<String, Set<Messenger>> entry : typedClients.entrySet()) {
String clientType = String.valueOf(entry.getKey());
Messenger[] clientArray =
entry.getValue().toArray(new Messenger[entry.getValue().size()]);
clientsBundle.putParcelableArray(clientType, clientArray);
}
msg.setData(clientsBundle);
}
// Send the message
try {
toMessenger.send(msg);
} catch (RemoteException e) {
// In this case the process was terminated or crashed before we could
// even do anything with it; there is nothing we can do other than removing
// this instrumentation connection instance.
Log.w(TAG, "The remote process is terminated unexpectedly", e);
// Clean up our otherInstrumentations list
instrBinderDied(toMessenger);
}
}
private void sendMessageToOtherInstr(int what, Bundle data) {
logDebugWithProcess(
TAG, "sendMessageToOtherInstr() called with: what = [%s], data = [%s]", what, data);
for (Messenger otherInstr : otherInstrumentations) {
sendMessageWithReply(otherInstr, what, data);
}
}
/**
* Helper method to extract the all clients from the given bundle and call {@link
* #registerClient(String, Messenger)} or {@link #unregisterClient(String, Messenger)}.
*
* @param clientsBundle The message bundle containing clients info
* @param shouldRegister Whether to register or unregister given clients
*/
private void clientsRegistrationFromBundle(Bundle clientsBundle, boolean shouldRegister) {
logDebugWithProcess(TAG, "clientsRegistrationFromBundle called");
if (null == clientsBundle) {
Log.w(TAG, "The client bundle is null, ignoring...");
return;
}
ArrayList<String> clientTypes = clientsBundle.getStringArrayList(BUNDLE_KEY_CLIENTS);
if (null == clientTypes) {
Log.w(TAG, "No clients found in the given bundle");
return;
}
for (String type : clientTypes) {
Parcelable[] clientArray = clientsBundle.getParcelableArray(String.valueOf(type));
if (clientArray != null) {
for (Parcelable client : clientArray) {
if (shouldRegister) {
registerClient(type, (Messenger) client);
} else {
unregisterClient(type, (Messenger) client);
}
}
}
}
}
private void registerClient(String type, Messenger client) {
logDebugWithProcess(
TAG, "registerClient called with type = [%s] client = [%s]", type, client);
checkNotNull(type, "type cannot be null!");
checkNotNull(client, "client cannot be null!");
Set<Messenger> clientSet = typedClients.get(type);
if (null == clientSet) {
// Add the new client
clientSet = new HashSet<>();
clientSet.add(client);
typedClients.put(type, clientSet);
return;
}
// Add the new client
clientSet.add(client);
}
private void unregisterClient(String type, Messenger client) {
logDebugWithProcess(
TAG, "unregisterClient called with type = [%s] client = [%s]", type, client);
checkNotNull(type, "type cannot be null!");
checkNotNull(client, "client cannot be null!");
if (!typedClients.containsKey(type)) {
Log.w(TAG, "There are no registered clients for type: " + type);
return;
}
Set<Messenger> clientSet = typedClients.get(type);
if (!clientSet.contains(client)) {
Log.w(
TAG,
"Could not unregister client for type "
+ type
+ " because it doesn't seem to be registered");
return;
}
// Remove this client first, to avoid notifying itself
clientSet.remove(client);
if (clientSet.isEmpty()) {
typedClients.remove(type);
}
}
private void instrBinderDied(Messenger instrMessenger) {
Message msg = obtainMessage(MSG_REMOVE_INSTRUMENTATION);
msg.replyTo = instrMessenger;
sendMessage(msg);
}
} // close IncomingHandler
}