public class

InstrumentationConnection

extends java.lang.Object

 java.lang.Object

↳androidx.test.internal.runner.InstrumentationConnection

Gradle dependencies

compile group: 'androidx.test', name: 'orchestrator', version: '1.5.0'

  • groupId: androidx.test
  • artifactId: orchestrator
  • version: 1.5.0

Artifact androidx.test:orchestrator:1.5.0 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.test:orchestrator com.android.support.test:orchestrator

Overview

A singleton class that facilitates the communication between different instrumentation test runner instances running on different processes.

Upon creation, a broadcast with an Intent containing a InstrumentationConnection.BROADCAST_FILTER action, will be sent out to notify other runners of this instance. During handshake, both instances will exchange their own IBinder's for further IPC, along with the list of all known client's s. Where a client could be any other testing framework that is built on top of AndroidJUnitRunner (example: Espresso, UiAutomator, etc.)

To get the instance of this object InstrumentationConnection.getInstance() should be called. The user of this class should then call InstrumentationConnection prior to attempting to use any functionality of this class. Call InstrumentationConnection.terminate() after using InstrumentationConnection to release any resources. Failure to do so will lead to memory leaks and unexpected behavior.

Summary

Fields
public static final java.lang.StringBROADCAST_FILTER

The intent action used to discover other instrumentation instances

Methods
public synchronized java.util.Set<Messenger>getClientsForType(java.lang.String type)

Helper method to obtain a set of clients of the same type.

public static InstrumentationConnectiongetInstance()

Returns a InstrumentationConnection object

public synchronized voidinit(Instrumentation instrumentation, MonitoringInstrumentation.ActivityFinisher finisher)

The initialization consists of:

  1. Sending a broadcast via a well known that contains a which others can use the send messages.

public synchronized voidregisterClient(java.lang.String type, Messenger messenger)

Register a client and notify all other clients of the same type if needed.

public synchronized voidrequestRemoteInstancesActivityCleanup()

Request all remote instrumentation instances to finish all activities in order to insure a clean state before/after each test.

public synchronized voidterminate()

This methods should be called after InstrumentationConnection was called.

public synchronized voidunregisterClient(java.lang.String type, Messenger messenger)

Un-register a client and notify all other clients of the same type if needed.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final java.lang.String BROADCAST_FILTER

The intent action used to discover other instrumentation instances

Methods

public static InstrumentationConnection getInstance()

Returns a InstrumentationConnection object

Returns:

an instance of InstrumentationConnection object.

public synchronized void init(Instrumentation instrumentation, MonitoringInstrumentation.ActivityFinisher finisher)

The initialization consists of:

  1. Sending a broadcast via a well known that contains a which others can use the send messages.
  2. Registering this instance for the same intent broadcasts to enable future discovery of newly started instrumentation runners on other processes.
The caller of this method must call InstrumentationConnection.terminate() to preform the proper clean up which includes unregister from the above defined broadcast.

Parameters:

instrumentation: the instance this running in
finisher: an activity finisher to use when performing cleanup

public synchronized void terminate()

This methods should be called after InstrumentationConnection was called. The purpose of this method is to preform any required clean up along with unregistering from any broadcast receivers.

public synchronized void requestRemoteInstancesActivityCleanup()

Request all remote instrumentation instances to finish all activities in order to insure a clean state before/after each test.

public synchronized void registerClient(java.lang.String type, Messenger messenger)

Register a client and notify all other clients of the same type if needed.

Parameters:

type: the type of the client
messenger: a to use for future communication with the client

public synchronized java.util.Set<Messenger> getClientsForType(java.lang.String type)

Helper method to obtain a set of clients of the same type.

Parameters:

type: the type of the client

Returns:

a Set of Messengers of the desired client type, null is returned if client type is unknown

public synchronized void unregisterClient(java.lang.String type, Messenger messenger)

Un-register a client and notify all other clients of the same type if needed.

Parameters:

type: the type of the client
messenger: a to use for future communication with the client

Source

/*
 * 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.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
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.
 *
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // used by espresso remote
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
}