Project import generated by Copybara

Companion SDK version: 2.0.1
 Compatible Android Mobile Version: 1.0.0
 Compatible IOS Mobile Version: 1.0.0

Release-Id: aae-companiondevice-android_20230925.01_RC00
Change-Id: I9dd6116e5dbf3fa93f19206142863bba68a2cb49
diff --git a/companiondevice/build.gradle b/companiondevice/build.gradle
index cbf8085..1abf7c7 100644
--- a/companiondevice/build.gradle
+++ b/companiondevice/build.gradle
@@ -8,13 +8,13 @@
 }
 
 android {
-    compileSdkVersion 33
+    compileSdkVersion 34
 
     defaultConfig {
         applicationId "com.google.android.companiondevicesupport"
         minSdkVersion 29
-        targetSdkVersion 33
-        versionCode 2161
+        targetSdkVersion 34
+        versionCode 2197
         versionName "1.0"
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
diff --git a/libs/connecteddevice/res/values/config.xml b/libs/connecteddevice/res/values/config.xml
index 910e553..ee45330 100644
--- a/libs/connecteddevice/res/values/config.xml
+++ b/libs/connecteddevice/res/values/config.xml
@@ -17,8 +17,8 @@
 
 <resources>
     <!-- Current version of the SDK -->
-    <string name="hu_companion_sdk_version" translatable="false">2.0.0</string>
-    <integer name="hu_companion_binder_version" translatable="false">0</integer>
+    <string name="hu_companion_sdk_version" translatable="false">2.0.1</string>
+    <integer name="hu_companion_binder_version" translatable="false">1</integer>
     <!-- Mobile SDK of later version should be compatible with the HU SDK. -->
     <string name="compatible_min_mobile_version_android" translatable="false">1.0.0</string>
     <string name="compatible_min_mobile_version_ios" translatable="false">1.0.0</string>
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/FeatureConnector.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/api/FeatureConnector.kt
index 4f61708..2631c56 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/api/FeatureConnector.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/FeatureConnector.kt
@@ -59,7 +59,6 @@
 class FeatureConnector(
   private val context: Context,
   override val featureId: ParcelUuid,
-  override val callback: SafeConnector.Callback,
   private val minSupportedVersion: Int = 0
 ) : SafeConnector {
 
@@ -75,6 +74,11 @@
 
   private val queryIdGenerator = QueryIdGenerator()
 
+  override lateinit var callback: SafeConnector.Callback
+
+  override val isConnected: Boolean
+    get() = coordinatorProxy != null && !waitingForConnection.get()
+
   override val connectedDevices: List<String>
     get() = coordinatorProxy?.getConnectedDevices() ?: emptyList()
 
@@ -128,6 +132,10 @@
           loge("Incompatible companion platform version. Aborting.")
           return
         }
+        if (!this@FeatureConnector::callback.isInitialized) {
+          loge("FeatureConnector callback was not initialized. Aborting.")
+          return
+        }
         coordinatorProxy =
           when {
             platformVersion > 0 ->
@@ -169,8 +177,9 @@
       }
     }
 
-  init {
-    logd("Initiating connection to companion platform.")
+  override fun connect(callback: SafeConnector.Callback) {
+    logd("Initiating connection to companion platform and initializing callback.")
+    this.callback = callback
     bindToService(ACTION_QUERY_API_VERSION, versionCheckConnection)
   }
 
@@ -220,7 +229,7 @@
     }
   }
 
-  override fun cleanUp() {
+  override fun disconnect() {
     logd("Disconnecting from the companion platform.")
     coordinatorProxy?.cleanUp()
     coordinatorProxy = null
@@ -240,7 +249,7 @@
 
   private fun onServiceDisconnected() {
     logd("Service has disconnected. Cleaning up.")
-    cleanUp()
+    disconnect()
   }
 
   private fun onNullBinding() {
@@ -411,6 +420,7 @@
     /** A generator of unique IDs for queries. */
     private class QueryIdGenerator {
       private val messageId = AtomicInteger(0)
+
       fun next(): Int {
         val current = messageId.getAndIncrement()
         messageId.compareAndSet(Int.MAX_VALUE, 0)
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/SafeConnector.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/api/SafeConnector.kt
index 43d7309..513a47e 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/api/SafeConnector.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/SafeConnector.kt
@@ -32,18 +32,23 @@
   val featureId: ParcelUuid
 
   /** [Callback] for connection events. */
-  val callback: Callback
+  var callback: Callback
 
   /** List of ids for the currently connected devices. */
   val connectedDevices: List<String>
 
+  /** Whether this [SafeConnector] is currently connected and ready for interaction. */
+  val isConnected: Boolean
+
+  /** Establishes a connection to the companion platform. */
+  fun connect(callback: Callback)
+
   /**
-  * Cleans up services and feature coordinators attached to the companion platform.
-  * Calling cleanUp will disconnect the feature from the platform and prevent it from receiving
-  * Companion related events. Expectation is that cleanUp will only be called before the
-  * SafeConnector object is disposed.
-  */
-  fun cleanUp()
+   * Cleans up services and feature coordinators attached to the companion platform. Will disconnect
+   * the feature from the platform and prevent it from receiving Companion-related events.
+   * Expectation is that disconnect will only be called before the SafeConnector object is disposed.
+   */
+  fun disconnect()
 
   /** Sends message to a device. */
   fun sendMessage(deviceId: String, message: ByteArray)
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/core/MultiProtocolDeviceController.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/core/MultiProtocolDeviceController.kt
index bec3c8e..6436fda 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/core/MultiProtocolDeviceController.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/core/MultiProtocolDeviceController.kt
@@ -164,7 +164,7 @@
       val driverDevices = storage.driverAssociatedDevices
       for (device in driverDevices) {
         if (device.isConnectionEnabled) {
-        initiateConnectionToDevice(UUID.fromString(device.deviceId))
+          initiateConnectionToDevice(UUID.fromString(device.deviceId))
         }
       }
       if (!enablePassenger) {
@@ -174,7 +174,7 @@
       logd(TAG, "Initiating connections with passenger devices.")
       val passengerDevices = storage.passengerAssociatedDevices
       for (device in passengerDevices) {
-          initiateConnectionToDevice(UUID.fromString(device.deviceId))
+        initiateConnectionToDevice(UUID.fromString(device.deviceId))
       }
     }
   }
@@ -406,6 +406,7 @@
   ) =
     object : IDiscoveryCallback.Stub() {
       override fun onDeviceConnected(protocolId: String) {
+        metricLogger.pushConnectedEvent()
         logd(
           TAG,
           "New connection protocol connected for $deviceId. id: $protocolId, protocol: $protocol"
@@ -443,6 +444,7 @@
       }
 
       override fun onDiscoveryStartedSuccessfully() {
+        metricLogger.pushDiscoveryStartedEvent()
         logd(TAG, "Connection discovery started successfully.")
       }
 
@@ -507,6 +509,7 @@
       }
 
       override fun onDiscoveryStartedSuccessfully() {
+        metricLogger.pushAssociationStartedEvent()
         associationCallback.aliveOrNull()?.onAssociationStartSuccess(response)
           ?: run {
             loge(
@@ -629,6 +632,7 @@
   }
 
   private fun handleAssociationError(error: Int, device: ConnectedRemoteDevice) {
+    metricLogger.pushCompanionErrorEvent(error, duringAssociation = device.callback != null)
     device.callback?.aliveOrNull()?.onAssociationError(error)
       ?: run {
         loge(
@@ -664,7 +668,7 @@
           callback.onSecureChannelEstablished(connectedDevice)
         }
         EventLog.onSecureChannelEstablished()
-        metricLogger.onSecureChannelEstablished()
+        metricLogger.pushSecureChannelEstablishedEvent()
       }
 
       override fun onEstablishSecureChannelFailure(error: ChannelError) {
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/metrics/CompanionStatsLog.java b/libs/connecteddevice/src/com/google/android/connecteddevice/metrics/CompanionStatsLog.java
index 08c2c19..11e7dab 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/metrics/CompanionStatsLog.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/metrics/CompanionStatsLog.java
@@ -17,6 +17,13 @@
    */
   public static final int COMPANION_STATUS_CHANGED = 210401;
 
+  /**
+   * CompanionErrorReported companion_error_reported<br>
+   * Usage: StatsLog.write(StatsLog.COMPANION_ERROR_REPORTED, int uid, int companion_error, boolean
+   * during_association);<br>
+   */
+  public static final int COMPANION_ERROR_REPORTED = 210402;
+
   // Constants for enum values.
 
   // Values for CompanionStatusChanged.companion_status
@@ -29,6 +36,28 @@
       4;
   public static final int COMPANION_STATUS_CHANGED__COMPANION_STATUS__DISCONNECTED = 5;
 
+  // Values for CompanionErrorReported.companion_error
+  public static final int COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_UNSPECIFIED = 0;
+  public static final int
+      COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_INVALID_HANDSHAKE = 1;
+  public static final int COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_INVALID_MSG = 2;
+  public static final int
+      COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_INVALID_DEVICE_ID = 3;
+  public static final int
+      COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_INVALID_VERIFICATION = 4;
+  public static final int
+      COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_INVALID_CHANNEL_STATE = 5;
+  public static final int
+      COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_INVALID_ENCRYPTION_KEY = 6;
+  public static final int COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_STORAGE_FAILURE =
+      7;
+  public static final int
+      COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_INVALID_SECURITY_KEY = 8;
+  public static final int
+      COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED = 9;
+  public static final int
+      COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_UNEXPECTED_DISCONNECTION = 10;
+
   // Annotation constants.
   public static final byte ANNOTATION_ID_IS_UID = StatsLog.ANNOTATION_ID_IS_UID;
   public static final byte ANNOTATION_ID_TRUNCATE_TIMESTAMP =
@@ -43,10 +72,27 @@
   public static final byte ANNOTATION_ID_STATE_NESTED = StatsLog.ANNOTATION_ID_STATE_NESTED;
 
   // Write methods
+  public static void write(int code, int arg1, int arg2, boolean arg3) {
+    final StatsEvent.Builder builder = StatsEvent.newBuilder();
+    builder.setAtomId(code);
+    builder.writeInt(arg1);
+    if (COMPANION_ERROR_REPORTED == code) {
+      builder.addBooleanAnnotation(ANNOTATION_ID_IS_UID, true);
+    }
+    builder.writeInt(arg2);
+    builder.writeBoolean(arg3);
+
+    builder.usePooledBuffer();
+    StatsLog.write(builder.build());
+  }
+
   public static void write(int code, int arg1, int arg2, long arg3) {
     final StatsEvent.Builder builder = StatsEvent.newBuilder();
     builder.setAtomId(code);
     builder.writeInt(arg1);
+    if (COMPANION_STATUS_CHANGED == code) {
+      builder.addBooleanAnnotation(ANNOTATION_ID_IS_UID, true);
+    }
     builder.writeInt(arg2);
     builder.writeLong(arg3);
 
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/metrics/EventMetricLogger.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/metrics/EventMetricLogger.kt
index bd3b013..a7a7a54 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/metrics/EventMetricLogger.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/metrics/EventMetricLogger.kt
@@ -1,19 +1,119 @@
 package com.google.android.connecteddevice.metrics
 
 import android.content.Context
-import android.os.Build
+import com.google.android.connecteddevice.model.Errors
+import android.os.RemoteException
+import com.google.android.connecteddevice.util.SafeLog.loge
+import java.time.Duration
+import java.time.Instant
 
 /** Logger to report Companion events to statsd */
 class EventMetricLogger(private val context: Context) {
   private val uid = context.packageManager.getPackageUid(context.packageName, 0)
-  fun onSecureChannelEstablished() {
-    // TODO(b/273968556): unable to find related System API on Android Q build.
-    if(Build.VERSION.SDK_INT < Build.VERSION_CODES.R ) return
-    CompanionStatsLog.write(
-      CompanionStatsLog.COMPANION_STATUS_CHANGED,
-      uid,
-      CompanionStatsLog.COMPANION_STATUS_CHANGED__COMPANION_STATUS__SECURE_CHANNEL_ESTABLISHED,
-      0
+  /** The time when association or reconnect discovery get started. */
+  private var discoveryStartedTime = Instant.EPOCH
+  private var connectedTime = Instant.EPOCH
+  private val defaultDuration = Duration.ZERO
+
+  /** Pushes the association discovery started event to statsd. */
+  fun pushAssociationStartedEvent() {
+    pushEventsWithoutException(
+      CompanionStatsLog.COMPANION_STATUS_CHANGED__COMPANION_STATUS__ASSOCIATION_STARTED,
+      defaultDuration
     )
   }
+
+  /** Pushes the reconnection discovery started event to statsd. */
+  fun pushDiscoveryStartedEvent() {
+    discoveryStartedTime = Instant.now()
+    pushEventsWithoutException(
+      CompanionStatsLog.COMPANION_STATUS_CHANGED__COMPANION_STATUS__DISCOVERY_STARTED,
+      defaultDuration
+    )
+  }
+
+  /** Pushes device get reconnected event to statsd. */
+  fun pushConnectedEvent() {
+    connectedTime = Instant.now()
+    pushEventsWithoutException(
+      CompanionStatsLog.COMPANION_STATUS_CHANGED__COMPANION_STATUS__CONNECTED,
+      Duration.between(discoveryStartedTime, connectedTime)
+    )
+  }
+
+  /** Pushes the secure channel established event to statsd. */
+  fun pushSecureChannelEstablishedEvent() {
+    pushEventsWithoutException(
+      CompanionStatsLog.COMPANION_STATUS_CHANGED__COMPANION_STATUS__SECURE_CHANNEL_ESTABLISHED,
+      Duration.between(connectedTime, Instant.now())
+    )
+  }
+
+  /** Pushes device disconnected event to statsd. */
+  fun pushDisconnectedEvent() {
+    pushEventsWithoutException(
+      CompanionStatsLog.COMPANION_STATUS_CHANGED__COMPANION_STATUS__DISCONNECTED,
+      defaultDuration
+    )
+  }
+
+  // TODO(b/298248724): Reports more errors.
+  fun pushCompanionErrorEvent(error: Int, duringAssociation: Boolean = false) {
+    val errorId =
+      when (error) {
+        Errors.DEVICE_ERROR_INVALID_HANDSHAKE ->
+          CompanionStatsLog
+            .COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_INVALID_HANDSHAKE
+        Errors.DEVICE_ERROR_INVALID_MSG ->
+          CompanionStatsLog.COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_INVALID_MSG
+        Errors.DEVICE_ERROR_INVALID_DEVICE_ID ->
+          CompanionStatsLog
+            .COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_INVALID_DEVICE_ID
+        Errors.DEVICE_ERROR_INVALID_VERIFICATION ->
+          CompanionStatsLog
+            .COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_INVALID_VERIFICATION
+        Errors.DEVICE_ERROR_INVALID_CHANNEL_STATE ->
+          CompanionStatsLog
+            .COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_INVALID_CHANNEL_STATE
+        Errors.DEVICE_ERROR_INVALID_ENCRYPTION_KEY ->
+          CompanionStatsLog
+            .COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_INVALID_ENCRYPTION_KEY
+        Errors.DEVICE_ERROR_STORAGE_FAILURE ->
+          CompanionStatsLog.COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_STORAGE_FAILURE
+        Errors.DEVICE_ERROR_INVALID_SECURITY_KEY ->
+          CompanionStatsLog
+            .COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_INVALID_SECURITY_KEY
+        Errors.DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED ->
+          CompanionStatsLog
+            .COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED
+        Errors.DEVICE_ERROR_UNEXPECTED_DISCONNECTION ->
+          CompanionStatsLog
+            .COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_UNEXPECTED_DISCONNECTION
+        else ->
+          CompanionStatsLog.COMPANION_ERROR_REPORTED__COMPANION_ERROR__DEVICE_ERROR_UNSPECIFIED
+      }
+    try {
+      CompanionStatsLog.write(
+        CompanionStatsLog.COMPANION_ERROR_REPORTED,
+        uid,
+        errorId,
+        duringAssociation
+      )
+    } catch (e: NoClassDefFoundError) {
+      loge(TAG, "Encounter error when pushing Companion error events to statsd; skip.")
+    }
+  }
+
+  private fun pushEventsWithoutException(eventId: Int, duration: Duration) {
+    try {
+      CompanionStatsLog.write(
+        CompanionStatsLog.COMPANION_STATUS_CHANGED, uid, eventId, duration.toMillis())
+    } catch (e: NoClassDefFoundError) {
+      loge(TAG, "Encounter error when pushing Companion events to statsd; skip.")
+    }
+  }
+
+  companion object {
+    private const val TAG = "EventMetricLogger"
+  }
 }
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceService.java b/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceService.java
index bd36f00..6dc294f 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceService.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceService.java
@@ -232,7 +232,8 @@
       return;
     }
     writer.printf(
-        "Companion SDK version is %s", getResources().getString(R.string.hu_companion_sdk_version));
+        "Companion SDK version is %s\n",
+        getResources().getString(R.string.hu_companion_sdk_version));
   }
 
   private void cleanup() {
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/FeatureConnectorTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/FeatureConnectorTest.kt
index 5486665..c691605 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/FeatureConnectorTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/FeatureConnectorTest.kt
@@ -28,6 +28,7 @@
 import com.google.protobuf.ExtensionRegistryLite
 import java.util.UUID
 import java.util.concurrent.ConcurrentHashMap
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.any
@@ -64,11 +65,23 @@
 
   private val testCoordinatorProxy = spy(TestCompanionApiProxy(true, listOf(testDeviceId)))
 
+  private lateinit var versionZeroConnector: FeatureConnector
+  private lateinit var versionOneConnector: FeatureConnector
+  private lateinit var versionTwoConnector: FeatureConnector
+
+  @Before
+  fun setUp() {
+    versionZeroConnector = FeatureConnector(context, testFeatureId, minSupportedVersion = 0)
+    versionOneConnector = FeatureConnector(context, testFeatureId, minSupportedVersion = 1)
+    versionTwoConnector = FeatureConnector(context, testFeatureId, minSupportedVersion  = 2)
+  }
+
   @Test
-  fun onInit_negativeVersions_aborts() {
+  fun onConnect_negativeVersions_aborts() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = -1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, -1)
+    val connector = FeatureConnector(context, testFeatureId, minSupportedVersion = -1)
+    connector.connect(mockCallback)
 
     assertThat(context.bindingActions)
       .containsExactly(ACTION_QUERY_API_VERSION, ACTION_BIND_FEATURE_COORDINATOR)
@@ -76,10 +89,11 @@
   }
 
   @Test
-  fun onInit_clientV1_platformV0_bindToFeatureCoordinator() {
+  fun onConnect_clientV1_platformV0_bindToFeatureCoordinator() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 0
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
     assertThat(connector.coordinatorProxy is LegacyApiProxy).isTrue()
     assertThat(context.bindingActions)
@@ -87,10 +101,11 @@
   }
 
   @Test
-  fun onInit_clientV1_platformV1_bindToFeatureCoordinator() {
+  fun onConnect_clientV1_platformV1_bindToFeatureCoordinator() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
     assertThat(connector.coordinatorProxy is SafeApiProxy).isTrue()
     assertThat(context.bindingActions)
@@ -98,10 +113,11 @@
   }
 
   @Test
-  fun onInit_clientV1_platformV2_bindToFeatureCoordinator() {
+  fun onConnect_clientV1_platformV2_bindToFeatureCoordinator() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 2
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
     assertThat(connector.coordinatorProxy is SafeApiProxy).isTrue()
     assertThat(context.bindingActions)
@@ -109,10 +125,11 @@
   }
 
   @Test
-  fun onInit_clientV2_platformV0_bindToFeatureCoordinator() {
+  fun onConnect_clientV2_platformV0_bindToFeatureCoordinator() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 0
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 2)
+    val connector = versionTwoConnector
+    connector.connect(mockCallback)
 
     assertThat(connector.coordinatorProxy is LegacyApiProxy).isTrue()
     assertThat(context.bindingActions)
@@ -120,11 +137,12 @@
   }
 
   @Test
-  fun onInit_clientV1_platformV1_incorrectServiceIssuesApiNotSupportedCallback() {
+  fun onConnect_clientV1_platformV1_incorrectServiceIssuesApiNotSupportedCallback() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
     val incorrectServiceContext = IncorrectServiceContext(mockPackageManager)
-    val connector = FeatureConnector(incorrectServiceContext, testFeatureId, mockCallback, 1)
+    val connector = FeatureConnector(incorrectServiceContext, testFeatureId, 1)
+    connector.connect(mockCallback)
 
     assertThat(connector.coordinatorProxy).isNull()
     assertThat(connector.platformVersion).isNull()
@@ -133,10 +151,11 @@
   }
 
   @Test
-  fun onInit_clientV2_platformV1_issuesApiNotSupportedCallback() {
+  fun onConnect_clientV2_platformV1_issuesApiNotSupportedCallback() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 2)
+    val connector = versionTwoConnector
+    connector.connect(mockCallback)
 
     assertThat(connector.coordinatorProxy).isNull()
     assertThat(connector.platformVersion).isNull()
@@ -145,10 +164,11 @@
   }
 
   @Test
-  fun onInit_clientV2_platformV2_bindToFeatureCoordinator() {
+  fun onConnect_clientV2_platformV2_bindToFeatureCoordinator() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 2
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 2)
+    val connector = versionTwoConnector
+    connector.connect(mockCallback)
 
     assertThat(connector.coordinatorProxy is SafeApiProxy).isTrue()
     assertThat(context.bindingActions)
@@ -156,10 +176,11 @@
   }
 
   @Test
-  fun onInit_clientV2_platformV3_bindToFeatureCoordinator() {
+  fun onConnect_clientV2_platformV3_bindToFeatureCoordinator() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 3
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 2)
+    val connector = versionTwoConnector
+    connector.connect(mockCallback)
 
     assertThat(connector.coordinatorProxy is SafeApiProxy).isTrue()
     assertThat(context.bindingActions)
@@ -182,17 +203,19 @@
     }
     setQueryIntentServicesAnswer(nullIntentAnswer)
     mockPlatformVersion = 0
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
     assertThat(connector.coordinatorProxy).isNull()
     verify(mockCallback).onFailedToConnect()
   }
 
   @Test
-  fun onSuccessfulInit_doesNotRetryBind() {
+  fun onSuccessfulConnect_doesNotRetryBind() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
     assertThat(connector.bindAttempts == 0).isTrue()
     assertThat(context.bindingActions)
@@ -205,7 +228,8 @@
     mockPlatformVersion = 1
     val failingContext = FailingContext(mockPackageManager)
     val shadowLooper = Shadows.shadowOf(Looper.getMainLooper())
-    val connector = FeatureConnector(failingContext, testFeatureId, mockCallback, 1)
+    val connector = FeatureConnector(failingContext, testFeatureId, 1)
+    connector.connect(mockCallback)
 
     repeat(FeatureConnector.MAX_BIND_ATTEMPTS) { shadowLooper.runToEndOfTasks() }
 
@@ -220,7 +244,8 @@
     mockPlatformVersion = 0
     val flakyContext = FailingLegacyContext(mockPackageManager)
     val shadowLooper = Shadows.shadowOf(Looper.getMainLooper())
-    val connector = FeatureConnector(flakyContext, testFeatureId, mockCallback, 1)
+    val connector = FeatureConnector(flakyContext, testFeatureId, 1)
+    connector.connect(mockCallback)
 
     repeat(FeatureConnector.MAX_BIND_ATTEMPTS) { shadowLooper.runToEndOfTasks() }
 
@@ -237,7 +262,8 @@
     mockPlatformVersion = 0
     val flakyContext = FlakyLegacyContext(mockPackageManager)
     val shadowLooper = Shadows.shadowOf(Looper.getMainLooper())
-    val connector = FeatureConnector(flakyContext, testFeatureId, mockCallback, 1)
+    val connector = FeatureConnector(flakyContext, testFeatureId, 1)
+    connector.connect(mockCallback)
 
     // Should repeat once for version query bind attempt, then reset, then MAX_BIND_ATTEMPTS times
     // for feature coordinator bind attempt.
@@ -261,7 +287,8 @@
   fun onDisconnected_invokedOnVersionCheckServiceDisconnected() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
     connector.versionCheckConnection.onServiceDisconnected(
       ComponentName(PACKAGE_NAME, VERSION_NAME)
@@ -273,7 +300,8 @@
   fun onDisconnected_invokedOnVersionCheckDeadBinding() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
     connector.versionCheckConnection.onBindingDied(ComponentName(PACKAGE_NAME, VERSION_NAME))
     verify(mockCallback).onDisconnected()
@@ -283,7 +311,8 @@
   fun onDisconnected_invokedOnFeatureCoordinatorServiceDisconnected() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
     connector.featureCoordinatorConnection.onServiceDisconnected(
       ComponentName(PACKAGE_NAME, FC_NAME)
@@ -295,7 +324,8 @@
   fun onFailedToConnect_invokedOnFeatureCoordinatorNullBinding() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
     connector.featureCoordinatorConnection.onNullBinding(ComponentName(PACKAGE_NAME, FC_NAME))
     verify(mockCallback).onFailedToConnect()
@@ -305,7 +335,8 @@
   fun onDisconnected_invokedOnFeatureCoordinatorDeadBinding() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
     connector.featureCoordinatorConnection.onBindingDied(ComponentName(PACKAGE_NAME, FC_NAME))
     verify(mockCallback).onDisconnected()
@@ -315,7 +346,8 @@
   fun onConnected_invokedAfterSuccessfulBind() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
     assertThat(connector.coordinatorProxy is SafeApiProxy).isTrue()
     verify(mockCallback).onConnected()
@@ -325,7 +357,8 @@
   fun onConnected_invokedAfterSuccessfulLegacyBind() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 0
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
     assertThat(connector.coordinatorProxy is LegacyApiProxy).isTrue()
     verify(mockCallback).onConnected()
@@ -335,7 +368,8 @@
   fun sendMessage_sendsMessage() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     connector.coordinatorProxy = testCoordinatorProxy
     connector.sendMessage(testDeviceId, testMessage)
 
@@ -346,7 +380,8 @@
   fun onMessageFailedToSend_invokedOnNullProxy() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     connector.coordinatorProxy = null
     connector.sendMessage(testDeviceId, testMessage)
 
@@ -357,7 +392,8 @@
   fun onMessageFailedToSend_invokedWhenDeviceNotFound() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     connector.coordinatorProxy = testCoordinatorProxy
     val unexpectedDevice = UUID.randomUUID().toString()
     connector.sendMessage(unexpectedDevice, testMessage)
@@ -371,7 +407,8 @@
   fun onMessageFailedToSend_invokedWhenSendMessageFails() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     val coordinatorProxy = TestCompanionApiProxy(false, listOf(testDeviceId))
     connector.coordinatorProxy = coordinatorProxy
     connector.sendMessage(testDeviceId, testMessage)
@@ -383,7 +420,8 @@
   fun sendMessage_onMessageFailedToSend_invokedOnVersionMismatch() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 0
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     connector.sendMessage(testDeviceId, testMessage)
 
     verify(mockCallback).onMessageFailedToSend(testDeviceId, testMessage, isTransient = false)
@@ -393,7 +431,8 @@
   fun sendQuery_sendsQueryToOwnFeatureId() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     connector.coordinatorProxy = testCoordinatorProxy
     val request = ByteUtils.randomBytes(10)
     val parameters = ByteUtils.randomBytes(10)
@@ -412,7 +451,8 @@
   fun sendQuery_addsCallbackToCallbacksMap() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     connector.coordinatorProxy = testCoordinatorProxy
     val request = ByteUtils.randomBytes(10)
     val parameters = ByteUtils.randomBytes(10)
@@ -426,7 +466,8 @@
   fun sendQuery_onQueryFailedToSend_invokedOnNullProxy() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     connector.coordinatorProxy = null
     val request = ByteUtils.randomBytes(10)
     val parameters = ByteUtils.randomBytes(10)
@@ -440,7 +481,8 @@
   fun sendQuery_onQueryFailedToSend_invokedOnDeviceIdNotFound() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     connector.coordinatorProxy = testCoordinatorProxy
     val request = ByteUtils.randomBytes(10)
     val parameters = ByteUtils.randomBytes(10)
@@ -454,7 +496,8 @@
   fun respondToQuery_doesNotSendResponseWithNullProxy() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     connector.coordinatorProxy = null
     val response = ByteUtils.randomBytes(10)
     connector.respondToQuery(testDeviceId, 1, success = true, response)
@@ -466,7 +509,8 @@
   fun respondToQuery_doesNotSendResponseWithUnrecognizedQueryId() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     connector.coordinatorProxy = testCoordinatorProxy
     val nonExistentQueryId = 0
     val response = ByteUtils.randomBytes(10)
@@ -481,7 +525,8 @@
     mockPlatformVersion = 1
     val queryId = 1
     val response = ByteUtils.randomBytes(10)
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     connector.coordinatorProxy = testCoordinatorProxy
     testCoordinatorProxy.queryResponseRecipients.put(queryId, testFeatureId)
     connector.respondToQuery(testDeviceId, queryId, success = true, response)
@@ -499,7 +544,8 @@
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
     val queryId = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     connector.coordinatorProxy = testCoordinatorProxy
     testCoordinatorProxy.queryResponseRecipients.put(queryId, testFeatureId)
     connector.respondToQuery(testDeviceId, queryId, success = true, null)
@@ -516,7 +562,8 @@
   fun respondToQuery_onMessageFailedToSend_invokedWhenSendMessageFailed() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     val proxy = spy(TestCompanionApiProxy(false, listOf(testDeviceId)))
     connector.coordinatorProxy = proxy
     val queryId = 1
@@ -531,7 +578,8 @@
   fun retrieveCompanionApplicationName_sendsAppNameQueryToSystemFeature() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     connector.coordinatorProxy = testCoordinatorProxy
     val callback = mock<SafeConnector.AppNameCallback>()
 
@@ -551,7 +599,8 @@
   fun retrieveCompanionApplicationName_onMessageFailedToSend_invokedWhenSendMessageFailed() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     val proxy = spy(TestCompanionApiProxy(false, listOf(testDeviceId)))
     connector.coordinatorProxy = proxy
     val appNameCallback = mock<SafeConnector.AppNameCallback>()
@@ -564,7 +613,8 @@
   fun retrieveAssociatedDevices_worksWithSafeListener() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     val mockListener = mock<ISafeOnAssociatedDevicesRetrievedListener>()
     connector.coordinatorProxy = testCoordinatorProxy
 
@@ -577,7 +627,8 @@
   fun retrieveAssociatedDevices_worksWithLegacyListener() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 0
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     val mockListener = mock<IOnAssociatedDevicesRetrievedListener>()
     connector.coordinatorProxy = testCoordinatorProxy
 
@@ -590,7 +641,8 @@
   fun retrieveAssociatedDevices_returnsSilentlyOnNullCoordinator() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
     val mockListener = mock<ISafeOnAssociatedDevicesRetrievedListener>()
     connector.coordinatorProxy = null
 
@@ -600,65 +652,71 @@
   }
 
   @Test
-  fun onCleanUp_cleansUpFeatureCoordinator() {
+  fun disconnect_cleansUpFeatureCoordinator() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
     assertThat(connector.coordinatorProxy).isNotNull()
-    connector.cleanUp()
+    connector.disconnect()
     assertThat(connector.coordinatorProxy).isNull()
   }
 
   @Test
-  fun onCleanUp_cleansUpFeatureCoordinator_legacyPlatform() {
+  fun disconnect_cleansUpFeatureCoordinator_legacyPlatform() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 0
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
     assertThat(connector.coordinatorProxy).isNotNull()
-    connector.cleanUp()
+    connector.disconnect()
     assertThat(connector.coordinatorProxy).isNull()
   }
 
   @Test
-  fun onCleanUp_callsCallbackDisconnected() {
+  fun disconnect_callsCallbackDisconnected() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
-    connector.cleanUp()
+    connector.disconnect()
     verify(mockCallback).onDisconnected()
   }
 
   @Test
-  fun onCleanUp_callsCallbackDisconnected_legacyPlatform() {
+  fun disconnect_callsCallbackDisconnected_legacyPlatform() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 0
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
-    connector.cleanUp()
+    connector.disconnect()
     verify(mockCallback).onDisconnected()
   }
 
   @Test
-  fun onCleanUp_unbindsFromContext() {
+  fun disconnect_unbindsFromContext() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 1
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
-    connector.cleanUp()
+    connector.disconnect()
     assertThat(context.unbindServiceConnection)
       .containsExactly(connector.featureCoordinatorConnection)
   }
 
   @Test
-  fun onCleanUp_unbindsFromContext_legacyPlatform() {
+  fun disconnect_unbindsFromContext_legacyPlatform() {
     setQueryIntentServicesAnswer(defaultServiceAnswer)
     mockPlatformVersion = 0
-    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val connector = versionOneConnector
+    connector.connect(mockCallback)
 
-    connector.cleanUp()
+    connector.disconnect()
     assertThat(context.unbindServiceConnection)
       .containsExactly(connector.featureCoordinatorConnection)
   }