Project import generated by Copybara

Included changes:

Release-Id: aae-companiondevice-android_20220202.00_RC00
Change-Id: Ib2767be29ae02933a60f44a7d6f749f459ed54b5
diff --git a/companiondevice/AndroidManifest.xml b/companiondevice/AndroidManifest.xml
index d579588..de87f10 100644
--- a/companiondevice/AndroidManifest.xml
+++ b/companiondevice/AndroidManifest.xml
@@ -16,7 +16,9 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.google.android.companiondevicesupport">
+          xmlns:tools="http://schemas.android.com/tools"
+          package="com.google.android.companiondevicesupport"
+          tools:ignore="all">
 
     <!--  Needed for BLE scanning/advertising -->
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
@@ -78,10 +80,6 @@
                 android:resource="@bool/enable_proxy" />
             <meta-data android:name="com.google.android.connecteddevice.transport_protocols"
                 android:resource="@array/transport_protocols" />
-            <meta-data android:name="com.google.android.connecteddevice.enable_capabilities_exchange"
-                android:resource="@bool/enable_capabilities" />
-            <meta-data android:name="com.google.android.connecteddevice.enable_feature_coordinator"
-                android:resource="@bool/enable_feature_coordinator" />
             <meta-data android:name="com.google.android.connecteddevice.supported_oob_channels"
                 android:resource="@array/supported_oob_channels"/>
 
@@ -101,8 +99,6 @@
                 android:resource="@array/early_fg_user_services"/>
             <meta-data android:name="com.google.android.connecteddevice.unlock_services"
                 android:resource="@array/unlock_fg_user_services"/>
-            <meta-data android:name="com.google.android.connecteddevice.enable_feature_coordinator"
-                android:resource="@bool/enable_feature_coordinator" />
         </service>
         <service
             android:name="com.google.android.connecteddevice.service.SppService"
@@ -205,6 +201,8 @@
                        android:resource="@string/trusted_device_feature_title" />
             <meta-data android:name="com.android.settings.category"
                        android:value="com.android.settings.category.ia.security" />
+            <meta-data android:name="com.android.settings.icon_tintable"
+                       android:value="true" />
         </activity>
     </application>
 </manifest>
diff --git a/companiondevice/build.gradle b/companiondevice/build.gradle
index 5a50a1b..20f8f2c 100644
--- a/companiondevice/build.gradle
+++ b/companiondevice/build.gradle
@@ -1,4 +1,5 @@
 apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
 
 buildscript {
     repositories {
@@ -13,7 +14,7 @@
         applicationId "com.google.android.companiondevicesupport"
         minSdkVersion 29
         targetSdkVersion 30
-        versionCode 1181
+        versionCode 1249
         versionName "1.0"
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
diff --git a/companiondevice/res/anim/fade_in.xml b/companiondevice/res/anim/fade_in.xml
new file mode 100644
index 0000000..bff039f
--- /dev/null
+++ b/companiondevice/res/anim/fade_in.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+    android:duration="@android:integer/config_shortAnimTime"
+    android:interpolator="@android:anim/decelerate_interpolator"
+    android:fromAlpha="0"
+    android:toAlpha="1" />
diff --git a/companiondevice/res/anim/fade_out.xml b/companiondevice/res/anim/fade_out.xml
new file mode 100644
index 0000000..35c88a3
--- /dev/null
+++ b/companiondevice/res/anim/fade_out.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+    android:duration="@android:integer/config_shortAnimTime"
+    android:interpolator="@android:anim/decelerate_interpolator"
+    android:fromAlpha="1"
+    android:toAlpha="0" />
diff --git a/companiondevice/res/drawable/ic_baseline_star_24.xml b/companiondevice/res/drawable/ic_baseline_star_24.xml
new file mode 100644
index 0000000..2a59f29
--- /dev/null
+++ b/companiondevice/res/drawable/ic_baseline_star_24.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
+</vector>
diff --git a/companiondevice/res/drawable/ic_baseline_star_border_24.xml b/companiondevice/res/drawable/ic_baseline_star_border_24.xml
new file mode 100644
index 0000000..fd73f5d
--- /dev/null
+++ b/companiondevice/res/drawable/ic_baseline_star_border_24.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
+</vector>
diff --git a/companiondevice/res/layout/associated_device_detail_fragment.xml b/companiondevice/res/layout/associated_device_detail_fragment.xml
index 84a1743..5cd268b 100644
--- a/companiondevice/res/layout/associated_device_detail_fragment.xml
+++ b/companiondevice/res/layout/associated_device_detail_fragment.xml
@@ -96,6 +96,35 @@
             android:textAppearance="@style/SettingsItemButtonTextStyle" />
       </LinearLayout>
       <LinearLayout
+          android:id="@+id/claim_button"
+          android:layout_width="0dp"
+          android:layout_height="wrap_content"
+          android:orientation="horizontal"
+          android:gravity="center"
+          android:layout_weight="1"
+          android:background="@drawable/car_ui_list_item_background"
+          android:clickable="true"
+          android:minHeight="?attr/companionListItemMinHeight">
+        <ImageView
+            android:id="@+id/claim_button_icon"
+            android:layout_width="?attr/companionSecondaryIconSize"
+            android:layout_height="?attr/companionSecondaryIconSize"
+            android:scaleType="fitXY"
+            android:src="@drawable/ic_baseline_star_border_24"
+            android:tint="?attr/companionColorAccent"
+            tools:ignore="ContentDescription" />
+        <TextView
+            android:id="@+id/claim_button_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/settings_list_item_text_no_icon_margin_start"
+            android:layout_marginEnd="@dimen/settings_list_item_text_margin_end"
+            android:layout_marginBottom="@dimen/settings_list_item_content_margin_bottom"
+            android:layout_marginTop="@dimen/settings_list_item_content_margin_top"
+            android:text="@string/unclaimed_device"
+            android:textAppearance="@style/SettingsItemButtonTextStyle" />
+      </LinearLayout>
+      <LinearLayout
           android:id="@+id/forget_button"
           android:layout_width="0dp"
           android:layout_height="wrap_content"
diff --git a/companiondevice/res/layout/associated_devices_list_fragment.xml b/companiondevice/res/layout/associated_devices_list_fragment.xml
new file mode 100644
index 0000000..6f419c8
--- /dev/null
+++ b/companiondevice/res/layout/associated_devices_list_fragment.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.android.car.ui.recyclerview.CarUiRecyclerView
+      android:id="@+id/associated_devices_list"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent" />
+</FrameLayout>
diff --git a/companiondevice/res/values/colors.xml b/companiondevice/res/values/colors.xml
index cce576f..14520d0 100644
--- a/companiondevice/res/values/colors.xml
+++ b/companiondevice/res/values/colors.xml
@@ -19,6 +19,7 @@
     <color name="connection_color_connected">#34A853</color>
     <color name="connection_color_disconnected">#EA4335</color>
     <color name="connection_color_not_detected">#C4C4C4</color>
+    <color name="connection_color_detected">#FFC107</color>
 
     <color name="car_red_300">@*android:color/car_red_300</color>
     <color name="divider_color">#5F6368</color>
diff --git a/companiondevice/res/values/config.xml b/companiondevice/res/values/config.xml
index e2f7b56..13ae152 100644
--- a/companiondevice/res/values/config.xml
+++ b/companiondevice/res/values/config.xml
@@ -50,8 +50,6 @@
     </string-array>
 
     <bool name="enable_proxy">false</bool>
-    <bool name="enable_capabilities">false</bool>
-    <bool name="enable_feature_coordinator">true</bool>
 
     <bool name="enable_qr_code">false</bool>
     <bool name="enable_passenger">false</bool>
diff --git a/companiondevice/res/values/strings.xml b/companiondevice/res/values/strings.xml
index 97259ad..ecf5294 100644
--- a/companiondevice/res/values/strings.xml
+++ b/companiondevice/res/values/strings.xml
@@ -28,6 +28,8 @@
     <string name="add_associated_device_title">Connect to MyCompanion</string>
     <!-- Instruction for adding associated device [CHAR LIMIT=100] -->
     <string name="add_associated_device_subtitle">You can use your phone as a companion device to help manage your driving experience</string>
+    <!-- Title for a button for a user to associate a new device [CHAR LIMIT=50]-->
+    <string name="add_associated_device_button" translatable="false">Add new device</string>
     <!-- Instruction for opening Companion App [CHAR LIMIT=100] -->
     <string name="associated_device_install_app">Make sure you have <b>Companion App</b> installed on your phone</string>
     <!-- Instruction for opening Companion App [CHAR LIMIT=100] -->
@@ -110,12 +112,14 @@
     <!-- Skip button text [CHAR LIMIT=20] -->
     <string name="skip">Skip</string>
 
-    <!-- Connected status text [CHAR LIMIT=20]-->
+    <!-- Connected with secure channel status text [CHAR LIMIT=20]-->
     <string name="connected">Connected</string>
     <!-- Not detected status text [CHAR LIMIT=20]-->
     <string name="notDetected">Not detected</string>
     <!-- Disconnected status text [CHAR LIMIT=20]-->
     <string name="disconnected">Disconnected</string>
+    <!-- Connected without secure channel status text [CHAR LIMIT=20]-->
+    <string name="detected">Detected</string>
     <!-- Unknown status text [CHAR LIMIT=20]-->
     <string name="unknown">Unknown</string>
 
@@ -199,4 +203,14 @@
     <!-- Car SUW setup profile page instructions for user to scan the QR code. [CHAR LIMIT=150]-->
     <string name="suw_qr_instruction_text">Scan QR code to connect to&lt;br /&gt; &lt;b><xliff:g id="car_name" example="MyVehicle">%1$s</xliff:g>&lt;/b> <xliff:g id="advertised_car_name" example="(Vehicle 0000)">%2$s</xliff:g></string>
 
+    <!-- Setting passenger strings' translatable to false until reference UI has been defined. -->
+    <!-- Text for a claim button on a device that has been claimed by the current user. [CHAR LIMIT=30] -->
+    <string name="claimed_device" translatable="false">This is my device</string>
+    <!-- Text for a claim button on a device that has not been claimed by the current user. [CHAR LIMIT=30] -->
+    <string name="unclaimed_device" translatable="false">This is not my device</string>
+    <!-- Text for a label on a driver's device in the list. [CHAR LIMIT=30] -->
+    <string name="driver_device" translatable="false">My device</string>
+    <!-- Text for a label on a passenger's device in the list. [CHAR LIMIT=30] -->
+    <string name="passenger_device" translatable="false">Passenger\'s device</string>
+
 </resources>
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDeviceDetailFragment.java b/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDeviceDetailFragment.java
index 63d31ab..85d07bc 100644
--- a/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDeviceDetailFragment.java
+++ b/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDeviceDetailFragment.java
@@ -16,8 +16,11 @@
 
 package com.google.android.companiondevicesupport;
 
+import static com.google.android.connecteddevice.util.SafeLog.logd;
+
 import android.app.AlertDialog;
 import android.app.Dialog;
+import android.content.Context;
 import android.content.DialogInterface;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
@@ -34,10 +37,10 @@
 import androidx.annotation.NonNull;
 import androidx.core.content.ContextCompat;
 import androidx.lifecycle.ViewModelProvider;
-import androidx.lifecycle.ViewModelStoreOwner;
-import com.google.android.connecteddevice.model.AssociatedDeviceDetails;
 import com.google.android.connecteddevice.model.TransportProtocols;
 import com.google.android.connecteddevice.trust.TrustedDeviceConstants;
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails;
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState;
 import com.google.android.connecteddevice.ui.AssociatedDeviceViewModel;
 import com.google.android.connecteddevice.ui.AssociatedDeviceViewModelFactory;
 import java.util.Arrays;
@@ -47,12 +50,31 @@
 public class AssociatedDeviceDetailFragment extends Fragment {
   private static final String TAG = "AssociatedDeviceDetailFragment";
   private static final String REMOVE_DEVICE_DIALOG_TAG = "RemoveDeviceDialog";
+  private static final String ASSOCIATED_DEVICE_DETAILS_KEY = "AssociatedDeviceDetailsKey";
+
+  private AssociatedDeviceDetails deviceDetails;
   private TextView deviceName;
   private TextView connectionStatusText;
   private ImageView connectionStatusIndicator;
   private TextView connectionText;
   private ImageView connectionIcon;
   private AssociatedDeviceViewModel model;
+  private TextView claimText;
+  private ImageView claimIcon;
+
+  /**
+   * Returns an instance of the {@link AssociatedDeviceDetailFragment} that will display the details
+   * of the given {@link AssociatedDeviceDetails}.
+   */
+  @NonNull
+  public static AssociatedDeviceDetailFragment newInstance(AssociatedDeviceDetails deviceDetails) {
+    Bundle arguments = new Bundle();
+    arguments.putParcelable(ASSOCIATED_DEVICE_DETAILS_KEY, deviceDetails);
+
+    AssociatedDeviceDetailFragment fragment = new AssociatedDeviceDetailFragment();
+    fragment.setArguments(arguments);
+    return fragment;
+  }
 
   @Override
   public View onCreateView(
@@ -67,64 +89,100 @@
     connectionText = view.findViewById(R.id.connection_button_text);
     connectionStatusText = view.findViewById(R.id.connection_status_text);
     connectionStatusIndicator = view.findViewById(R.id.connection_status_indicator);
+    claimText = view.findViewById(R.id.claim_button_text);
+    claimIcon = view.findViewById(R.id.claim_button_icon);
     List<String> transportProtocols =
         Arrays.asList(getResources().getStringArray(R.array.transport_protocols));
     model =
         new ViewModelProvider(
-                (ViewModelStoreOwner) requireActivity(),
+                requireActivity(),
                 new AssociatedDeviceViewModelFactory(
                     requireActivity().getApplication(),
                     transportProtocols.contains(TransportProtocols.PROTOCOL_SPP),
-                    getResources().getString(R.string.ble_device_name_prefix)))
+                    getResources().getString(R.string.ble_device_name_prefix),
+                    getResources().getBoolean(R.bool.enable_passenger)))
             .get(AssociatedDeviceViewModel.class);
-    model.getCurrentDeviceDetails().observe(this, this::setDeviceDetails);
+    model.getAssociatedDevicesDetails().observe(this, this::setDeviceDetails);
 
     view.findViewById(R.id.connection_button)
         .setOnClickListener(
-            l -> {
-              model.toggleConnectionStatusForCurrentDevice();
-            });
+            l -> model.toggleConnectionStatusForDevice(deviceDetails.getAssociatedDevice()));
     view.findViewById(R.id.forget_button).setOnClickListener(v -> showRemoveDeviceDialog());
-    view.findViewById(R.id.trusted_device_feature_button)
-        .setOnClickListener(
-            v ->
-                model.startFeatureActivityForCurrentDevice(
-                    TrustedDeviceConstants.INTENT_ACTION_TRUSTED_DEVICE_SETTING));
+    view.findViewById(R.id.trusted_device_feature_button).setOnClickListener(
+        v ->
+            model.startFeatureActivityForDevice(
+                TrustedDeviceConstants.INTENT_ACTION_TRUSTED_DEVICE_SETTING,
+                deviceDetails.getAssociatedDevice()));
+    View claimButton = view.findViewById(R.id.claim_button);
+    if (getResources().getBoolean(R.bool.enable_passenger)) {
+      claimButton.setOnClickListener(v -> toggleClaim());
+    } else {
+      claimButton.setVisibility(View.GONE);
+    }
   }
 
   @Override
   public void onCreate(@Nullable Bundle bundle) {
     super.onCreate(bundle);
+
+    Bundle arguments = getArguments();
+    arguments.setClassLoader(AssociatedDeviceDetails.class.getClassLoader());
+    deviceDetails = arguments.getParcelable(ASSOCIATED_DEVICE_DETAILS_KEY);
+
     if (bundle != null) {
       resumeRemoveDeviceDialog();
     }
   }
 
-  private void setDeviceDetails(AssociatedDeviceDetails deviceDetails) {
-    if (deviceDetails == null) {
+  private void setDeviceDetails(List<AssociatedDeviceDetails> associatedDevicesDetails) {
+    int index = associatedDevicesDetails.indexOf(deviceDetails);
+    if (index == -1) {
+      logd(TAG, "Device details updated for non-matching device. Ignoring.");
+      return;
+    }
+    deviceDetails = associatedDevicesDetails.get(index);
+
+    Context context = getContext();
+    if (context == null) {
       return;
     }
     deviceName.setText(deviceDetails.getDeviceName());
 
     if (!deviceDetails.isConnectionEnabled()) {
       setConnectionStatus(
-          ContextCompat.getColor(getContext(), R.color.connection_color_disconnected),
+          ContextCompat.getColor(context, R.color.connection_color_disconnected),
           getString(R.string.disconnected),
-          ContextCompat.getDrawable(getContext(), R.drawable.ic_phonelink_ring_24dp),
+          ContextCompat.getDrawable(context, R.drawable.ic_phonelink_ring_24dp),
           getString(R.string.connect));
-    } else if (deviceDetails.isConnected()) {
+    } else if (deviceDetails.getConnectionState() == ConnectionState.CONNECTED) {
       setConnectionStatus(
-          ContextCompat.getColor(getContext(), R.color.connection_color_connected),
+          ContextCompat.getColor(context, R.color.connection_color_connected),
           getString(R.string.connected),
+          ContextCompat.getDrawable(context, R.drawable.ic_phonelink_erase_24dp),
+          getString(R.string.disconnect));
+    } else if (deviceDetails.getConnectionState() == ConnectionState.DETECTED) {
+      setConnectionStatus(
+          ContextCompat.getColor(getContext(), R.color.connection_color_detected),
+          getString(R.string.detected),
           ContextCompat.getDrawable(getContext(), R.drawable.ic_phonelink_erase_24dp),
           getString(R.string.disconnect));
     } else {
       setConnectionStatus(
-          ContextCompat.getColor(getContext(), R.color.connection_color_not_detected),
+          ContextCompat.getColor(context, R.color.connection_color_not_detected),
           getString(R.string.notDetected),
-          ContextCompat.getDrawable(getContext(), R.drawable.ic_phonelink_erase_24dp),
+          ContextCompat.getDrawable(context, R.drawable.ic_phonelink_erase_24dp),
           getString(R.string.disconnect));
     }
+
+    if (deviceDetails.belongsToDriver()) {
+      claimText.setText(getString(R.string.claimed_device));
+      claimIcon.setImageDrawable(
+          ContextCompat.getDrawable(context, R.drawable.ic_baseline_star_24));
+    } else {
+      claimText.setText(getString(R.string.unclaimed_device));
+      claimIcon.setImageDrawable(
+          ContextCompat.getDrawable(context, R.drawable.ic_baseline_star_border_24));
+    }
   }
 
   private void setConnectionStatus(
@@ -139,9 +197,10 @@
   }
 
   private void showRemoveDeviceDialog() {
-    String deviceName = model.getCurrentDeviceDetails().getValue().getDeviceName();
+    String deviceName = deviceDetails.getDeviceName();
     RemoveDeviceDialogFragment dialogFragment =
-        RemoveDeviceDialogFragment.newInstance(deviceName, (d, w) -> model.removeCurrentDevice());
+        RemoveDeviceDialogFragment.newInstance(
+          deviceName, (d, w) -> model.removeDevice(deviceDetails.getAssociatedDevice()));
     dialogFragment.show(getParentFragmentManager(), REMOVE_DEVICE_DIALOG_TAG);
   }
 
@@ -150,7 +209,16 @@
         (RemoveDeviceDialogFragment)
             getParentFragmentManager().findFragmentByTag(REMOVE_DEVICE_DIALOG_TAG);
     if (removeDeviceDialogFragment != null) {
-      removeDeviceDialogFragment.setOnConfirmListener((d, w) -> model.removeCurrentDevice());
+      removeDeviceDialogFragment.setOnConfirmListener(
+        (d, w) -> model.removeDevice(deviceDetails.getAssociatedDevice()));
+    }
+  }
+
+  private void toggleClaim() {
+    if (deviceDetails.belongsToDriver()) {
+      model.removeClaimOnDevice(deviceDetails.getAssociatedDevice());
+    } else {
+      model.claimDevice(deviceDetails.getAssociatedDevice());
     }
   }
 
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDevicesListFragment.kt b/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDevicesListFragment.kt
new file mode 100644
index 0000000..754bcab
--- /dev/null
+++ b/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDevicesListFragment.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2021 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 com.google.android.companiondevicesupport
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.ColorRes
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelStoreOwner
+import com.android.car.ui.recyclerview.CarUiContentListItem
+import com.android.car.ui.recyclerview.CarUiContentListItem.Action
+import com.android.car.ui.recyclerview.CarUiListItem
+import com.android.car.ui.recyclerview.CarUiListItemAdapter
+import com.android.car.ui.recyclerview.CarUiRecyclerView
+import com.google.android.connecteddevice.model.TransportProtocols
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState.CONNECTED
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState.DETECTED
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState.NOT_DETECTED
+import com.google.android.connecteddevice.ui.AssociatedDeviceViewModel
+import com.google.android.connecteddevice.ui.AssociatedDeviceViewModelFactory
+import com.google.android.connecteddevice.util.SafeLog.logd
+
+/** Fragment that shows the a list of associated devices and their connection status. */
+class AssociatedDevicesListFragment : Fragment() {
+  private lateinit var deviceListView: CarUiRecyclerView
+  private lateinit var model: AssociatedDeviceViewModel
+  private lateinit var adapter: CarUiListItemAdapter
+
+  /** The list of devices that are set within the [adapter]. */
+  private val listItems = mutableListOf<CarUiListItem>()
+
+  /** A listener for clicks on items within the list shown by this fragment. */
+  interface OnListItemClickListener {
+    /**
+     * Invoked when an item in the list has been clicked. This method is passed the device that is
+     * being shown by the item.
+     */
+    fun onListItemClicked(associatedDeviceDetails: AssociatedDeviceDetails)
+  }
+
+  public var onListItemClickListener: OnListItemClickListener? = null
+
+  override fun onCreateView(
+    inflater: LayoutInflater,
+    container: ViewGroup?,
+    savedInstanceState: Bundle?
+  ): View =
+    inflater.inflate(
+      R.layout.associated_devices_list_fragment,
+      container,
+      /* attachToRoot= */ false
+    )
+
+  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+    deviceListView = view.findViewById(R.id.associated_devices_list) as CarUiRecyclerView
+    deviceListView.adapter = CarUiListItemAdapter(listItems).also { adapter = it }
+
+    model = obtainAssociatedDeviceViewModel()
+    model.getAssociatedDevicesDetails().observe(this, ::refreshDeviceList)
+  }
+
+  private fun obtainAssociatedDeviceViewModel(): AssociatedDeviceViewModel {
+    val activity = requireActivity()
+    val transportProtocols = resources.getStringArray(R.array.transport_protocols)
+
+    return ViewModelProvider(
+        activity as ViewModelStoreOwner,
+        AssociatedDeviceViewModelFactory(
+          activity.getApplication(),
+          transportProtocols.contains(TransportProtocols.PROTOCOL_SPP),
+          resources.getString(R.string.ble_device_name_prefix),
+          resources.getBoolean(R.bool.enable_passenger)
+        )
+      )
+      .get(AssociatedDeviceViewModel::class.java)
+  }
+
+  private fun refreshDeviceList(associatedDeviceDetails: List<AssociatedDeviceDetails>) {
+    logd(TAG, "Change detected in associated devices. Refreshing list.")
+
+    // The adapter retains a reference to this list. So modify it in-place to ensure the adapter
+    // has the up-to-date contents.
+    listItems.clear()
+    listItems.addAll(associatedDeviceDetails.map { it.toCarUiContentListItem() })
+
+    adapter.notifyDataSetChanged()
+  }
+
+  /**
+   * Converts this [AssociatedDeviceDetails] to its analogous [CarUiContentListItem].
+   *
+   * When the list item is clicked, any [onListItemClickListener] set on this fragment will be
+   * invoked.
+   */
+  private fun AssociatedDeviceDetails.toCarUiContentListItem(): CarUiContentListItem {
+    val context = requireContext()
+
+    return CarUiContentListItem(Action.CHEVRON).apply {
+      setTitle(deviceName)
+      setBody(
+        context.getString(
+          if (belongsToDriver()) R.string.driver_device else R.string.passenger_device
+        )
+      )
+      context.getDrawable(R.drawable.ic_connection_indicator)?.mutate()?.let {
+        icon =
+          DrawableCompat.wrap(it).apply {
+            setTint(context.getColor(connectionState.toColorRes(isConnectionEnabled)))
+          }
+      }
+
+      setOnItemClickedListener {
+        onListItemClickListener?.onListItemClicked(this@toCarUiContentListItem)
+      }
+    }
+  }
+
+  @ColorRes
+  private fun ConnectionState.toColorRes(isConnectionEnabled: Boolean): Int =
+    if (isConnectionEnabled) {
+      when (this) {
+        NOT_DETECTED -> {
+          R.color.connection_color_not_detected
+        }
+        DETECTED -> {
+          R.color.connection_color_detected
+        }
+        CONNECTED -> {
+          R.color.connection_color_connected
+        }
+      }
+    } else {
+      R.color.connection_color_disconnected
+    }
+
+  companion object {
+    private const val TAG = "AssociatedDevicesListFragment"
+  }
+}
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/AssociationActivity.java b/companiondevice/src/com/google/android/companiondevicesupport/AssociationActivity.java
index 5d2e314..996c223 100644
--- a/companiondevice/src/com/google/android/companiondevicesupport/AssociationActivity.java
+++ b/companiondevice/src/com/google/android/companiondevicesupport/AssociationActivity.java
@@ -29,8 +29,10 @@
 import androidx.fragment.app.DialogFragment;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
 import android.view.View;
 import android.widget.Toast;
+import androidx.annotation.Nullable;
 import androidx.lifecycle.ViewModelProvider;
 import androidx.lifecycle.ViewModelStoreOwner;
 import com.android.car.setupwizardlib.CarSetupWizardCompatLayout;
@@ -39,6 +41,7 @@
 import com.google.android.connecteddevice.api.RemoteFeature;
 import com.google.android.connecteddevice.model.AssociatedDevice;
 import com.google.android.connecteddevice.model.TransportProtocols;
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails;
 import com.google.android.connecteddevice.ui.AssociatedDeviceViewModel;
 import com.google.android.connecteddevice.ui.AssociatedDeviceViewModelFactory;
 import java.util.Arrays;
@@ -50,6 +53,7 @@
   private static final String TAG = "CompanionAssociationActivity";
   private static final String COMPANION_LANDING_FRAGMENT_TAG = "CompanionLandingFragment";
   private static final String DEVICE_DETAIL_FRAGMENT_TAG = "AssociatedDeviceDetailFragment";
+  private static final String DEVICES_LIST_FRAGMENT_TAG = "AssociatedDevicesListFragment";
   private static final String PAIRING_CODE_FRAGMENT_TAG = "ConfirmPairingCodeFragment";
   private static final String TURN_ON_BLUETOOTH_FRAGMENT_TAG = "TurnOnBluetoothFragment";
   private static final String ASSOCIATION_ERROR_FRAGMENT_TAG = "AssociationErrorFragment";
@@ -58,25 +62,52 @@
   private static final String EXTRA_AUTH_IS_SETUP_PROFILE = "is_setup_profile_association";
   private static final String EXTRA_USE_IMMERSIVE_MODE = "useImmersiveMode";
   private static final String EXTRA_HIDE_SKIP_BUTTON = "hide_skip_button";
+  private static final String ASSOCIATED_DEVICE_DETAILS_BACKSTACK_NAME =
+      "AssociatedDeviceDetailsBackstack";
 
   private ToolbarController toolbar;
   private CarSetupWizardCompatLayout carSetupWizardLayout;
   private AssociatedDeviceViewModel model;
+
+  private boolean isPassengerEnabled = false;
   private boolean isStartedForSuw = false;
   private boolean isStartedForSetupProfile = false;
   private boolean isImmersive = false;
   private boolean hideSkipButton = false;
 
   @Override
-  public void onCreate(Bundle saveInstanceState) {
+  public void onCreate(Bundle savedInstanceState) {
+    isPassengerEnabled = getResources().getBoolean(R.bool.enable_passenger);
+
     resolveIntent();
+
     // Set theme before calling super.onCreate(bundle) to avoid recreating activity.
     setAssociationTheme();
-    super.onCreate(saveInstanceState);
+    super.onCreate(savedInstanceState);
     prepareLayout();
+
+    // Only need to attach the click listener if we are recreating this activity due to a
+    // configuration change.
+    if (isPassengerEnabled && savedInstanceState != null) {
+      maybeAttachItemClickListener();
+    }
+
     observeViewModel();
   }
 
+  /**
+   * Attempts to attach a click listener to a {@link AssociatedDevicesListFragment} if it is
+   * currently visible.
+   */
+  private void maybeAttachItemClickListener() {
+    Fragment fragment = getSupportFragmentManager().findFragmentByTag(DEVICES_LIST_FRAGMENT_TAG);
+    if (fragment == null) {
+      return;
+    }
+    ((AssociatedDevicesListFragment) fragment).setOnListItemClickListener(
+        this::showAssociatedDeviceDetailFragment);
+  }
+
   @Override
   protected void onStart() {
     super.onStart();
@@ -155,7 +186,8 @@
                 new AssociatedDeviceViewModelFactory(
                     getApplication(),
                     transportProtocols.contains(TransportProtocols.PROTOCOL_SPP),
-                    getResources().getString(R.string.ble_device_name_prefix)))
+                    getResources().getString(R.string.ble_device_name_prefix),
+                    getResources().getBoolean(R.bool.enable_passenger)))
             .get(AssociatedDeviceViewModel.class);
 
     model
@@ -198,15 +230,7 @@
 
     model
         .getBluetoothState()
-        .observe(
-            this,
-            state -> {
-              if (state != BluetoothAdapter.STATE_ON
-                  && getSupportFragmentManager().findFragmentByTag(DEVICE_DETAIL_FRAGMENT_TAG)
-                      != null) {
-                runOnUiThread(this::showTurnOnBluetoothDialog);
-              }
-            });
+        .observe(this, this::handleBluetoothStateChange);
     model
         .getPairingCode()
         .observe(
@@ -217,22 +241,8 @@
               }
             });
     model
-        .getCurrentDeviceDetails()
-        .observe(
-            this,
-            deviceDetails -> {
-              if (deviceDetails == null) {
-                return;
-              }
-              AssociatedDevice device = deviceDetails.getAssociatedDevice();
-              if (isStartedByFeature()) {
-                // Features always expect activity result when they start AssociationActivity.
-                setDeviceToReturn(device);
-                finish();
-              } else {
-                runOnUiThread(this::showAssociatedDeviceDetailFragment);
-              }
-            });
+        .getAssociatedDevicesDetails()
+        .observe(this, this::handleAssociatedDevicesDetailsChange);
     model
         .getRemovedDevice()
         .observe(
@@ -240,7 +250,6 @@
             device -> {
               if (device != null) {
                 runOnUiThread(() -> showDeviceRemovedToast(device.getDeviceName()));
-                showCompanionLandingFragment();
               }
             });
     model
@@ -258,6 +267,80 @@
             });
   }
 
+  private void handleBluetoothStateChange(int state) {
+    FragmentManager fragmentManager = getSupportFragmentManager();
+
+    // A warning dialog for Bluetooth should only be shown during association.
+    if (state != BluetoothAdapter.STATE_ON
+        && fragmentManager.findFragmentByTag(DEVICE_DETAIL_FRAGMENT_TAG) != null
+        && fragmentManager.findFragmentByTag(DEVICES_LIST_FRAGMENT_TAG) != null) {
+      runOnUiThread(this::showTurnOnBluetoothDialog);
+    }
+  }
+
+  private void handleAssociatedDevicesDetailsChange(List<AssociatedDeviceDetails> devicesDetails) {
+    // An empty list means that there are no more associated devices.
+    if (devicesDetails.isEmpty()) {
+      handleEmptyDeviceList();
+      return;
+    }
+
+    if (isPassengerEnabled) {
+      showAssociatedDevicesList(devicesDetails);
+    } else {
+      // Otherwise, there will only be one associated device.
+      showAssociatedDeviceDetails(devicesDetails.get(0));
+    }
+  }
+
+  private void showAssociatedDeviceDetails(AssociatedDeviceDetails deviceDetails) {
+    if (isStartedByFeature()) {
+      // Features always expect activity result when they start AssociationActivity.
+      setDeviceToReturn(deviceDetails.getAssociatedDevice());
+      finish();
+      return;
+    }
+
+    runOnUiThread(() -> showAssociatedDeviceDetailFragment(deviceDetails));
+  }
+
+  /** Shows a list of associated devices. The provided list should be non-empty. */
+  private void showAssociatedDevicesList(List<AssociatedDeviceDetails> devicesDetails) {
+    if (isStartedByFeature()) {
+      setDeviceToReturn(findFirstDriverDevice(devicesDetails));
+      finish();
+      return;
+    }
+
+    // If the device details are showing, then the details fragment will handle any UI changes.
+    if (isAssociatedDeviceDetailsShowing()) {
+      return;
+    }
+
+    runOnUiThread(this::showAssociatedDevicesListFragment);
+  }
+
+  @Nullable
+  private AssociatedDevice findFirstDriverDevice(List<AssociatedDeviceDetails> devicesDetails) {
+    for (AssociatedDeviceDetails deviceDetails : devicesDetails) {
+      if (deviceDetails.belongsToDriver()) {
+        return deviceDetails.getAssociatedDevice();
+      }
+    }
+    return null;
+  }
+
+  private boolean isAssociatedDeviceDetailsShowing() {
+    FragmentManager fragmentManager = getSupportFragmentManager();
+    for (int i = 0; i < fragmentManager.getBackStackEntryCount(); i++) {
+      if (ASSOCIATED_DEVICE_DETAILS_BACKSTACK_NAME.equals(
+          fragmentManager.getBackStackEntryAt(i).getName())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   private void showDeviceRemovedToast(String deviceName) {
     Toast.makeText(
             getApplicationContext(),
@@ -276,7 +359,23 @@
     showProgressBar();
   }
 
+  private void handleEmptyDeviceList() {
+    Fragment fragment =
+        getSupportFragmentManager().findFragmentByTag(COMPANION_LANDING_FRAGMENT_TAG);
+
+    if (fragment != null) {
+      logd(TAG,
+          "Device list is empty, but landing fragment already showing. Nothing more to be done.");
+      return;
+    }
+
+    showCompanionLandingFragment();
+  }
+
   private void showCompanionLandingFragment() {
+    maybeClearDetailsFragmentFromBackstack();
+    dismissButtons();
+
     if (getResources().getBoolean(R.bool.enable_qr_code)) {
       logd(TAG, "Showing LandingFragment with QR code.");
       showCompanionQrCodeLandingFragment();
@@ -297,6 +396,7 @@
         (CompanionQrCodeLandingFragment)
             getSupportFragmentManager().findFragmentByTag(COMPANION_LANDING_FRAGMENT_TAG);
     if (fragment != null && fragment.isVisible()) {
+      logd(TAG, "Attempted to show QR code, but fragment is visible already. Ignoring");
       return;
     }
     fragment =
@@ -328,11 +428,37 @@
     launchFragment(fragment, ASSOCIATION_ERROR_FRAGMENT_TAG);
   }
 
-  private void showAssociatedDeviceDetailFragment() {
-    dismissButtons();
+  private void showAssociatedDeviceDetailFragment(AssociatedDeviceDetails deviceDetails) {
+    // If passenger mode is enabled, then the device list fragment is shown first and will take
+    // care of adjusting the toolbar buttons.
+    if (!isPassengerEnabled) {
+      dismissButtons();
+    }
+
     hideProgressBar();
-    AssociatedDeviceDetailFragment fragment = new AssociatedDeviceDetailFragment();
-    launchFragment(fragment, DEVICE_DETAIL_FRAGMENT_TAG);
+
+    AssociatedDeviceDetailFragment fragment =
+        AssociatedDeviceDetailFragment.newInstance(deviceDetails);
+
+    // If passenger mode is enabled, the details should be pushed on top of the list of devices.
+    // Otherwise, it will be the root view.
+    if (isPassengerEnabled) {
+      addFragment(fragment, DEVICE_DETAIL_FRAGMENT_TAG);
+    } else {
+      launchFragment(fragment, DEVICE_DETAIL_FRAGMENT_TAG);
+    }
+
+    showTurnOnBluetoothDialog();
+  }
+
+  private void showAssociatedDevicesListFragment() {
+    showAssociateButton();
+    hideProgressBar();
+
+    AssociatedDevicesListFragment fragment = new AssociatedDevicesListFragment();
+    fragment.setOnListItemClickListener(this::showAssociatedDeviceDetailFragment);
+
+    launchFragment(fragment, DEVICES_LIST_FRAGMENT_TAG);
     showTurnOnBluetoothDialog();
   }
 
@@ -389,6 +515,23 @@
         });
   }
 
+  private void showAssociateButton() {
+    if (isStartedForSuw) {
+      return;
+    }
+
+    MenuItem associationButton =
+        MenuItem.builder(this)
+            .setTitle(R.string.add_associated_device_button)
+            .setOnClickListener(v -> {
+              dismissButtons();
+              model.startAssociation();
+            })
+           .build();
+
+    toolbar.setMenuItems(Arrays.asList(associationButton));
+  }
+
   private void showProgressBar() {
     if (isStartedForSuw) {
       carSetupWizardLayout.setProgressBarVisible(true);
@@ -412,6 +555,23 @@
     }
   }
 
+  /** Checks for the details page being shown and clears it from the Fragment backstack. */
+  private void maybeClearDetailsFragmentFromBackstack() {
+    // The details page is only added to the backstack if passenger mode is enabled. Also, no need
+    // to clear if the fragment isn't there.
+    if (!isPassengerEnabled
+        || getSupportFragmentManager().findFragmentByTag(DEVICE_DETAIL_FRAGMENT_TAG) == null) {
+      return;
+    }
+
+    // If the details fragment is showing, then it will be on the backstack.
+    FragmentManager fragmentManager = getSupportFragmentManager();
+    for (int i = 0; i < fragmentManager.getBackStackEntryCount(); i++) {
+      fragmentManager.popBackStack();
+    }
+  }
+
+  /** Displays the given {@code fragment} and replaces the current content. */
   private void launchFragment(Fragment fragment, String tag) {
     getSupportFragmentManager()
         .beginTransaction()
@@ -419,6 +579,21 @@
         .commit();
   }
 
+  /** Displays the given {@code fragment} and adds it to the backstack. */
+  private void addFragment(Fragment fragment, String tag) {
+    getSupportFragmentManager()
+        .beginTransaction()
+        .setCustomAnimations(
+            R.anim.fade_in,
+            R.anim.fade_out,
+            R.anim.fade_in,
+            R.anim.fade_out
+        )
+        .replace(R.id.fragment_container, fragment, tag)
+        .addToBackStack(ASSOCIATED_DEVICE_DETAILS_BACKSTACK_NAME)
+        .commit();
+  }
+
   private void retryAssociation() {
     dismissButtons();
     showProgressBar();
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/AssociationErrorFragment.java b/companiondevice/src/com/google/android/companiondevicesupport/AssociationErrorFragment.java
index 1c815bc..fe005bd 100644
--- a/companiondevice/src/com/google/android/companiondevicesupport/AssociationErrorFragment.java
+++ b/companiondevice/src/com/google/android/companiondevicesupport/AssociationErrorFragment.java
@@ -50,7 +50,8 @@
                           new AssociatedDeviceViewModelFactory(
                               requireActivity().getApplication(),
                               transportProtocols.contains(TransportProtocols.PROTOCOL_SPP),
-                              getResources().getString(R.string.ble_device_name_prefix)))
+                              getResources().getString(R.string.ble_device_name_prefix),
+                              getResources().getBoolean(R.bool.enable_passenger)))
                       .get(AssociatedDeviceViewModel.class);
               model.retryAssociation();
             });
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/CompanionLandingFragment.java b/companiondevice/src/com/google/android/companiondevicesupport/CompanionLandingFragment.java
index f7a6f9e..ea7bd21 100644
--- a/companiondevice/src/com/google/android/companiondevicesupport/CompanionLandingFragment.java
+++ b/companiondevice/src/com/google/android/companiondevicesupport/CompanionLandingFragment.java
@@ -75,7 +75,8 @@
                 new AssociatedDeviceViewModelFactory(
                     requireActivity().getApplication(),
                     transportProtocols.contains(TransportProtocols.PROTOCOL_SPP),
-                    getResources().getString(R.string.ble_device_name_prefix)))
+                    getResources().getString(R.string.ble_device_name_prefix),
+                    getResources().getBoolean(R.bool.enable_passenger)))
             .get(AssociatedDeviceViewModel.class);
     TextView connectToCarTextView = view.findViewById(R.id.connect_to_car_text);
     model
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/CompanionQrCodeLandingFragment.java b/companiondevice/src/com/google/android/companiondevicesupport/CompanionQrCodeLandingFragment.java
index 200c8e9..b0cf579 100644
--- a/companiondevice/src/com/google/android/companiondevicesupport/CompanionQrCodeLandingFragment.java
+++ b/companiondevice/src/com/google/android/companiondevicesupport/CompanionQrCodeLandingFragment.java
@@ -99,7 +99,8 @@
                 new AssociatedDeviceViewModelFactory(
                     requireActivity().getApplication(),
                     transportProtocols.contains(TransportProtocols.PROTOCOL_SPP),
-                    getResources().getString(R.string.ble_device_name_prefix)))
+                    getResources().getString(R.string.ble_device_name_prefix),
+                    getResources().getBoolean(R.bool.enable_passenger)))
             .get(AssociatedDeviceViewModel.class);
 
     TextView connectToCarTextView = view.findViewById(R.id.connect_to_car_instruction);
diff --git a/libs/connecteddevice/build.gradle b/libs/connecteddevice/build.gradle
index fb665bd..6105fad 100644
--- a/libs/connecteddevice/build.gradle
+++ b/libs/connecteddevice/build.gradle
@@ -1,6 +1,7 @@
 apply plugin: 'com.android.library'
 apply plugin: 'com.google.protobuf'
 apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-parcelize'
 
 buildscript {
     repositories {
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/ConnectedDeviceManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/ConnectedDeviceManager.java
deleted file mode 100644
index 6cf5668..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/ConnectedDeviceManager.java
+++ /dev/null
@@ -1,922 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static com.google.android.connecteddevice.util.SafeLog.logw;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.connecteddevice.connection.AssociationCallback;
-import com.google.android.connecteddevice.connection.CarBluetoothManager;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.ConnectedDevice;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.model.Errors;
-import com.google.android.connecteddevice.model.StartAssociationResponse;
-import com.google.android.connecteddevice.oob.OobChannel;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage.AssociatedDeviceCallback;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage.OnAssociatedDevicesRetrievedListener;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.android.connecteddevice.util.EventLog;
-import com.google.android.connecteddevice.util.SafeConsumer;
-import com.google.android.connecteddevice.util.ThreadSafeCallbacks;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/** Manager of devices connected to the car. */
-@SuppressWarnings("AndroidConcurrentHashMap") // Targeting sdk version 29
-public class ConnectedDeviceManager {
-
-  private static final String TAG = "ConnectedDeviceManager";
-
-  // Device name length is limited by available bytes in BLE advertisement data packet.
-  //
-  // BLE advertisement limits data packet length to 31
-  // Currently we send:
-  // - 18 bytes for 16 chars UUID: 16 bytes + 2 bytes for header;
-  // - 3 bytes for advertisement being connectable;
-  // which leaves 10 bytes.
-  // Subtracting 2 bytes used by header, we have 8 bytes for device name.
-  // The device name length defined here should be smaller than the limit 8.
-  private static final int DEVICE_NAME_LENGTH = 2;
-
-  private final ConnectedDeviceStorage storage;
-
-  private final CarBluetoothManager carBluetoothManager;
-
-  /**
-   * The {@link Executor} that will handle tasks linked with connecting to a remote device; this
-   * includes advertising for association and reconnection with an associated device.
-   */
-  private final Executor connectionExecutor;
-
-  private final Executor storageExecutor;
-
-  private final ThreadSafeCallbacks<DeviceAssociationCallback> deviceAssociationCallbacks =
-      new ThreadSafeCallbacks<>();
-
-  private final ThreadSafeCallbacks<ConnectionCallback> activeUserConnectionCallbacks =
-      new ThreadSafeCallbacks<>();
-
-  private final ThreadSafeCallbacks<ConnectionCallback> allUserConnectionCallbacks =
-      new ThreadSafeCallbacks<>();
-
-  // deviceId -> (recipientId -> callbacks)
-  private final Map<String, Map<UUID, ThreadSafeCallbacks<DeviceCallback>>> deviceCallbacks =
-      new ConcurrentHashMap<>();
-
-  // deviceId -> device
-  private final Map<String, ConnectedDevice> connectedDevices = new ConcurrentHashMap<>();
-
-  // recipientId -> (deviceId -> message bytes)
-  private final Map<UUID, Map<String, List<DeviceMessage>>> recipientMissedMessages =
-      new ConcurrentHashMap<>();
-
-  // Recipient ids that received multiple callback registrations indicate that the recipient id
-  // has been compromised. Another party now has access the messages intended for that recipient.
-  // As a safeguard, that recipient id will be added to this list and blocked from further
-  // callback notifications.
-  private final Set<UUID> blockedRecipients = new CopyOnWriteArraySet<>();
-
-  private final AtomicBoolean isConnectingToUserDevice = new AtomicBoolean(false);
-
-  private final AtomicBoolean hasStarted = new AtomicBoolean(false);
-
-  private byte[] nameForAssociation;
-
-  private AssociationCallback associationCallback;
-
-  private MessageDeliveryDelegate messageDeliveryDelegate;
-
-  private OobChannel oobChannel;
-
-  public ConnectedDeviceManager(
-      @NonNull CarBluetoothManager carBluetoothManager, @NonNull ConnectedDeviceStorage storage) {
-    this(
-        carBluetoothManager,
-        storage,
-        Executors.newCachedThreadPool(),
-        Executors.newSingleThreadExecutor(),
-        Executors.newSingleThreadExecutor());
-  }
-
-  @VisibleForTesting
-  ConnectedDeviceManager(
-      @NonNull CarBluetoothManager carBluetoothManager,
-      @NonNull ConnectedDeviceStorage storage,
-      @NonNull Executor connectionExecutor,
-      @NonNull Executor callbackExecutor,
-      @NonNull Executor storageExecutor) {
-    this.storage = storage;
-    this.carBluetoothManager = carBluetoothManager;
-    this.connectionExecutor = connectionExecutor;
-    this.carBluetoothManager.registerCallback(generateCarManagerCallback(), callbackExecutor);
-    this.storage.registerAssociatedDeviceCallback(associatedDeviceCallback);
-    this.storageExecutor = storageExecutor;
-  }
-
-  /**
-   * Start internal processes and begin discovering devices. Must be called before any connections
-   * can be made using {@link #connectToActiveUserDevice()}.
-   */
-  public void start() {
-    if (hasStarted.getAndSet(true)) {
-      reset();
-    } else {
-      logd(TAG, "Starting ConnectedDeviceManager.");
-      EventLog.onConnectedDeviceManagerStarted();
-    }
-    carBluetoothManager.start();
-    connectToActiveUserDevice();
-  }
-
-  /** Reset internal processes and disconnect any active connections. */
-  public void reset() {
-    logd(TAG, "Resetting ConnectedDeviceManager.");
-    for (ConnectedDevice device : connectedDevices.values()) {
-      removeConnectedDevice(device.getDeviceId());
-    }
-    carBluetoothManager.stop();
-    isConnectingToUserDevice.set(false);
-    if (oobChannel != null) {
-      oobChannel.interrupt();
-      oobChannel = null;
-    }
-    associationCallback = null;
-  }
-
-  /** Returns {@link List<ConnectedDevice>} of devices currently connected. */
-  @NonNull
-  public List<ConnectedDevice> getActiveUserConnectedDevices() {
-    List<ConnectedDevice> activeUserConnectedDevices = new ArrayList<>();
-    for (ConnectedDevice device : connectedDevices.values()) {
-      if (device.isAssociatedWithDriver()) {
-        activeUserConnectedDevices.add(device);
-      }
-    }
-    logd(TAG, "Returned " + activeUserConnectedDevices.size() + " active user devices.");
-    return activeUserConnectedDevices;
-  }
-
-  /**
-   * Register a callback for triggered associated device related events.
-   *
-   * @param callback {@link DeviceAssociationCallback} to register.
-   * @param executor {@link Executor} to execute triggers on.
-   */
-  public void registerDeviceAssociationCallback(
-      @NonNull DeviceAssociationCallback callback, @NonNull Executor executor) {
-    deviceAssociationCallbacks.add(callback, executor);
-  }
-
-  /**
-   * Unregister a device association callback.
-   *
-   * @param callback {@link DeviceAssociationCallback} to unregister.
-   */
-  public void unregisterDeviceAssociationCallback(@NonNull DeviceAssociationCallback callback) {
-    deviceAssociationCallbacks.remove(callback);
-  }
-
-  /**
-   * Register a callback for manager triggered connection events for only the currently active
-   * user's devices.
-   *
-   * @param callback {@link ConnectionCallback} to register.
-   * @param executor {@link Executor} to execute triggers on.
-   */
-  public void registerActiveUserConnectionCallback(
-      @NonNull ConnectionCallback callback, @NonNull Executor executor) {
-    activeUserConnectionCallbacks.add(callback, executor);
-    logd(TAG, "ActiveUserConnectionCallback registered.");
-  }
-
-  /**
-   * Unregister a connection callback from manager.
-   *
-   * @param callback {@link ConnectionCallback} to unregister.
-   */
-  public void unregisterConnectionCallback(ConnectionCallback callback) {
-    activeUserConnectionCallbacks.remove(callback);
-    allUserConnectionCallbacks.remove(callback);
-  }
-
-  /** Connect to a device for the active user if available. */
-  @VisibleForTesting
-  void connectToActiveUserDevice() {
-    connectionExecutor.execute(
-        () -> {
-          logd(TAG, "Received request to connect to active user's device.");
-          connectToActiveUserDeviceInternal();
-        });
-  }
-
-  private void connectToActiveUserDeviceInternal() {
-    boolean isLockAcquired = isConnectingToUserDevice.compareAndSet(false, true);
-    if (!isLockAcquired) {
-      logd(
-          TAG,
-          "A request has already been made to connect to this user's device. "
-              + "Ignoring redundant request.");
-      return;
-    }
-    List<AssociatedDevice> userDevices = storage.getDriverAssociatedDevices();
-    if (userDevices.isEmpty()) {
-      logw(TAG, "No devices associated with active user. Ignoring.");
-      isConnectingToUserDevice.set(false);
-      return;
-    }
-
-    // Only currently support one device per user for fast association, so take the
-    // first one.
-    AssociatedDevice userDevice = userDevices.get(0);
-    if (!userDevice.isConnectionEnabled()) {
-      logd(TAG, "Connection is disabled on device " + userDevice + ".");
-      isConnectingToUserDevice.set(false);
-      return;
-    }
-    if (connectedDevices.containsKey(userDevice.getDeviceId())) {
-      logd(TAG, "Device has already been connected. No need to attempt connection " + "again.");
-      isConnectingToUserDevice.set(false);
-      return;
-    }
-    EventLog.onStartDeviceSearchStarted();
-    carBluetoothManager.connectToDevice(UUID.fromString(userDevice.getDeviceId()));
-  }
-
-  /**
-   * Start the association with a new device.
-   *
-   * @param callback Callback for association events.
-   */
-  public void startAssociation(@NonNull AssociationCallback callback) {
-    associationCallback = callback;
-    connectionExecutor.execute(
-        () -> {
-          logd(TAG, "Received request to start association.");
-          carBluetoothManager.startAssociation(
-              getNameForAssociation(), internalAssociationCallback);
-        });
-  }
-
-  /** Stop the association with any device. */
-  public void stopAssociation(@NonNull AssociationCallback callback) {
-    if (associationCallback != callback) {
-      logd(TAG, "Stop association called with unrecognized callback. Ignoring.");
-      return;
-    }
-    logd(TAG, "Stopping association.");
-    associationCallback = null;
-    carBluetoothManager.stopAssociation();
-    if (oobChannel != null) {
-      oobChannel.interrupt();
-    }
-    oobChannel = null;
-  }
-
-  /**
-   * Retrieves devices associated with the active user and notifies the given {@link
-   * OnAssociatedDevicesRetrievedListener}.
-   */
-  public void retrieveActiveUserAssociatedDevices(OnAssociatedDevicesRetrievedListener listener) {
-    storageExecutor.execute(
-        () -> listener.onAssociatedDevicesRetrieved(storage.getDriverAssociatedDevices()));
-  }
-
-  /** Notify that the user has accepted a pairing code or any out-of-band confirmation. */
-  public void notifyOutOfBandAccepted() {
-    carBluetoothManager.notifyOutOfBandAccepted();
-  }
-
-  /**
-   * Remove the associated device with the given device identifier for the current user.
-   *
-   * @param deviceId Device identifier.
-   */
-  public void removeActiveUserAssociatedDevice(@NonNull String deviceId) {
-    storageExecutor.execute(() -> storage.removeAssociatedDevice(deviceId));
-    disconnectDevice(deviceId);
-  }
-
-  /**
-   * Enable connection on an associated device.
-   *
-   * @param deviceId Device identifier.
-   */
-  public void enableAssociatedDeviceConnection(@NonNull String deviceId) {
-    logd(TAG, "enableAssociatedDeviceConnection() called on " + deviceId);
-    storageExecutor.execute(
-        () -> {
-          storage.updateAssociatedDeviceConnectionEnabled(
-              deviceId, /* isConnectionEnabled= */ true);
-          connectToActiveUserDevice();
-        });
-  }
-
-  /**
-   * Disable connection on an associated device.
-   *
-   * @param deviceId Device identifier.
-   */
-  public void disableAssociatedDeviceConnection(@NonNull String deviceId) {
-    logd(TAG, "disableAssociatedDeviceConnection() called on " + deviceId);
-    storageExecutor.execute(
-        () -> {
-          storage.updateAssociatedDeviceConnectionEnabled(
-              deviceId, /* isConnectionEnabled= */ false);
-          disconnectDevice(deviceId);
-          isConnectingToUserDevice.set(false);
-        });
-  }
-
-  private void disconnectDevice(String deviceId) {
-    ConnectedDevice device = connectedDevices.get(deviceId);
-    if (device != null) {
-      carBluetoothManager.disconnectDevice(deviceId);
-      removeConnectedDevice(deviceId);
-    }
-  }
-
-  /**
-   * Register a callback for a specific device and recipient.
-   *
-   * @param device {@link ConnectedDevice} to register triggers on.
-   * @param recipientId {@link UUID} to register as recipient of.
-   * @param callback {@link DeviceCallback} to register.
-   * @param executor {@link Executor} on which to execute callback.
-   */
-  public void registerDeviceCallback(
-      @NonNull ConnectedDevice device,
-      @NonNull UUID recipientId,
-      @NonNull DeviceCallback callback,
-      @NonNull Executor executor) {
-    if (isRecipientDenyListed(recipientId)) {
-      notifyOfBlocking(device, recipientId, callback, executor);
-      return;
-    }
-    logd(
-        TAG,
-        "New callback registered on device "
-            + device.getDeviceId()
-            + " for recipient "
-            + recipientId);
-    String deviceId = device.getDeviceId();
-    Map<UUID, ThreadSafeCallbacks<DeviceCallback>> recipientCallbacks =
-        deviceCallbacks.computeIfAbsent(deviceId, key -> new ConcurrentHashMap<>());
-
-    // Device already has a callback registered with this recipient UUID. For the
-    // protection of the user, this UUID is now deny listed from future subscriptions
-    // and the original subscription is notified and removed.
-    if (recipientCallbacks.containsKey(recipientId)) {
-      denyListRecipient(deviceId, recipientId);
-      notifyOfBlocking(device, recipientId, callback, executor);
-      return;
-    }
-
-    ThreadSafeCallbacks<DeviceCallback> newCallbacks = new ThreadSafeCallbacks<>();
-    newCallbacks.add(callback, executor);
-    recipientCallbacks.put(recipientId, newCallbacks);
-
-    List<DeviceMessage> messages = popMissedMessages(recipientId, device.getDeviceId());
-    if (messages != null) {
-      for (DeviceMessage message : messages) {
-        newCallbacks.invoke(deviceCallback -> deviceCallback.onMessageReceived(device, message));
-      }
-    }
-  }
-
-  /**
-   * Set the delegate for message delivery operations.
-   *
-   * @param delegate The {@link MessageDeliveryDelegate} to set. {@code null} to unset.
-   */
-  public void setMessageDeliveryDelegate(@Nullable MessageDeliveryDelegate delegate) {
-    messageDeliveryDelegate = delegate;
-  }
-
-  private static void notifyOfBlocking(
-      @NonNull ConnectedDevice device,
-      @NonNull UUID recipientId,
-      @NonNull DeviceCallback callback,
-      @NonNull Executor executor) {
-    loge(
-        TAG,
-        "Multiple callbacks registered for recipient "
-            + recipientId
-            + "! Your recipient id is no longer secure and has been blocked from future use.");
-    executor.execute(
-        () -> callback.onDeviceError(device, Errors.DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED));
-  }
-
-  private void saveMissedMessage(@NonNull String deviceId, @NonNull DeviceMessage message) {
-    // Store last message in case recipient registers callbacks in the future.
-    UUID recipientId = message.getRecipient();
-    logd(
-        TAG,
-        "No recipient registered for device "
-            + deviceId
-            + " and recipient "
-            + recipientId
-            + " combination. Saving message.");
-    recipientMissedMessages
-        .computeIfAbsent(recipientId, __ -> new HashMap<>())
-        .computeIfAbsent(deviceId, __ -> new ArrayList<>())
-        .add(message);
-  }
-
-  /**
-   * Remove all messages sent for this device prior to a {@link DeviceCallback} being registered.
-   *
-   * @param recipientId Recipient's id
-   * @param deviceId Device id
-   * @return The missed {@code DeviceMessage}s, or {@code null} if no messages were missed.
-   */
-  @Nullable
-  private List<DeviceMessage> popMissedMessages(
-      @NonNull UUID recipientId,
-      @NonNull String deviceId) {
-    Map<String, List<DeviceMessage>> missedMessages = recipientMissedMessages.get(recipientId);
-    if (missedMessages == null) {
-      return null;
-    }
-
-    return missedMessages.remove(deviceId);
-  }
-
-  /**
-   * Unregister callback from device events.
-   *
-   * @param device {@link ConnectedDevice} callback was registered on.
-   * @param recipientId {@link UUID} callback was registered under.
-   * @param callback {@link DeviceCallback} to unregister.
-   */
-  public void unregisterDeviceCallback(
-      @NonNull ConnectedDevice device,
-      @NonNull UUID recipientId,
-      @NonNull DeviceCallback callback) {
-    logd(
-        TAG,
-        "Device callback unregistered on device "
-            + device.getDeviceId()
-            + " for "
-            + "recipient "
-            + recipientId
-            + ".");
-
-    Map<UUID, ThreadSafeCallbacks<DeviceCallback>> recipientCallbacks =
-        deviceCallbacks.get(device.getDeviceId());
-    if (recipientCallbacks == null) {
-      return;
-    }
-    ThreadSafeCallbacks<DeviceCallback> callbacks = recipientCallbacks.get(recipientId);
-    if (callbacks == null) {
-      return;
-    }
-
-    callbacks.remove(callback);
-    if (callbacks.size() == 0) {
-      recipientCallbacks.remove(recipientId);
-    }
-  }
-
-  /**
-   * Securely send message to a device.
-   *
-   * @param device {@link ConnectedDevice} to send the message to.
-   * @param deviceMessage Message to send to device.
-   * @throws IllegalStateException Secure channel has not been established.
-   */
-  public void sendMessage(@NonNull ConnectedDevice device, @NonNull DeviceMessage deviceMessage) {
-    String deviceId = device.getDeviceId();
-    boolean isEncrypted = deviceMessage.isMessageEncrypted();
-    logd(
-        TAG,
-        "Sending new message to device "
-            + deviceId
-            + " for "
-            + deviceMessage.getRecipient()
-            + " containing "
-            + deviceMessage.getMessage().length
-            + ". Message will be sent securely: "
-            + isEncrypted
-            + ".");
-
-    ConnectedDevice connectedDevice = connectedDevices.get(deviceId);
-    if (connectedDevice == null) {
-      loge(TAG, "Attempted to send message to unknown device " + deviceId + ". Ignoring.");
-      return;
-    }
-
-    if (isEncrypted && !connectedDevice.hasSecureChannel()) {
-      throw new IllegalStateException(
-          "Cannot send a message securely to device that has not "
-              + "established a secure channel.");
-    }
-
-    carBluetoothManager.sendMessage(deviceId, deviceMessage);
-  }
-
-  private boolean isRecipientDenyListed(UUID recipientId) {
-    return blockedRecipients.contains(recipientId);
-  }
-
-  private void denyListRecipient(@NonNull String deviceId, @NonNull UUID recipientId) {
-    Map<UUID, ThreadSafeCallbacks<DeviceCallback>> recipientCallbacks =
-        deviceCallbacks.get(deviceId);
-    if (recipientCallbacks == null) {
-      // Should never happen, but null-safety check.
-      return;
-    }
-
-    ThreadSafeCallbacks<DeviceCallback> existingCallback = recipientCallbacks.get(recipientId);
-    if (existingCallback == null) {
-      // Should never happen, but null-safety check.
-      return;
-    }
-
-    ConnectedDevice connectedDevice = connectedDevices.get(deviceId);
-    if (connectedDevice != null) {
-      recipientCallbacks
-          .get(recipientId)
-          .invoke(
-              callback ->
-                  callback.onDeviceError(
-                      connectedDevice, Errors.DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED));
-    }
-
-    recipientCallbacks.remove(recipientId);
-    blockedRecipients.add(recipientId);
-  }
-
-  @VisibleForTesting
-  void addConnectedDevice(@NonNull String deviceId) {
-    if (connectedDevices.containsKey(deviceId)) {
-      // Device already connected. No-op until secure channel established.
-      return;
-    }
-    logd(TAG, "New device with id " + deviceId + " connected.");
-    ConnectedDevice connectedDevice =
-        new ConnectedDevice(
-            deviceId,
-            /* deviceName= */ null,
-            storage.getDriverAssociatedDeviceIds().contains(deviceId),
-            /* hasSecureChannel= */ false);
-
-    connectedDevices.put(deviceId, connectedDevice);
-    invokeConnectionCallbacks(
-        connectedDevice.isAssociatedWithDriver(),
-        callback -> callback.onDeviceConnected(connectedDevice));
-  }
-
-  @VisibleForTesting
-  void removeConnectedDevice(@NonNull String deviceId) {
-    logd(TAG, "Device " + deviceId + " disconnected.");
-    ConnectedDevice connectedDevice = connectedDevices.get(deviceId);
-    isConnectingToUserDevice.set(false);
-    boolean isAssociated = false;
-    if (connectedDevice != null) {
-      connectedDevices.remove(deviceId);
-      isAssociated = connectedDevice.isAssociatedWithDriver();
-      invokeConnectionCallbacks(
-          isAssociated, callback -> callback.onDeviceDisconnected(connectedDevice));
-    }
-
-    if (isAssociated || connectedDevices.isEmpty()) {
-      // Try to regain connection to active user's device.
-      connectToActiveUserDevice();
-    }
-  }
-
-  @VisibleForTesting
-  void onSecureChannelEstablished(@NonNull String deviceId) {
-    if (connectedDevices.get(deviceId) == null) {
-      loge(TAG, "Secure channel established on unknown device " + deviceId + ".");
-      return;
-    }
-    ConnectedDevice connectedDevice = connectedDevices.get(deviceId);
-    ConnectedDevice updatedConnectedDevice =
-        new ConnectedDevice(
-            connectedDevice.getDeviceId(),
-            getConnectedDeviceName(deviceId),
-            connectedDevice.isAssociatedWithDriver(),
-            /* hasSecureChannel= */ true);
-
-    boolean notifyCallbacks = connectedDevices.get(deviceId) != null;
-
-    // TODO (b/143088482) Implement interrupt
-    // Ignore if central already holds the active device connection and interrupt the
-    // connection.
-
-    connectedDevices.put(deviceId, updatedConnectedDevice);
-    logd(
-        TAG,
-        "Secure channel established to "
-            + deviceId
-            + " . Notifying callbacks: "
-            + notifyCallbacks
-            + ".");
-    if (notifyCallbacks) {
-      notifyAllDeviceCallbacks(
-          deviceId, callback -> callback.onSecureChannelEstablished(updatedConnectedDevice));
-    }
-  }
-
-  @VisibleForTesting
-  void onMessageReceived(@NonNull String deviceId, @NonNull DeviceMessage message) {
-    logd(
-        TAG,
-        "New message received from device "
-            + deviceId
-            + " intended for "
-            + message.getRecipient()
-            + " containing "
-            + message.getMessage().length
-            + " bytes.");
-
-    ConnectedDevice connectedDevice = connectedDevices.get(deviceId);
-    if (connectedDevice == null) {
-      logw(
-          TAG,
-          "Received message from unknown device "
-              + deviceId
-              + "or to unknown "
-              + "recipient "
-              + message.getRecipient()
-              + ".");
-      return;
-    }
-
-    if (messageDeliveryDelegate != null
-        && !messageDeliveryDelegate.shouldDeliverMessageForDevice(connectedDevice)) {
-      logw(
-          TAG,
-          "The message delegate has rejected this message. It will not be "
-              + "delivered to the intended recipient.");
-      return;
-    }
-
-    UUID recipientId = message.getRecipient();
-    Map<UUID, ThreadSafeCallbacks<DeviceCallback>> deviceCallbacks =
-        this.deviceCallbacks.get(deviceId);
-    if (deviceCallbacks == null) {
-      saveMissedMessage(deviceId, message);
-      return;
-    }
-    ThreadSafeCallbacks<DeviceCallback> recipientCallbacks = deviceCallbacks.get(recipientId);
-    if (recipientCallbacks == null) {
-      saveMissedMessage(deviceId, message);
-      return;
-    }
-
-    recipientCallbacks.invoke(
-        callback -> callback.onMessageReceived(connectedDevice, message));
-  }
-
-  @VisibleForTesting
-  void deviceErrorOccurred(@NonNull String deviceId) {
-    ConnectedDevice connectedDevice = connectedDevices.get(deviceId);
-    if (connectedDevice == null) {
-      logw(TAG, "Failed to establish secure channel on unknown device " + deviceId + ".");
-      return;
-    }
-
-    notifyAllDeviceCallbacks(
-        deviceId,
-        callback ->
-            callback.onDeviceError(connectedDevice, Errors.DEVICE_ERROR_INVALID_SECURITY_KEY));
-  }
-
-  @VisibleForTesting
-  void onAssociationCompleted(@NonNull String deviceId) {
-    ConnectedDevice connectedDevice = connectedDevices.get(deviceId);
-    if (connectedDevice == null) {
-      return;
-    }
-
-    // The previous device is now obsolete and should be replaced with a new one properly
-    // reflecting the state of belonging to the active user and notify features.
-    if (connectedDevice.isAssociatedWithDriver()) {
-      // Device was already marked as belonging to active user. No need to reissue callbacks.
-      return;
-    }
-    removeConnectedDevice(deviceId);
-    addConnectedDevice(deviceId);
-  }
-
-  @Nullable
-  private String getConnectedDeviceName(@NonNull String deviceId) {
-    ConnectedDevice device = connectedDevices.get(deviceId);
-    if (device == null) {
-      return null;
-    }
-    String deviceName = device.getDeviceName();
-    if (deviceName != null) {
-      return deviceName;
-    }
-    AssociatedDevice associatedDevice = storage.getAssociatedDevice(deviceId);
-    if (associatedDevice == null) {
-      return null;
-    }
-    return associatedDevice.getDeviceName();
-  }
-
-  private void invokeConnectionCallbacks(
-      boolean belongsToActiveUser, @NonNull SafeConsumer<ConnectionCallback> notification) {
-    logd(
-        TAG,
-        "Notifying connection callbacks for device belonging to active user "
-            + belongsToActiveUser
-            + ".");
-    if (belongsToActiveUser) {
-      activeUserConnectionCallbacks.invoke(notification);
-    }
-    allUserConnectionCallbacks.invoke(notification);
-  }
-
-  private void notifyAllDeviceCallbacks(
-      @NonNull String deviceId, @NonNull SafeConsumer<DeviceCallback> notification) {
-    logd(TAG, "Notifying all device callbacks for device " + deviceId + ".");
-    Map<UUID, ThreadSafeCallbacks<DeviceCallback>> deviceCallbacks =
-        this.deviceCallbacks.get(deviceId);
-    if (deviceCallbacks == null) {
-      return;
-    }
-
-    for (ThreadSafeCallbacks<DeviceCallback> callbacks : deviceCallbacks.values()) {
-      callbacks.invoke(notification);
-    }
-  }
-
-  /**
-   * Returns the name that should be used for the device during association.
-   *
-   * <p>The returned name will be a randomized byte array.
-   */
-  @NonNull
-  private byte[] getNameForAssociation() {
-    if (nameForAssociation == null) {
-      nameForAssociation = ByteUtils.randomBytes(DEVICE_NAME_LENGTH);
-    }
-    return nameForAssociation;
-  }
-
-  @NonNull
-  private CarBluetoothManager.Callback generateCarManagerCallback() {
-    return new CarBluetoothManager.Callback() {
-      @Override
-      public void onDeviceConnected(String deviceId) {
-        addConnectedDevice(deviceId);
-      }
-
-      @Override
-      public void onDeviceDisconnected(String deviceId) {
-        removeConnectedDevice(deviceId);
-      }
-
-      @Override
-      public void onSecureChannelEstablished(String deviceId) {
-        EventLog.onSecureChannelEstablished();
-        ConnectedDeviceManager.this.onSecureChannelEstablished(deviceId);
-      }
-
-      @Override
-      public void onMessageReceived(String deviceId, DeviceMessage message) {
-        ConnectedDeviceManager.this.onMessageReceived(deviceId, message);
-      }
-
-      @Override
-      public void onSecureChannelError(String deviceId) {
-        deviceErrorOccurred(deviceId);
-      }
-    };
-  }
-
-  private final AssociationCallback internalAssociationCallback =
-      new AssociationCallback() {
-        @Override
-        public void onAssociationStartSuccess(StartAssociationResponse response) {
-          if (associationCallback != null) {
-            associationCallback.onAssociationStartSuccess(response);
-          }
-        }
-
-        @Override
-        public void onAssociationStartFailure() {
-          if (associationCallback != null) {
-            associationCallback.onAssociationStartFailure();
-          }
-        }
-
-        @Override
-        public void onAssociationError(int error) {
-          if (associationCallback != null) {
-            associationCallback.onAssociationError(error);
-          }
-        }
-
-        @Override
-        public void onVerificationCodeAvailable(String code) {
-          if (associationCallback != null) {
-            associationCallback.onVerificationCodeAvailable(code);
-          }
-        }
-
-        @Override
-        public void onAssociationCompleted(String deviceId) {
-          if (associationCallback != null) {
-            associationCallback.onAssociationCompleted(deviceId);
-          }
-          ConnectedDeviceManager.this.onAssociationCompleted(deviceId);
-        }
-      };
-
-  private final AssociatedDeviceCallback associatedDeviceCallback =
-      new AssociatedDeviceCallback() {
-        @Override
-        public void onAssociatedDeviceAdded(AssociatedDevice device) {
-          deviceAssociationCallbacks.invoke(callback -> callback.onAssociatedDeviceAdded(device));
-        }
-
-        @Override
-        public void onAssociatedDeviceRemoved(AssociatedDevice device) {
-          deviceAssociationCallbacks.invoke(callback -> callback.onAssociatedDeviceRemoved(device));
-          logd(TAG, "Successfully removed associated device " + device + ".");
-        }
-
-        @Override
-        public void onAssociatedDeviceUpdated(AssociatedDevice device) {
-          deviceAssociationCallbacks.invoke(callback -> callback.onAssociatedDeviceUpdated(device));
-        }
-      };
-
-  /** Callback for triggered connection events from {@link ConnectedDeviceManager}. */
-  public interface ConnectionCallback {
-    /** Triggered when a new device has connected. */
-    void onDeviceConnected(@NonNull ConnectedDevice device);
-
-    /** Triggered when a device has disconnected. */
-    void onDeviceDisconnected(@NonNull ConnectedDevice device);
-  }
-
-  /** Triggered device events for a connected device from {@link ConnectedDeviceManager}. */
-  public interface DeviceCallback {
-    /**
-     * Triggered when secure channel has been established on a device. Encrypted messaging now
-     * available.
-     */
-    void onSecureChannelEstablished(@NonNull ConnectedDevice device);
-
-    /** Triggered when a new message is received from a device. */
-    void onMessageReceived(@NonNull ConnectedDevice device, @NonNull DeviceMessage message);
-
-    /** Triggered when an error has occurred for a device. */
-    void onDeviceError(@NonNull ConnectedDevice device, @Errors.DeviceError int error);
-  }
-
-  /** Callback for association device related events. */
-  public interface DeviceAssociationCallback {
-
-    /** Triggered when an associated device has been added. */
-    void onAssociatedDeviceAdded(@NonNull AssociatedDevice device);
-
-    /** Triggered when an associated device has been removed. */
-    void onAssociatedDeviceRemoved(@NonNull AssociatedDevice device);
-
-    /** Triggered when the name of an associated device has been updated. */
-    void onAssociatedDeviceUpdated(@NonNull AssociatedDevice device);
-  }
-
-  /** Delegate for message delivery operations. */
-  public interface MessageDeliveryDelegate {
-
-    /** Indicate whether a message should be delivered for the specified device. */
-    boolean shouldDeliverMessageForDevice(@NonNull ConnectedDevice device);
-  }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/CompanionConnector.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/api/CompanionConnector.kt
index c38218b..fec1907 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/api/CompanionConnector.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/CompanionConnector.kt
@@ -31,11 +31,8 @@
 import com.google.android.companionprotos.SystemQuery
 import com.google.android.companionprotos.SystemQueryType
 import com.google.android.connecteddevice.api.Connector.AppNameCallback
-import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_ASSOCIATION
 import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_FEATURE_COORDINATOR
 import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_FEATURE_COORDINATOR_FG
-import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_REMOTE_FEATURE
-import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_REMOTE_FEATURE_FG
 import com.google.android.connecteddevice.api.Connector.Companion.SYSTEM_FEATURE_ID
 import com.google.android.connecteddevice.api.Connector.Companion.USER_TYPE_ALL
 import com.google.android.connecteddevice.api.Connector.Companion.USER_TYPE_DRIVER
@@ -74,12 +71,12 @@
   private val isForegroundProcess: Boolean = false,
   private val userType: @UserType Int = USER_TYPE_DRIVER
 ) : Connector {
-  private val isLegacyOnly = AtomicBoolean(false)
-
   private val retryHandler = Handler(Looper.getMainLooper())
 
   private val loggerId = Logger.getLogger().loggerId
 
+  private val isPlatformInitialized = AtomicBoolean(false)
+
   private val featureCoordinatorAction =
     if (isForegroundProcess) {
       ACTION_BIND_FEATURE_COORDINATOR_FG
@@ -87,13 +84,6 @@
       ACTION_BIND_FEATURE_COORDINATOR
     }
 
-  private val connectedDeviceManagerAction =
-    if (isForegroundProcess) {
-      ACTION_BIND_REMOTE_FEATURE_FG
-    } else {
-      ACTION_BIND_REMOTE_FEATURE
-    }
-
   private val featureCoordinatorConnection =
     object : ServiceConnection {
       override fun onServiceConnected(name: ComponentName, service: IBinder) {
@@ -101,6 +91,7 @@
         featureCoordinator =
           IFeatureCoordinator.Stub.asInterface(service)
             ?: throw IllegalStateException("Cannot create wrapper of a null feature coordinator.")
+        logd(TAG, "Feature coordinator is alive: ${featureCoordinator?.asBinder()?.isBinderAlive}")
         this@CompanionConnector.onServiceConnected()
       }
 
@@ -114,42 +105,6 @@
       }
     }
 
-  private val connectedDeviceManagerConnection =
-    object : ServiceConnection {
-      override fun onServiceConnected(name: ComponentName, service: IBinder) {
-        logd(TAG, "ConnectedDeviceManager binding has connected successfully.")
-        connectedDeviceManager = IConnectedDeviceManager.Stub.asInterface(service)
-        this@CompanionConnector.onServiceConnected()
-      }
-
-      override fun onServiceDisconnected(name: ComponentName) {
-        this@CompanionConnector.onServiceDisconnected()
-      }
-
-      override fun onNullBinding(name: ComponentName) {
-        loge(TAG, "Received a null binding for ConnectedDeviceManager.")
-        this@CompanionConnector.onNullBinding()
-      }
-    }
-
-  private val associatedDeviceManagerConnection =
-    object : ServiceConnection {
-      override fun onServiceConnected(name: ComponentName, service: IBinder) {
-        logd(TAG, "AssociatedDeviceManager binding has connected successfully.")
-        associatedDeviceManager = IAssociatedDeviceManager.Stub.asInterface(service)
-        this@CompanionConnector.onServiceConnected()
-      }
-
-      override fun onServiceDisconnected(name: ComponentName) {
-        this@CompanionConnector.onServiceDisconnected()
-      }
-
-      override fun onNullBinding(name: ComponentName) {
-        loge(TAG, "Received a null binding for AssociatedDeviceManager.")
-        this@CompanionConnector.onNullBinding()
-      }
-    }
-
   private val connectionCallback =
     object : IConnectionCallback.Stub() {
       override fun onDeviceConnected(device: ConnectedDevice) {
@@ -164,7 +119,6 @@
         // Only call method once. If featureCoordinator is not null, connectedDeviceManager is a
         // wrapper and will result in a double call on featureCoordinator.
         aliveFeatureCoordinator?.registerDeviceCallback(device, featureId, deviceCallback)
-          ?: aliveConnectedDeviceManager?.registerDeviceCallback(device, featureId, deviceCallback)
       }
 
       override fun onDeviceDisconnected(device: ConnectedDevice) {
@@ -176,11 +130,6 @@
         // Only call method once. If featureCoordinator is not null, connectedDeviceManager is a
         // wrapper and will result in a double call on featureCoordinator.
         aliveFeatureCoordinator?.unregisterDeviceCallback(device, featureId, deviceCallback)
-          ?: aliveConnectedDeviceManager?.unregisterDeviceCallback(
-            device,
-            featureId,
-            deviceCallback
-          )
       }
     }
 
@@ -225,7 +174,6 @@
         val loggerBytes = Logger.getLogger().toByteArray()
         try {
           featureCoordinator?.processLogRecords(loggerId, loggerBytes)
-            ?: connectedDeviceManager?.processLogRecords(loggerId, loggerBytes)
         } catch (e: RemoteException) {
           loge(TAG, "Failed to send log records for logger $loggerId.", e)
         }
@@ -242,30 +190,7 @@
 
   private var bindAttempts = 0
 
-  /**
-   * When bound to a [featureCoordinator], this connector will wrap [connectedDeviceManager] and
-   * [associatedDeviceManager] around the coordinator. Otherwise, a legacy binding will be made
-   * for both [connectedDeviceManager] and [associatedDeviceManager] separately.
-   *
-   * Note: legacy support is a temporary state and will be removed once all clients have
-   * successfully moved to the [featureCoordinator] binding version.
-   */
-  @VisibleForTesting
-  internal var featureCoordinator: IFeatureCoordinator? = null
-    set(value) {
-      field = value
-      if (value == null) {
-        connectedDeviceManager = null
-        associatedDeviceManager = null
-        return
-      }
-      connectedDeviceManager = createConnectedDeviceManagerWrapper(value)
-      associatedDeviceManager = createAssociatedDeviceManagerWrapper(value)
-    }
-
-  @VisibleForTesting internal var connectedDeviceManager: IConnectedDeviceManager? = null
-
-  @VisibleForTesting internal var associatedDeviceManager: IAssociatedDeviceManager? = null
+  @VisibleForTesting internal var featureCoordinator: IFeatureCoordinator? = null
 
   override var featureId: ParcelUuid? = null
 
@@ -283,51 +208,32 @@
       }
       return when (userType) {
         USER_TYPE_DRIVER -> aliveFeatureCoordinator?.connectedDevicesForDriver
-            ?: aliveConnectedDeviceManager?.activeUserConnectedDevices
         USER_TYPE_PASSENGER -> aliveFeatureCoordinator?.connectedDevicesForPassengers
         USER_TYPE_ALL -> aliveFeatureCoordinator?.allConnectedDevices
-            ?: aliveConnectedDeviceManager?.activeUserConnectedDevices
         else -> null
       }
         ?: emptyList()
     }
 
   override val isConnected: Boolean
-    get() =
-      aliveFeatureCoordinator != null ||
-        (aliveConnectedDeviceManager != null && aliveAssociatedDeviceManager != null)
+    get() = aliveFeatureCoordinator != null
 
   private val isBound: Boolean
-    get() =
-      featureCoordinator != null ||
-        (connectedDeviceManager != null && associatedDeviceManager != null)
+    get() = featureCoordinator != null
 
   private val aliveFeatureCoordinator
     get() = featureCoordinator?.aliveOrNull()
 
-  private val aliveConnectedDeviceManager
-    get() = connectedDeviceManager?.aliveOrNull()
-
-  private val aliveAssociatedDeviceManager
-    get() = associatedDeviceManager?.aliveOrNull()
-
   override fun connect() {
     logd(TAG, "Initiating connection to companion platform.")
     if (isConnected) {
       logd(TAG, "Platform is already connected. Skipping binding.")
       initializePlatform()
-      callback?.onConnected()
       return
     }
 
     logd(TAG, "Platform is not currently connected. Initiating binding.")
-    val intent =
-      if (isLegacyOnly.get()) {
-        resolveIntent(connectedDeviceManagerAction)
-      } else {
-        resolveIntent(featureCoordinatorAction)
-          ?: resolveIntent(connectedDeviceManagerAction).also { isLegacyOnly.set(true) }
-      }
+    val intent = resolveIntent(featureCoordinatorAction)
 
     if (intent == null) {
       loge(TAG, "No services found supporting companion device. Aborting.")
@@ -335,15 +241,7 @@
       return
     }
 
-    val success =
-      if (isLegacyOnly.get()) {
-        val associationIntent =
-          Intent(ACTION_BIND_ASSOCIATION).apply { component = intent.component }
-        context.bindService(intent, connectedDeviceManagerConnection, /* flag= */ 0) &&
-          context.bindService(associationIntent, associatedDeviceManagerConnection, /* flag= */ 0)
-      } else {
-        context.bindService(intent, featureCoordinatorConnection, /* flag= */ 0)
-      }
+    val success = context.bindService(intent, featureCoordinatorConnection, /* flag= */ 0)
     if (success) {
       logd(TAG, "Successfully started binding with ${intent.action}.")
       return
@@ -364,18 +262,13 @@
     val wasConnected = isBound
     logd(TAG, "FeatureCoordinator is null: ${featureCoordinator == null} isBound: $isBound")
     cleanUpFeatureCoordinator()
-    cleanUpConnectedDeviceManager()
-    associatedDeviceManager = null
     retryHandler.removeCallbacksAndMessages(/* token= */ null)
     try {
       context.unbindService(featureCoordinatorConnection)
-      context.unbindService(connectedDeviceManagerConnection)
-      context.unbindService(associatedDeviceManagerConnection)
     } catch (e: IllegalArgumentException) {
       logw(TAG, "Attempted to unbind an already unbound service.")
     }
     bindAttempts = 0
-    isLegacyOnly.set(false)
     if (wasConnected) {
       callback?.onDisconnected()
     }
@@ -404,35 +297,13 @@
     if (featureCoordinator != null) {
       featureCoordinator = null
     }
-  }
-
-  private fun cleanUpConnectedDeviceManager() {
-    logd(TAG, "Cleaning up ConnectedDeviceManager.")
-    aliveConnectedDeviceManager?.let {
-      try {
-        it.unregisterConnectionCallback(connectionCallback)
-        val featureId = featureId
-        if (featureId != null) {
-          for (device in it.activeUserConnectedDevices) {
-            it.unregisterDeviceCallback(device, featureId, deviceCallback)
-          }
-        }
-        it.unregisterDeviceAssociationCallback(deviceAssociationCallback)
-        it.unregisterOnLogRequestedListener(loggerId, logRequestedListener)
-      } catch (e: RemoteException) {
-        loge(TAG, "Error while cleaning up ConnectedDeviceManager.", e)
-      }
-    }
-    connectedDeviceManager = null
+    isPlatformInitialized.set(false)
   }
 
   override fun binderForAction(action: String): IBinder? {
     return when (action) {
       ACTION_BIND_FEATURE_COORDINATOR, ACTION_BIND_FEATURE_COORDINATOR_FG ->
         featureCoordinator?.asBinder()
-      ACTION_BIND_REMOTE_FEATURE, ACTION_BIND_REMOTE_FEATURE_FG ->
-        connectedDeviceManager?.asBinder()
-      ACTION_BIND_ASSOCIATION -> associatedDeviceManager?.asBinder()
       else -> null
     }
   }
@@ -596,7 +467,6 @@
     val connectedDevices =
       try {
         aliveFeatureCoordinator?.allConnectedDevices
-          ?: aliveConnectedDeviceManager?.activeUserConnectedDevices
       } catch (e: RemoteException) {
         loge(TAG, "Exception while retrieving connected devices.", e)
         null
@@ -642,49 +512,38 @@
 
   override fun startAssociation(callback: IAssociationCallback) {
     aliveFeatureCoordinator?.startAssociation(callback)
-      ?: aliveAssociatedDeviceManager?.startAssociation(callback)
   }
 
   override fun startAssociation(identifier: ParcelUuid, callback: IAssociationCallback) {
-    if (isLegacyOnly.get()) {
-      loge(TAG, "Called unsupported startAssociationWithIdentifier while in legacy mode.")
-      return
-    }
     aliveFeatureCoordinator?.startAssociationWithIdentifier(callback, identifier)
   }
 
   override fun stopAssociation() {
-    aliveFeatureCoordinator?.stopAssociation() ?: aliveAssociatedDeviceManager?.stopAssociation()
+    aliveFeatureCoordinator?.stopAssociation()
   }
 
   override fun acceptVerification() {
     aliveFeatureCoordinator?.acceptVerification()
-      ?: aliveAssociatedDeviceManager?.acceptVerification()
   }
 
   override fun removeAssociatedDevice(deviceId: String) {
     aliveFeatureCoordinator?.removeAssociatedDevice(deviceId)
-      ?: aliveAssociatedDeviceManager?.removeAssociatedDevice(deviceId)
   }
 
   override fun enableAssociatedDeviceConnection(deviceId: String) {
     aliveFeatureCoordinator?.enableAssociatedDeviceConnection(deviceId)
-      ?: aliveAssociatedDeviceManager?.enableAssociatedDeviceConnection(deviceId)
   }
 
   override fun disableAssociatedDeviceConnection(deviceId: String) {
     aliveFeatureCoordinator?.disableAssociatedDeviceConnection(deviceId)
-      ?: aliveAssociatedDeviceManager?.disableAssociatedDeviceConnection(deviceId)
   }
 
   override fun retrieveAssociatedDevices(listener: IOnAssociatedDevicesRetrievedListener) {
     aliveFeatureCoordinator?.retrieveAssociatedDevices(listener)
-      ?: aliveAssociatedDeviceManager?.retrievedActiveUserAssociatedDevices(listener)
   }
 
   override fun retrieveAssociatedDevicesForDriver(listener: IOnAssociatedDevicesRetrievedListener) {
     aliveFeatureCoordinator?.retrieveAssociatedDevicesForDriver(listener)
-      ?: aliveAssociatedDeviceManager?.retrievedActiveUserAssociatedDevices(listener)
   }
 
   override fun retrieveAssociatedDevicesForPassengers(
@@ -694,15 +553,20 @@
       ?: listener.onAssociatedDevicesRetrieved(emptyList())
   }
 
+  override fun claimAssociatedDevice(deviceId: String) {
+    aliveFeatureCoordinator?.claimAssociatedDevice(deviceId)
+  }
+
+  override fun removeAssociatedDeviceClaim(deviceId: String) {
+    aliveFeatureCoordinator?.removeAssociatedDeviceClaim(deviceId)
+  }
+
   private fun sendMessageInternal(device: ConnectedDevice, message: DeviceMessage) {
     aliveFeatureCoordinator?.sendMessage(device, message)
-      ?: aliveConnectedDeviceManager?.sendMessage(device, message)
   }
 
   private fun onServiceConnected() {
-    if (featureCoordinator == null &&
-        (connectedDeviceManager == null || associatedDeviceManager == null)
-    ) {
+    if (!isConnected) {
       logd(
         TAG,
         "Initialization criteria have not been met yet. Waiting for further service connections " +
@@ -711,15 +575,13 @@
       return
     }
     initializePlatform()
-    callback?.onConnected()
   }
 
   private fun initializePlatform() {
-    initializeFeatureCoordinator()
-    initializeConnectedDeviceManager()
-  }
-
-  private fun initializeFeatureCoordinator() {
+    if (!isPlatformInitialized.compareAndSet(false, true)) {
+      logw(TAG, "Platform is already initialized. Ignoring.")
+      return
+    }
     logd(TAG, "Initializing FeatureCoordinator.")
     val featureCoordinator = featureCoordinator
     if (featureCoordinator == null) {
@@ -763,40 +625,7 @@
     } catch (e: RemoteException) {
       loge(TAG, "Error while initializing FeatureCoordinator.", e)
     }
-  }
-
-  private fun initializeConnectedDeviceManager() {
-    logd(TAG, "Initializing ConnectedDeviceManager.")
-    if (featureCoordinator != null) {
-      logd(TAG, "FeatureCoordinator has already registered callbacks.")
-      return
-    }
-    val connectedDeviceManager =
-      connectedDeviceManager
-        ?: throw IllegalStateException(
-          "Attempted to initialize the platform with a null ConnectedDeviceManager! This should " +
-            "not happen and is indicative of a serious logic flaw."
-        )
-    try {
-      connectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback)
-      connectedDeviceManager.registerDeviceAssociationCallback(deviceAssociationCallback)
-      connectedDeviceManager.registerOnLogRequestedListener(loggerId, logRequestedListener)
-      val activeUserConnectedDevices = connectedDeviceManager.activeUserConnectedDevices
-      val featureId = featureId
-      for (device in activeUserConnectedDevices) {
-        callback?.onDeviceConnected(device)
-        if (device.hasSecureChannel()) {
-          callback?.onSecureChannelEstablished(device)
-        }
-        if (featureId == null) {
-          logd(TAG, "There is currently no feature id set. Skipping device registration.")
-          continue
-        }
-        connectedDeviceManager.registerDeviceCallback(device, featureId, deviceCallback)
-      }
-    } catch (e: RemoteException) {
-      loge(TAG, "Error while initializing ConnectedDeviceManager.", e)
-    }
+    callback?.onConnected()
   }
 
   private fun onServiceDisconnected() {
@@ -805,17 +634,8 @@
   }
 
   private fun onNullBinding() {
-    if (!isLegacyOnly.compareAndSet(false, true)) {
-      logd(TAG, "Issuing onFailedToConnect callback after legacy null binding.")
-      callback?.onFailedToConnect()
-      return
-    }
-    logd(
-      TAG,
-      "Service does not support feature coordinator despite defining the action. " +
-        "Attempting to bind again in legacy mode."
-    )
-    connect()
+    logd(TAG, "Issuing onFailedToConnect callback after null binding.")
+    callback?.onFailedToConnect()
   }
 
   private fun resolveIntent(action: String): Intent? {
@@ -908,160 +728,6 @@
         this.featureCoordinator = featureCoordinator
       }
 
-    @JvmStatic
-    /** Create a [CompanionConnector] instance with a [connectedDeviceManager] already populated. */
-    fun createLocalConnector(
-      context: Context,
-      userType: @UserType Int,
-      connectedDeviceManager: IConnectedDeviceManager,
-      associatedDeviceManager: IAssociatedDeviceManager
-    ): CompanionConnector =
-      CompanionConnector(context, userType = userType).apply {
-        this.connectedDeviceManager = connectedDeviceManager
-        this.associatedDeviceManager = associatedDeviceManager
-      }
-
-    /**
-     * Generate an [IConnectedDeviceManager] wrapper around the [fromFeatureCoordinator] to support
-     * legacy functionality.
-     */
-    @VisibleForTesting
-    internal fun createConnectedDeviceManagerWrapper(
-      fromFeatureCoordinator: IFeatureCoordinator
-    ): IConnectedDeviceManager {
-
-      return object : IConnectedDeviceManager.Stub() {
-        @Throws(RemoteException::class)
-        override fun getActiveUserConnectedDevices(): List<ConnectedDevice> {
-          return fromFeatureCoordinator.connectedDevicesForDriver
-        }
-
-        @Throws(RemoteException::class)
-        override fun registerActiveUserConnectionCallback(callback: IConnectionCallback) {
-          fromFeatureCoordinator.registerDriverConnectionCallback(callback)
-        }
-
-        @Throws(RemoteException::class)
-        override fun unregisterConnectionCallback(callback: IConnectionCallback) {
-          fromFeatureCoordinator.unregisterConnectionCallback(callback)
-        }
-
-        @Throws(RemoteException::class)
-        override fun registerDeviceCallback(
-          connectedDevice: ConnectedDevice,
-          recipientId: ParcelUuid,
-          callback: IDeviceCallback
-        ) {
-          fromFeatureCoordinator.registerDeviceCallback(connectedDevice, recipientId, callback)
-        }
-
-        @Throws(RemoteException::class)
-        override fun unregisterDeviceCallback(
-          connectedDevice: ConnectedDevice,
-          recipientId: ParcelUuid,
-          callback: IDeviceCallback
-        ) {
-          fromFeatureCoordinator.unregisterDeviceCallback(connectedDevice, recipientId, callback)
-        }
-
-        @Throws(RemoteException::class)
-        override fun sendMessage(
-          connectedDevice: ConnectedDevice,
-          message: DeviceMessage
-        ): Boolean {
-          return fromFeatureCoordinator.sendMessage(connectedDevice, message)
-        }
-
-        @Throws(RemoteException::class)
-        override fun registerDeviceAssociationCallback(callback: IDeviceAssociationCallback) {
-          fromFeatureCoordinator.registerDeviceAssociationCallback(callback)
-        }
-
-        @Throws(RemoteException::class)
-        override fun unregisterDeviceAssociationCallback(callback: IDeviceAssociationCallback) {
-          fromFeatureCoordinator.unregisterDeviceAssociationCallback(callback)
-        }
-
-        @Throws(RemoteException::class)
-        override fun registerOnLogRequestedListener(
-          loggerId: Int,
-          listener: IOnLogRequestedListener
-        ) {
-          fromFeatureCoordinator.registerOnLogRequestedListener(loggerId, listener)
-        }
-
-        @Throws(RemoteException::class)
-        override fun unregisterOnLogRequestedListener(
-          loggerId: Int,
-          listener: IOnLogRequestedListener
-        ) {
-          fromFeatureCoordinator.unregisterOnLogRequestedListener(loggerId, listener)
-        }
-
-        @Throws(RemoteException::class)
-        override fun processLogRecords(loggerId: Int, logRecords: ByteArray) {
-          fromFeatureCoordinator.processLogRecords(loggerId, logRecords)
-        }
-      }
-    }
-
-    @VisibleForTesting
-    internal fun createAssociatedDeviceManagerWrapper(
-      fromFeatureCoordinator: IFeatureCoordinator
-    ): IAssociatedDeviceManager.Stub {
-      return object : IAssociatedDeviceManager.Stub() {
-        override fun startAssociation(callback: IAssociationCallback) {
-          fromFeatureCoordinator.startAssociation(callback)
-        }
-
-        override fun stopAssociation() {
-          fromFeatureCoordinator.stopAssociation()
-        }
-
-        override fun retrievedActiveUserAssociatedDevices(
-          listener: IOnAssociatedDevicesRetrievedListener
-        ) {
-          fromFeatureCoordinator.retrieveAssociatedDevicesForDriver(listener)
-        }
-
-        override fun acceptVerification() {
-          fromFeatureCoordinator.acceptVerification()
-        }
-
-        override fun removeAssociatedDevice(deviceId: String) {
-          fromFeatureCoordinator.removeAssociatedDevice(deviceId)
-        }
-
-        override fun registerDeviceAssociationCallback(callback: IDeviceAssociationCallback) {
-          fromFeatureCoordinator.registerDeviceAssociationCallback(callback)
-        }
-
-        override fun unregisterDeviceAssociationCallback(callback: IDeviceAssociationCallback) {
-          fromFeatureCoordinator.unregisterDeviceAssociationCallback(callback)
-        }
-
-        override fun getActiveUserConnectedDevices(): List<ConnectedDevice> {
-          return fromFeatureCoordinator.connectedDevicesForDriver
-        }
-
-        override fun registerConnectionCallback(callback: IConnectionCallback) {
-          fromFeatureCoordinator.registerDriverConnectionCallback(callback)
-        }
-
-        override fun unregisterConnectionCallback(callback: IConnectionCallback) {
-          fromFeatureCoordinator.unregisterConnectionCallback(callback)
-        }
-
-        override fun enableAssociatedDeviceConnection(deviceId: String) {
-          fromFeatureCoordinator.enableAssociatedDeviceConnection(deviceId)
-        }
-
-        override fun disableAssociatedDeviceConnection(deviceId: String) {
-          fromFeatureCoordinator.disableAssociatedDeviceConnection(deviceId)
-        }
-      }
-    }
-
     /** A generator of unique IDs for queries. */
     private class QueryIdGenerator {
       private val messageId = AtomicInteger(0)
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/Connector.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/api/Connector.kt
index dc7ff1b..1263ed9 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/api/Connector.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/Connector.kt
@@ -135,6 +135,12 @@
    */
   fun retrieveAssociatedDevicesForPassengers(listener: IOnAssociatedDevicesRetrievedListener)
 
+  /** Claim an associated device as belonging to the current user. */
+  fun claimAssociatedDevice(deviceId: String)
+
+  /** Remove the claim on the identified associated device. */
+  fun removeAssociatedDeviceClaim(deviceId: String)
+
   /** Callbacks invoked on connection events. */
   interface Callback {
     /** Invoked when a connection has been successfully established. */
@@ -215,21 +221,6 @@
 
   companion object {
     /**
-     * When a client calls [Context.bindService] to get the [IConnectedDeviceManager], this action
-     * is required in the param [Intent].
-     */
-    const val ACTION_BIND_REMOTE_FEATURE =
-      "com.google.android.connecteddevice.api.BIND_REMOTE_FEATURE"
-
-    /**
-     * When a client calls [Context.bindService] to get the [IConnectedDeviceManager] from a service
-     * running in the foreground user. Any process that resides outside of the service host
-     * application must use this action in its [Intent].
-     */
-    const val ACTION_BIND_REMOTE_FEATURE_FG =
-      "com.google.android.connecteddevice.api.BIND_REMOTE_FEATURE_FG"
-
-    /**
      * When a client calls [Context.bindService] to get the [IFeatureCoordinator], this action is
      * required in the param [Intent].
      */
@@ -244,12 +235,6 @@
     const val ACTION_BIND_FEATURE_COORDINATOR_FG =
       "com.google.android.connecteddevice.api.BIND_FEATURE_COORDINATOR_FG"
 
-    /**
-     * When a client calls [Context.bindService] to get the [IAssociatedDeviceManager], this action
-     * is required in the param [Intent].
-     */
-    const val ACTION_BIND_ASSOCIATION = "com.google.android.connecteddevice.BIND_ASSOCIATION"
-
     /** Type associated with a driver's device. */
     const val USER_TYPE_DRIVER = 1 shl 0
 
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/IAssociatedDeviceManager.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/api/IAssociatedDeviceManager.aidl
deleted file mode 100644
index 34f9866..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/api/IAssociatedDeviceManager.aidl
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.api;
-
-import com.google.android.connecteddevice.api.IAssociationCallback;
-import com.google.android.connecteddevice.api.IConnectionCallback;
-import com.google.android.connecteddevice.api.IDeviceAssociationCallback;
-import com.google.android.connecteddevice.api.IOnAssociatedDevicesRetrievedListener;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.ConnectedDevice;
-import com.google.android.connecteddevice.model.OobEligibleDevice;
-
-/** Manager of devices associated with the car. */
-interface IAssociatedDeviceManager {
-    /**
-     * Starts the association with a new device.
-     *
-     * @param callback {@link IAssociationCallback} to be notified for association event.
-     */
-    void startAssociation(in IAssociationCallback callback);
-
-    /** Stops the association with current device. */
-    void stopAssociation();
-
-    /**
-     * Retrieves the devices associated with the active user from the database.
-     *
-     * @param listener {@link IOnAssociatedDevicesRetrievedListener} that will
-     * be notified when the associated devices are retrieved.
-     */
-    void retrievedActiveUserAssociatedDevices(in IOnAssociatedDevicesRetrievedListener listener);
-
-    /** Confirms the paring code. */
-    void acceptVerification();
-
-    /** Remove the associated device of the given identifier for the active user. */
-    void removeAssociatedDevice(in String deviceId);
-
-    /**
-     * Register a callback for associated device related events.
-     *
-     * @param callback {@link IDeviceAssociationCallback} to register.
-     */
-    void registerDeviceAssociationCallback(in IDeviceAssociationCallback callback);
-
-    /** Unregister the device association callback from manager.
-     *
-     * @param callback {@link IDeviceAssociationCallback} to unregister.
-     */
-    void unregisterDeviceAssociationCallback(in IDeviceAssociationCallback callback);
-
-    /** Returns {@link List<ConnectedDevice>} of devices currently connected. */
-    List<ConnectedDevice> getActiveUserConnectedDevices();
-
-    /**
-     * Register a callback for connection events for only the currently active user's devices.
-     *
-     * @param callback {@link IConnectionCallback} to register.
-     */
-    void registerConnectionCallback(in IConnectionCallback callback);
-
-    /**
-     * Unregister the connection callback from manager.
-     *
-     * @param callback {@link IConnectionCallback} to unregister.
-     */
-    void unregisterConnectionCallback(in IConnectionCallback callback);
-
-    /** Enable connection on the associated device with the given identifier. */
-    void enableAssociatedDeviceConnection(in String deviceId);
-
-    /** Disable connection on the associated device with the given identifier. */
-    void disableAssociatedDeviceConnection(in String deviceId);
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/IConnectedDeviceManager.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/api/IConnectedDeviceManager.aidl
deleted file mode 100644
index ef0606f..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/api/IConnectedDeviceManager.aidl
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.api;
-
-import android.os.ParcelUuid;
-
-import com.google.android.connecteddevice.api.IConnectionCallback;
-import com.google.android.connecteddevice.api.IDeviceAssociationCallback;
-import com.google.android.connecteddevice.api.IDeviceCallback;
-import com.google.android.connecteddevice.api.IOnLogRequestedListener;
-import com.google.android.connecteddevice.model.ConnectedDevice;
-import com.google.android.connecteddevice.model.DeviceMessage;
-
-/** Manager of devices connected to the car. */
-interface IConnectedDeviceManager {
-
-    /** Returns {@link List<ConnectedDevice>} of devices currently connected. */
-    List<ConnectedDevice> getActiveUserConnectedDevices();
-
-    /**
-     * Register a callback for manager triggered connection events for only the currently active
-     * user's devices.
-     *
-     * @param callback {@link IConnectionCallback} to register.
-     */
-    void registerActiveUserConnectionCallback(in IConnectionCallback callback);
-
-    /**
-     * Unregister a connection callback from manager.
-     *
-     * @param callback {@link IConnectionCallback} to unregister.
-     */
-    void unregisterConnectionCallback(in IConnectionCallback callback);
-
-    /**
-     * Register a callback for a specific connectedDevice and recipient.
-     *
-     * @param connectedDevice {@link ConnectedDevice} to register triggers on.
-     * @param recipientId {@link ParcelUuid} to register as recipient of.
-     * @param callback {@link IDeviceCallback} to register.
-     */
-    void registerDeviceCallback(in ConnectedDevice connectedDevice, in ParcelUuid recipientId,
-            in IDeviceCallback callback);
-
-    /**
-     * Unregister callback from connectedDevice events.
-     *
-     * @param connectedDevice {@link ConnectedDevice} callback was registered on.
-     * @param recipientId {@link ParcelUuid} callback was registered under.
-     * @param callback {@link IDeviceCallback} to unregister.
-     */
-    void unregisterDeviceCallback(in ConnectedDevice connectedDevice, in ParcelUuid recipientId,
-            in IDeviceCallback callback);
-
-    /**
-     * Send a message to a connected device.
-     *
-     * @param connectedDevice {@link ConnectedDevice} to send the message to.
-     * @param message Message to send.
-     */
-    boolean sendMessage(in ConnectedDevice connectedDevice, in DeviceMessage message);
-
-
-    /**
-     * Register a callback for associated device related events.
-     *
-     * @param callback {@link IDeviceAssociationCallback} to register.
-     */
-    void registerDeviceAssociationCallback(in IDeviceAssociationCallback callback);
-
-    /**
-     * Unregister a device association callback from manager.
-     *
-     * @param callback {@link IDeviceAssociationCallback} to unregister.
-     */
-    void unregisterDeviceAssociationCallback(in IDeviceAssociationCallback callback);
-
-    /**
-     * Register listener for the log request with the given logger identifier.
-     *
-     * @param listener {@link IOnLogRequestedListener} to register.
-     */
-    void registerOnLogRequestedListener(in int loggerId, in IOnLogRequestedListener listener);
-
-    /**
-     * Unregister listener from the log request.
-     *
-     * @param listener {@link IOnLogRequestedListener} to unregister.
-     */
-    void unregisterOnLogRequestedListener(in int loggerId, in IOnLogRequestedListener listener);
-
-    /**
-     * Process log records in the logger with given id so it can be combined with log records
-     * from other loggers.
-     *
-     * @param loggerId of the logger.
-     * @param logRecords to process.
-     */
-    void processLogRecords(in int loggerId, in byte[] logRecords);
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/AssociationSecureChannel.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/AssociationSecureChannel.java
deleted file mode 100644
index 38be547..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/AssociationSecureChannel.java
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static com.google.android.encryptionrunner.EncryptionRunnerFactory.newRunner;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.android.encryptionrunner.EncryptionRunner;
-import com.google.android.encryptionrunner.EncryptionRunnerFactory.EncryptionRunnerType;
-import com.google.android.encryptionrunner.HandshakeException;
-import com.google.android.encryptionrunner.HandshakeMessage;
-import com.google.android.encryptionrunner.HandshakeMessage.HandshakeState;
-import com.google.android.encryptionrunner.Key;
-import java.security.InvalidParameterException;
-import java.util.Arrays;
-import java.util.UUID;
-
-/** A secure channel established with the association flow. */
-public class AssociationSecureChannel extends SecureChannel {
-
-  private static final String TAG = "AssociationSecureChannel";
-
-  private static final int DEVICE_ID_BYTES = 16;
-
-  private final ConnectedDeviceStorage storage;
-
-  private ShowVerificationCodeListener showVerificationCodeListener;
-
-  @HandshakeState private int state = HandshakeState.UNKNOWN;
-
-  private Key pendingKey;
-
-  private String deviceId;
-
-  public AssociationSecureChannel(DeviceMessageStream stream, ConnectedDeviceStorage storage) {
-    this(stream, storage, newRunner(EncryptionRunnerType.UKEY2));
-  }
-
-  AssociationSecureChannel(
-      DeviceMessageStream stream,
-      ConnectedDeviceStorage storage,
-      EncryptionRunner encryptionRunner) {
-    super(stream, encryptionRunner);
-    encryptionRunner.setIsReconnect(false);
-    this.storage = storage;
-  }
-
-  @Override
-  void processHandshake(@NonNull byte[] message) throws HandshakeException {
-    switch (state) {
-      case HandshakeState.UNKNOWN:
-        processHandshakeUnknown(message);
-        break;
-      case HandshakeState.IN_PROGRESS:
-        processHandshakeInProgress(message);
-        break;
-      case HandshakeState.FINISHED:
-        processHandshakeDeviceIdAndSecret(message);
-        break;
-      default:
-        loge(TAG, "Encountered unexpected handshake state: " + state + ".");
-        notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
-    }
-  }
-
-  private void processHandshakeUnknown(@NonNull byte[] message) throws HandshakeException {
-    logd(TAG, "Responding to handshake init request.");
-    HandshakeMessage handshakeMessage = getEncryptionRunner().respondToInitRequest(message);
-    state = handshakeMessage.getHandshakeState();
-    sendHandshakeMessage(handshakeMessage.getNextMessage(), /* isEncrypted= */ false);
-  }
-
-  private void processHandshakeInProgress(@NonNull byte[] message) throws HandshakeException {
-    logd(TAG, "Continuing handshake.");
-    HandshakeMessage handshakeMessage = getEncryptionRunner().continueHandshake(message);
-    state = handshakeMessage.getHandshakeState();
-    if (state != HandshakeState.VERIFICATION_NEEDED) {
-      loge(
-          TAG,
-          "processHandshakeInProgress: Encountered unexpected handshake state: " + state + ".");
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
-      return;
-    }
-
-    String code = handshakeMessage.getVerificationCode();
-    if (code == null) {
-      loge(TAG, "Unable to get verification code.");
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
-      return;
-    }
-    processVerificationCode(code);
-  }
-
-  private void processVerificationCode(@NonNull String code) {
-    if (showVerificationCodeListener == null) {
-      loge(
-          TAG,
-          "No verification code listener has been set. Unable to display verification code to "
-              + "user.");
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
-      return;
-    }
-
-    logd(TAG, "Showing pairing code: " + code);
-    showVerificationCodeListener.showVerificationCode(code);
-  }
-
-  private void processHandshakeDeviceIdAndSecret(@NonNull byte[] message) {
-    UUID deviceId = ByteUtils.bytesToUUID(Arrays.copyOf(message, DEVICE_ID_BYTES));
-    if (deviceId == null) {
-      loge(TAG, "Received invalid device id. Aborting.");
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_DEVICE_ID);
-      return;
-    }
-    this.deviceId = deviceId.toString();
-    notifyCallback(callback -> callback.onDeviceIdReceived(this.deviceId));
-
-    storage.saveEncryptionKey(this.deviceId, pendingKey.asBytes());
-    pendingKey = null;
-    try {
-      storage.saveChallengeSecret(
-          this.deviceId, Arrays.copyOfRange(message, DEVICE_ID_BYTES, message.length));
-    } catch (InvalidParameterException e) {
-      loge(TAG, "Error saving challenge secret.", e);
-      notifySecureChannelFailure(CHANNEL_ERROR_STORAGE_ERROR);
-      return;
-    }
-
-    notifyCallback(Callback::onSecureChannelEstablished);
-  }
-
-  /** Set the listener that notifies to show verification code. {@code null} to clear. */
-  public void setShowVerificationCodeListener(@Nullable ShowVerificationCodeListener listener) {
-    showVerificationCodeListener = listener;
-  }
-
-  @VisibleForTesting
-  @Nullable
-  public ShowVerificationCodeListener getShowVerificationCodeListener() {
-    return showVerificationCodeListener;
-  }
-
-  /**
-   * Called by the client to notify that the user has accepted a pairing code or any out-of-band
-   * confirmation, and send confirmation signals to remote bluetooth device.
-   */
-  public void notifyOutOfBandAccepted() {
-    HandshakeMessage message;
-    try {
-      message = getEncryptionRunner().notifyPinVerified();
-    } catch (HandshakeException e) {
-      loge(TAG, "Error during PIN verification", e);
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
-      return;
-    }
-    if (message.getHandshakeState() != HandshakeState.FINISHED) {
-      loge(
-          TAG,
-          "Handshake not finished after calling verify PIN. Instead got "
-              + "state: "
-              + message.getHandshakeState()
-              + ".");
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
-      return;
-    }
-
-    Key localKey = message.getKey();
-    if (localKey == null) {
-      loge(TAG, "Unable to finish association, generated key is null.");
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
-      return;
-    }
-    state = message.getHandshakeState();
-    setEncryptionKey(localKey);
-    pendingKey = localKey;
-    logd(TAG, "Pairing code successfully verified.");
-    sendUniqueIdToClient();
-  }
-
-  private void sendUniqueIdToClient() {
-    UUID uniqueId = storage.getUniqueId();
-    logd(TAG, "Sending car's device id of " + uniqueId + " to device.");
-    sendHandshakeMessage(ByteUtils.uuidToBytes(uniqueId), /* isEncrypted= */ true);
-  }
-
-  @HandshakeState
-  int getState() {
-    return state;
-  }
-
-  void setState(@HandshakeState int state) {
-    this.state = state;
-  }
-
-  /** Listener that will be invoked to display verification code. */
-  public interface ShowVerificationCodeListener {
-    /**
-     * Invoke when a verification need to be displayed during device association.
-     *
-     * @param code The verification code to show.
-     */
-    void showVerificationCode(@NonNull String code);
-  }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/CarBluetoothManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/CarBluetoothManager.java
deleted file mode 100644
index f5b8613..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/CarBluetoothManager.java
+++ /dev/null
@@ -1,605 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection;
-
-import static com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange.OobChannelType.BT_RFCOMM;
-import static com.google.android.connecteddevice.model.Errors.DEVICE_ERROR_INVALID_HANDSHAKE;
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static com.google.android.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange.OobChannelType;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.model.OobEligibleDevice;
-import com.google.android.connecteddevice.oob.OobChannel;
-import com.google.android.connecteddevice.oob.OobConnectionManager;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.util.ThreadSafeCallbacks;
-import com.google.common.collect.ImmutableList;
-import java.util.List;
-import java.util.Objects;
-import java.util.UUID;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.Executor;
-import java.util.stream.Collectors;
-
-/**
- * Generic manager for a car that keeps track of connected devices and their associated callbacks.
- */
-public abstract class CarBluetoothManager {
-
-  private static final String TAG = "CarBluetoothManager";
-
-  private static final ImmutableList<OobChannelType> SUPPORTED_OOB_CAPABILITIES =
-      ImmutableList.of(BT_RFCOMM);
-
-  protected final ConnectedDeviceStorage storage;
-
-  protected final CopyOnWriteArraySet<ConnectedRemoteDevice> connectedDevices =
-      new CopyOnWriteArraySet<>();
-
-  protected final ThreadSafeCallbacks<Callback> callbacks = new ThreadSafeCallbacks<>();
-
-  protected final OobChannel oobChannel;
-
-  private final boolean isCompressionEnabled;
-
-  private final OobConnectionManager oobConnectionManager;
-
-  private final boolean isCapabilitiesEligible;
-
-  private String clientDeviceName;
-
-  private String clientDeviceAddress;
-
-  private AssociationCallback associationCallback;
-
-  protected CarBluetoothManager(
-      @NonNull ConnectedDeviceStorage connectedDeviceStorage,
-      boolean enableCompression,
-      @Nullable OobChannel oobChannel,
-      boolean isCapabilitiesEligible) {
-    storage = connectedDeviceStorage;
-    isCompressionEnabled = enableCompression;
-    oobConnectionManager = new OobConnectionManager();
-    this.oobChannel = oobChannel;
-    this.isCapabilitiesEligible = isCapabilitiesEligible;
-  }
-
-  protected CarBluetoothManager(
-      @NonNull ConnectedDeviceStorage connectedDeviceStorage,
-      boolean enableCompression,
-      boolean isCapabilitiesEligible) {
-    this(connectedDeviceStorage, enableCompression, /* oobChannel= */ null, isCapabilitiesEligible);
-  }
-
-  /** Attempt to connect to device with provided id. */
-  public void connectToDevice(@NonNull UUID deviceId) {
-    for (ConnectedRemoteDevice device : connectedDevices) {
-      if (UUID.fromString(device.deviceId).equals(deviceId)) {
-        logd(TAG, "Already connected to device " + deviceId + ".");
-        // Already connected to this device. Ignore requests to connect again.
-        return;
-      }
-    }
-    // Clear any previous session before starting a new one.
-    reset();
-    initiateConnectionToDevice(deviceId);
-  }
-
-  /** Start to connect to associated devices */
-  public abstract void initiateConnectionToDevice(@NonNull UUID deviceId);
-
-  /** Start the association with a new device */
-  public abstract void startAssociation(
-      @NonNull byte[] nameForAssociation, @NonNull AssociationCallback callback);
-
-  /** Disconnect the provided device from this manager. */
-  public abstract void disconnectDevice(@NonNull String deviceId);
-
-  /** Get current {@link OobConnectionManager}. */
-  @Nullable
-  @VisibleForTesting
-  public OobConnectionManager getOobConnectionManager() {
-    return oobConnectionManager;
-  }
-
-  /** Get current {@link AssociationCallback}. */
-  @Nullable
-  public AssociationCallback getAssociationCallback() {
-    return associationCallback;
-  }
-
-  /** Set current {@link AssociationCallback}. */
-  public void setAssociationCallback(@Nullable AssociationCallback callback) {
-    this.associationCallback = callback;
-  }
-
-  /** Set the value of the client device name */
-  public void setClientDeviceName(String deviceName) {
-    clientDeviceName = deviceName;
-  }
-
-  /** Set the value of client device's mac address */
-  public void setClientDeviceAddress(String macAddress) {
-    clientDeviceAddress = macAddress;
-  }
-
-  /** Initialize and start the manager. */
-  @CallSuper
-  public void start() {}
-
-  /** Stop the manager and clean up. */
-  public void stop() {
-    for (ConnectedRemoteDevice device : connectedDevices) {
-      if (device.gatt != null) {
-        device.gatt.close();
-      }
-    }
-    connectedDevices.clear();
-  }
-
-  /** Stop the association process with any device. */
-  public void stopAssociation() {
-    if (!isAssociating()) {
-      return;
-    }
-    reset();
-  }
-
-  /** Register a {@link Callback} to be notified on the {@link Executor}. */
-  public void registerCallback(@NonNull Callback callback, @NonNull Executor executor) {
-    callbacks.add(callback, executor);
-  }
-
-  /**
-   * Unregister a callback.
-   *
-   * @param callback The {@link Callback} to unregister.
-   */
-  public void unregisterCallback(@NonNull Callback callback) {
-    callbacks.remove(callback);
-  }
-
-  /**
-   * Send a message to a connected device.
-   *
-   * @param deviceId Id of connected device.
-   * @param message {@link DeviceMessage} to send.
-   */
-  public void sendMessage(@NonNull String deviceId, @NonNull DeviceMessage message) {
-    ConnectedRemoteDevice device = getConnectedDevice(deviceId);
-    if (device == null) {
-      logw(TAG, "Attempted to send message to unknown device $deviceId. Ignored.");
-      return;
-    }
-
-    sendMessage(device, message);
-  }
-
-  /**
-   * Send a message to a connected device.
-   *
-   * @param device The connected {@link ConnectedRemoteDevice}.
-   * @param message {@link DeviceMessage} to send.
-   */
-  public void sendMessage(@NonNull ConnectedRemoteDevice device, @NonNull DeviceMessage message) {
-    String deviceId = device.deviceId;
-    if (deviceId == null) {
-      deviceId = "Unidentified device";
-    }
-
-    logd(TAG, "Writing " + message.getMessage().length + " bytes to " + deviceId + ".");
-    device.secureChannel.sendClientMessage(message);
-  }
-
-  /** Clean manager status and callbacks. */
-  @CallSuper
-  public void reset() {
-    // Clean up state prior to issuing callbacks to avoid race conditions.
-    List<String> callbackIds =
-        connectedDevices
-            .stream()
-            .map((device) -> device.deviceId)
-            .filter(Objects::nonNull)
-            .collect(Collectors.toList());
-    clientDeviceAddress = null;
-    clientDeviceName = null;
-    connectedDevices.clear();
-    oobConnectionManager.reset();
-    associationCallback = null;
-    for (String deviceId : callbackIds) {
-      callbacks.invoke(callback -> callback.onDeviceDisconnected(deviceId));
-    }
-  }
-
-  /** Notify that the user has accepted a pairing code or other out-of-band confirmation. */
-  public void notifyOutOfBandAccepted() {
-    if (getConnectedDevice() == null) {
-      disconnectWithError(
-          "Null connected device found when out-of-band confirmation " + "received.");
-      return;
-    }
-
-    AssociationSecureChannel secureChannel =
-        (AssociationSecureChannel) getConnectedDevice().secureChannel;
-    if (secureChannel == null) {
-      disconnectWithError(
-          "Null SecureBleChannel found for the current connected device "
-              + "when out-of-band confirmation received.");
-      return;
-    }
-
-    secureChannel.notifyOutOfBandAccepted();
-  }
-
-  /** Returns the secure channel of current connected device. */
-  @Nullable
-  public SecureChannel getConnectedDeviceChannel() {
-    ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-    if (connectedDevice == null) {
-      return null;
-    }
-
-    return connectedDevice.secureChannel;
-  }
-
-  /** Return the current connected device. */
-  @Nullable
-  protected final ConnectedRemoteDevice getConnectedDevice() {
-    if (connectedDevices.isEmpty()) {
-      return null;
-    }
-    // Directly return the next because there will only be one device connected at one time.
-    return connectedDevices.iterator().next();
-  }
-
-  /**
-   * Get the {@link ConnectedRemoteDevice} with matching {@link BluetoothGatt} if available. Returns
-   * {@code null} if no matches are found.
-   */
-  @Nullable
-  protected final ConnectedRemoteDevice getConnectedDevice(@NonNull BluetoothGatt gatt) {
-    for (ConnectedRemoteDevice device : connectedDevices) {
-      if (device.gatt == gatt) {
-        return device;
-      }
-    }
-
-    return null;
-  }
-
-  /**
-   * Get the {@link ConnectedRemoteDevice} with matching {@link BluetoothDevice} if available.
-   * Returns {@code null} if no matches are found.
-   */
-  @Nullable
-  protected final ConnectedRemoteDevice getConnectedDevice(@NonNull BluetoothDevice device) {
-    for (ConnectedRemoteDevice connectedDevice : connectedDevices) {
-      if (device.equals(connectedDevice.device)) {
-        return connectedDevice;
-      }
-    }
-
-    return null;
-  }
-
-  /**
-   * Get the {@link ConnectedRemoteDevice} with matching device id if available. Returns {@code
-   * null} if no matches are found.
-   */
-  @Nullable
-  protected final ConnectedRemoteDevice getConnectedDevice(@NonNull String deviceId) {
-    for (ConnectedRemoteDevice device : connectedDevices) {
-      if (deviceId.equals(device.deviceId)) {
-        return device;
-      }
-    }
-
-    return null;
-  }
-
-  protected final void onDeviceConnectedForAssociation(
-      DeviceMessageStream secureStream, ConnectedRemoteDevice connectedDevice) {
-
-    BluetoothDevice device = connectedDevice.device;
-    ImmutableList<OobChannelType> supportedOobCapabilities = SUPPORTED_OOB_CAPABILITIES;
-    if (!isCapabilitiesEligible) {
-      logd(TAG, "Capabilities exchange is not supported.");
-      supportedOobCapabilities = ImmutableList.of();
-    }
-
-    // TODO(b/180743873) Hardcoding to true until ConnectionResolver is capable of resolving all
-    // the way to a SecureChannel. At that time we will introduce another flag for whether we
-    // are reconnecting and move this logic internal to the resolver itself.
-    ConnectionResolver connectionResolver =
-        new ConnectionResolver(
-            secureStream,
-            /* isCapabilitiesEligible= */ true,
-            supportedOobCapabilities);
-
-    connectionResolver.resolveConnection(
-        resolvedConnection -> {
-          ImmutableList<OobChannelType> oobChannels = resolvedConnection.oobChannelTypes();
-
-          if (oobChannel == null || oobChannels == null || !oobChannels.contains(BT_RFCOMM)) {
-            logd(TAG, "Oob channels list are empty, fallback to normal association " + "flow.");
-            addConnectedDeviceWithAssociationSecureChannel(
-                connectedDevice, new AssociationSecureChannel(secureStream, storage));
-            return;
-          }
-
-          logd(TAG, "Completing out of band data exchange.");
-          oobChannel.completeOobDataExchange(
-              new OobEligibleDevice(device.getAddress(), OobEligibleDevice.OOB_TYPE_BLUETOOTH),
-              new OobChannel.Callback() {
-                @Override
-                public void onOobExchangeSuccess() {
-                  logd(
-                      TAG,
-                      "Out of band exchange succeeded. Proceeding to association with"
-                          + " device.");
-                  if (getOobConnectionManager().startOobExchange(oobChannel)) {
-                    addConnectedDeviceWithAssociationSecureChannel(
-                        connectedDevice,
-                        new OobAssociationSecureChannel(
-                            secureStream, storage, getOobConnectionManager()));
-                  } else {
-                    logw(
-                        TAG,
-                        "Out of band exchange failed, falling back to Numeric" + " Comparison.");
-                    addConnectedDeviceWithAssociationSecureChannel(
-                        connectedDevice, new AssociationSecureChannel(secureStream, storage));
-                  }
-                }
-
-                @Override
-                public void onOobExchangeFailure() {
-                  logw(
-                      TAG, "Out of band exchange failed, falling back to Numeric" + " Comparison.");
-                  addConnectedDeviceWithAssociationSecureChannel(
-                      connectedDevice, new AssociationSecureChannel(secureStream, storage));
-                }
-              });
-        });
-  }
-
-  protected void onVerificationCodeAvailable(String code) {
-    if (!isAssociating()) {
-      loge(TAG, "No valid callback for association.");
-      return;
-    }
-    getAssociationCallback().onVerificationCodeAvailable(code);
-  }
-  /** Add the {@link ConnectedRemoteDevice} that has connected. */
-  protected final void addConnectedDevice(@NonNull ConnectedRemoteDevice device) {
-    device.secureChannel.setCompressionEnabled(isCompressionEnabled);
-    connectedDevices.add(device);
-  }
-
-  /** Return the number of devices currently connected. */
-  protected final int getConnectedDevicesCount() {
-    return connectedDevices.size();
-  }
-
-  /** Remove [@link BleDevice} that has been disconnected. */
-  protected final void removeConnectedDevice(@NonNull ConnectedRemoteDevice device) {
-    connectedDevices.remove(device);
-  }
-
-  /** Return [@code true} if the manager is currently in an association process. */
-  protected final boolean isAssociating() {
-    return getAssociationCallback() != null;
-  }
-
-  /**
-   * Set the device id of {@link ConnectedRemoteDevice} and then notify device connected callback.
-   *
-   * @param deviceId The device id received from remote device.
-   */
-  protected final void setDeviceIdAndNotifyCallbacks(@NonNull String deviceId) {
-    logd(TAG, "Setting device id: " + deviceId);
-    ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-    if (connectedDevice == null) {
-      disconnectWithError("Null connected device found when device id received.");
-      return;
-    }
-
-    connectedDevice.deviceId = deviceId;
-    callbacks.invoke(callback -> callback.onDeviceConnected(deviceId));
-  }
-
-  /** Log error which causes the disconnect with {@link Exception} and notify callbacks. */
-  protected final void disconnectWithError(@NonNull String errorMessage, @Nullable Exception e) {
-    loge(TAG, errorMessage, e);
-    if (isAssociating()) {
-      associationCallback.onAssociationError(DEVICE_ERROR_INVALID_HANDSHAKE);
-    }
-    reset();
-  }
-
-  /** Log error which cause the disconnection and notify callbacks. */
-  protected final void disconnectWithError(@NonNull String errorMessage) {
-    disconnectWithError(errorMessage, null);
-  }
-
-  private void addConnectedDeviceWithAssociationSecureChannel(
-      ConnectedRemoteDevice connectedDevice,
-      AssociationSecureChannel secureChannel) {
-    secureChannel.setShowVerificationCodeListener(this::onVerificationCodeAvailable);
-    secureChannel.registerCallback(secureChannelCallback);
-    connectedDevice.secureChannel = secureChannel;
-    addConnectedDevice(connectedDevice);
-  }
-
-  protected final SecureChannel.Callback secureChannelCallback =
-      new SecureChannel.Callback() {
-        @Override
-        public void onSecureChannelEstablished() {
-          ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-          if (connectedDevice == null || connectedDevice.deviceId == null) {
-            disconnectWithError("Null device id found when secure channel " + "established.");
-            return;
-          }
-          String deviceId = connectedDevice.deviceId;
-          if (clientDeviceAddress == null) {
-            disconnectWithError("Null device address found when secure channel " + "established.");
-            return;
-          }
-
-          if (isAssociating()) {
-            logd(
-                TAG,
-                "Secure channel established for un-associated device. Saving "
-                    + "association of that device for current user.");
-            storage.addAssociatedDeviceForDriver(
-                new AssociatedDevice(
-                    deviceId,
-                    clientDeviceAddress,
-                    clientDeviceName,
-                    /* isConnectionEnabled= */ true));
-            if (associationCallback != null) {
-              associationCallback.onAssociationCompleted(deviceId);
-              oobConnectionManager.reset();
-              setAssociationCallback(null);
-            }
-          }
-          callbacks.invoke(callback -> callback.onSecureChannelEstablished(deviceId));
-        }
-
-        @Override
-        public void onEstablishSecureChannelFailure(int error) {
-          ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-          if (connectedDevice == null || connectedDevice.deviceId == null) {
-            disconnectWithError(
-                "Null device id found when secure channel " + "failed to establish.");
-            return;
-          }
-          String deviceId = connectedDevice.deviceId;
-          callbacks.invoke(callback -> callback.onSecureChannelError(deviceId));
-
-          if (isAssociating()) {
-            associationCallback.onAssociationError(error);
-            oobConnectionManager.reset();
-          }
-
-          disconnectWithError("Error while establishing secure connection.");
-        }
-
-        @Override
-        public void onMessageReceived(DeviceMessage deviceMessage) {
-          ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-          if (connectedDevice == null || connectedDevice.deviceId == null) {
-            disconnectWithError("Null device id found when message received.");
-            return;
-          }
-
-          logd(
-              TAG,
-              "Received new message from "
-                  + connectedDevice.deviceId
-                  + " with "
-                  + deviceMessage.getMessage().length
-                  + " bytes in its "
-                  + "payload. Notifying "
-                  + callbacks.size()
-                  + " callbacks.");
-          callbacks.invoke(
-              callback -> callback.onMessageReceived(connectedDevice.deviceId, deviceMessage));
-        }
-
-        @Override
-        public void onMessageReceivedError(Exception exception) {
-          disconnectWithError("Error while receiving message.");
-        }
-
-        @Override
-        public void onDeviceIdReceived(String deviceId) {
-          setDeviceIdAndNotifyCallbacks(deviceId);
-        }
-      };
-
-  /** State for a connected device. */
-  public enum ConnectedDeviceState {
-    CONNECTING,
-    PENDING_VERIFICATION,
-    CONNECTED,
-    UNKNOWN
-  }
-
-  /** Container class to hold information about a connected device. */
-  public static class ConnectedRemoteDevice {
-    @NonNull public BluetoothDevice device;
-    @Nullable public BluetoothGatt gatt;
-    @NonNull public ConnectedDeviceState state;
-    @Nullable public String deviceId;
-    @Nullable public SecureChannel secureChannel;
-
-    public ConnectedRemoteDevice(@NonNull BluetoothDevice device, @Nullable BluetoothGatt gatt) {
-      this.device = device;
-      this.gatt = gatt;
-      state = ConnectedDeviceState.UNKNOWN;
-    }
-  }
-
-  /** Callback for triggered events from {@link CarBluetoothManager}. */
-  public interface Callback {
-    /**
-     * Triggered when device is connected and device id retrieved. Device is now ready to receive
-     * messages.
-     *
-     * @param deviceId Id of device that has connected.
-     */
-    void onDeviceConnected(@NonNull String deviceId);
-
-    /**
-     * Triggered when device is disconnected.
-     *
-     * @param deviceId Id of device that has disconnected.
-     */
-    void onDeviceDisconnected(@NonNull String deviceId);
-
-    /**
-     * Triggered when device has established encryption for secure communication.
-     *
-     * @param deviceId Id of device that has established encryption.
-     */
-    void onSecureChannelEstablished(@NonNull String deviceId);
-
-    /**
-     * Triggered when a new message is received.
-     *
-     * @param deviceId Id of the device that sent the message.
-     * @param message {@link DeviceMessage} received.
-     */
-    void onMessageReceived(@NonNull String deviceId, @NonNull DeviceMessage message);
-
-    /**
-     * Triggered when an error when establishing the secure channel.
-     *
-     * @param deviceId Id of the device that experienced the error.
-     */
-    void onSecureChannelError(@NonNull String deviceId);
-  }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ChannelResolver.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ChannelResolver.kt
index 28660a9..309dddc 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ChannelResolver.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ChannelResolver.kt
@@ -15,8 +15,6 @@
  */
 package com.google.android.connecteddevice.connection
 
-import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange
-import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange.OobChannelType
 import com.google.android.companionprotos.VersionExchangeProto.VersionExchange
 import com.google.android.connecteddevice.model.DeviceMessage
 import com.google.android.connecteddevice.model.DeviceMessage.OperationType.ENCRYPTION_HANDSHAKE
@@ -24,22 +22,18 @@
 import com.google.android.connecteddevice.oob.OobRunner
 import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
 import com.google.android.connecteddevice.transport.ConnectionProtocol
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DataReceivedListener
+import com.google.android.connecteddevice.transport.IDataReceivedListener
 import com.google.android.connecteddevice.transport.ProtocolDevice
 import com.google.android.connecteddevice.util.SafeLog.logd
 import com.google.android.connecteddevice.util.SafeLog.loge
-import com.google.android.connecteddevice.util.SafeLog.logw
 import com.google.android.encryptionrunner.EncryptionRunner
 import com.google.android.encryptionrunner.EncryptionRunnerFactory.EncryptionRunnerType
 import com.google.android.encryptionrunner.EncryptionRunnerFactory.newRunner
-import com.google.common.util.concurrent.MoreExecutors.directExecutor
 import com.google.protobuf.ExtensionRegistryLite
 import com.google.protobuf.InvalidProtocolBufferException
 import java.lang.Integer.min
 import java.util.UUID
 import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.Executor
-import java.util.concurrent.atomic.AtomicBoolean
 import java.util.concurrent.atomic.AtomicReference
 import kotlin.properties.Delegates
 
@@ -53,10 +47,8 @@
  *
  * Channel resolving for association:
  * 1. Process the first received data as version exchange message, send back car versions.
- * 2. If capability exchange is supported, process the second received data as capability exchange
- * message and send back car capabilities.
- * 3. Create a [ProtocolStream] with the current [ConnectionProtocol].
- * 4. Create a [MultiProtocolSecureChannel], add [ProtocolStream]s for all connected
+ * 2. Create a [ProtocolStream] with the current [ConnectionProtocol].
+ * 3. Create a [MultiProtocolSecureChannel], add [ProtocolStream]s for all connected
  * [ConnectionProtocol]s and notify that the channel has been resolved.
  *
  * Channel resolving for reconnect:
@@ -77,16 +69,13 @@
 ) {
   private val currentDevice: AtomicReference<ProtocolDevice?> =
     AtomicReference<ProtocolDevice?>(null)
-  private val capabilityExchanged = AtomicBoolean(false)
-  private val executor: Executor = directExecutor()
   // protocol device -> listeners. This map tracks listeners for channel resolving on connected
   // protocols.
-  private val protocolDevices = ConcurrentHashMap<ProtocolDevice, DataReceivedListener>()
+  private val protocolDevices = ConcurrentHashMap<ProtocolDevice, IDataReceivedListener>()
   private var isReconnect by Delegates.notNull<Boolean>()
   private var deviceId: UUID? = null
   private var challenge: ByteArray? = null
   private var oobRunner: OobRunner? = null
-  private var resolvedSecurityVersion: Int = 0
 
   /** Resolves the [MultiProtocolSecureChannel] for a reconnect with [deviceId] and [challenge]. */
   fun resolveReconnect(deviceId: UUID, challenge: ByteArray) {
@@ -114,24 +103,20 @@
   fun addProtocolDevice(device: ProtocolDevice) {
     logd(TAG, "Registering listener for received data received.")
     val dataReceivedListener =
-      object : DataReceivedListener {
+      object : IDataReceivedListener.Stub() {
         override fun onDataReceived(protocolId: String, data: ByteArray) {
           if (currentDevice.compareAndSet(null, device)) {
             processVersionMessage(data, device)
             return
           }
-          if (isReconnect || !capabilityExchanged.compareAndSet(false, true)) {
-            logd(
-              TAG,
-              "Channel already resolving with connection ${currentDevice.get()?.protocolId}. " +
-                "Ignoring data received from connection $protocolId."
-            )
-            return
-          }
-          processCapabilityMessage(data, device)
+          logd(
+            TAG,
+            "Channel already resolving with connection ${currentDevice.get()?.protocolId}. " +
+              "Ignoring data received from connection $protocolId."
+          )
         }
       }
-    device.protocol.registerDataReceivedListener(device.protocolId, dataReceivedListener, executor)
+    device.protocol.registerDataReceivedListener(device.protocolId, dataReceivedListener)
     protocolDevices[device] = dataReceivedListener
   }
 
@@ -157,7 +142,7 @@
     }
 
     val resolvedMessageVersion = min(MAX_MESSAGING_VERSION, version.maxSupportedMessagingVersion)
-    resolvedSecurityVersion = min(MAX_SECURITY_VERSION, version.maxSupportedSecurityVersion)
+    val resolvedSecurityVersion = min(MAX_SECURITY_VERSION, version.maxSupportedSecurityVersion)
     logd(
       TAG,
       "Resolved to messaging version $resolvedMessageVersion and security version " +
@@ -170,87 +155,21 @@
         .setMaxSupportedSecurityVersion(MAX_SECURITY_VERSION)
         .setMinSupportedSecurityVersion(MIN_SECURITY_VERSION)
         .build()
-    device.protocol.sendData(device.protocolId, carVersion.toByteArray())
+    device.protocol.sendData(device.protocolId, carVersion.toByteArray(), callback = null)
 
-    if (!isReconnect && resolvedSecurityVersion == SECURITY_VERSION_FOR_CAPABILITIES_EXCHANGE) {
-      logd(TAG, "Waiting for capabilities exchange message.")
-      return
+    if (!isReconnect) {
+      logd(TAG, "Send OOB data to remote device $device.")
+      oobRunner?.sendOobData(device)
     }
 
-    logd(TAG, "Capabilities exchange not supported, resolving stream.")
-    if (!isReconnect && resolvedSecurityVersion > SECURITY_VERSION_FOR_CAPABILITIES_EXCHANGE) {
-      logd(TAG, "Start OOB data exchange.")
-      val supportedChannels =
-        oobRunner?.supportedTypes?.map { it.asOobChannelType() } ?: emptyList<OobChannelType>()
-      oobRunner?.startOobDataExchange(device, supportedChannels, resolvedSecurityVersion)
-    }
     resolveStream(device)
   }
 
-  private fun processCapabilityMessage(message: ByteArray, device: ProtocolDevice) {
-    val oobRunner = this.oobRunner
-    if (oobRunner == null) {
-      logw(TAG, "OOB runner not set during capabilities exchange. Sending empty list.")
-      sendCapabilities(emptyList<OobChannelType>(), device)
-      resolveStream(device)
-      return
-    }
-
-    val capabilities =
-      try {
-        CapabilitiesExchange.parseFrom(message, ExtensionRegistryLite.getEmptyRegistry())
-      } catch (e: InvalidProtocolBufferException) {
-        onError("Received a malformed capabilities exchange message. Disconnecting.", e)
-        return
-      }
-    val sharedOobChannels = capabilities.supportedOobChannelsList.toMutableList()
-    logd(TAG, "Received ${sharedOobChannels.size} OOB channels from device.")
-    val onDeviceChannels = oobRunner.supportedTypes.map { it.asOobChannelType() }
-    logd(TAG, "Car supported ${onDeviceChannels.size} OOB channels.")
-    sharedOobChannels.retainAll(onDeviceChannels.toMutableList())
-    sendCapabilities(sharedOobChannels, device)
-
-    if (!oobRunner.startOobDataExchange(
-        device,
-        sharedOobChannels,
-        resolvedSecurityVersion,
-        generateOobRunnerCallback(device)
-      )
-    ) {
-      logw(TAG, "Failed to start OOB data exchange, fallback to numeric comparison.")
-      resolveStream(device)
-    }
-  }
-
-  private fun generateOobRunnerCallback(device: ProtocolDevice) =
-    object : OobRunner.Callback {
-      override fun onOobDataExchangeSuccess() {
-        logd(TAG, "OOB data exchange completed successfully.")
-        encryptionRunner = newRunner(EncryptionRunnerType.OOB_UKEY2)
-        resolveStream(device)
-      }
-
-      override fun onOobDataExchangeFailure() {
-        logw(TAG, "Failed to exchange OOB data, fallback to numeric comparison.")
-        resolveStream(device)
-      }
-    }
-  private fun sendCapabilities(sharedOobChannels: List<OobChannelType>, device: ProtocolDevice) {
-    logd(TAG, "Found ${sharedOobChannels.size} common OOB channels.")
-    val carCapabilities =
-      CapabilitiesExchange.newBuilder().addAllSupportedOobChannels(sharedOobChannels).build()
-    device.protocol.sendData(device.protocolId, carCapabilities.toByteArray())
-  }
-
-  private fun String.asOobChannelType(): OobChannelType =
-    OobChannelType.values().firstOrNull { it.name.equals(this) }
-      ?: OobChannelType.OOB_CHANNEL_UNKNOWN
-
   private fun resolveStream(device: ProtocolDevice) {
     protocolDevices.remove(device)?.let { listener ->
       device.protocol.unregisterDataReceivedListener(device.protocolId, listener)
     }
-    val stream = streamFactory.createProtocolStream(device, executor)
+    val stream = streamFactory.createProtocolStream(device)
     if (isReconnect && device.protocol.isDeviceVerificationRequired) {
       verifyDevice(stream)
       return
@@ -315,22 +234,7 @@
   private fun resolveChannel(stream: ProtocolStream) {
     encryptionRunner.setIsReconnect(isReconnect)
     val channel =
-      if (resolvedSecurityVersion > SECURITY_VERSION_FOR_CAPABILITIES_EXCHANGE)
-        MultiProtocolSecureChannelV4(
-          stream,
-          storage,
-          encryptionRunner,
-          oobRunner,
-          deviceId?.toString()
-        )
-      else
-        MultiProtocolSecureChannelPreV4(
-          stream,
-          storage,
-          encryptionRunner,
-          oobRunner,
-          deviceId?.toString()
-        )
+      MultiProtocolSecureChannel(stream, storage, encryptionRunner, oobRunner, deviceId?.toString())
     protocolDevices.keys.forEach { channel.addStream(ProtocolStream(it)) }
     clearDataReceivedListeners()
     callback.onChannelResolved(channel)
@@ -361,9 +265,8 @@
   companion object {
     internal const val MIN_MESSAGING_VERSION = 3
     internal const val MAX_MESSAGING_VERSION = 3
-    internal const val MIN_SECURITY_VERSION = 2
+    internal const val MIN_SECURITY_VERSION = 4
     internal const val MAX_SECURITY_VERSION = 4
-    internal const val SECURITY_VERSION_FOR_CAPABILITIES_EXCHANGE = 3
     private const val TAG = "ChannelResolver"
   }
 }
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ConnectionResolver.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ConnectionResolver.java
deleted file mode 100644
index b9c4c99..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ConnectionResolver.java
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
- * Copyright (C) 2021 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 com.google.android.connecteddevice.connection;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static java.lang.Math.min;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange;
-import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange.OobChannelType;
-import com.google.android.companionprotos.VersionExchangeProto.VersionExchange;
-import com.google.auto.value.AutoValue;
-import com.google.common.collect.ImmutableList;
-import com.google.protobuf.ExtensionRegistryLite;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Manages the version and capabilities exchange that must be completed in order to establish a
- * secure channel with a remote device.
- *
- * <p>The {@link ResolvedConnection} result is used to determine which {@code SecureChannel} to use
- * for the association flow.
- */
-public final class ConnectionResolver {
-  private static final String TAG = "ConnectionResolver";
-
-  public static final int MIN_SECURITY_VERSION_FOR_CAPABILITIES_EXCHANGE = 3;
-  // Messaging and security versions are dictated by this device.
-  @VisibleForTesting public static final int MESSAGING_VERSION = 3;
-  @VisibleForTesting public static final int MIN_SECURITY_VERSION = 2;
-  @VisibleForTesting public static final int MAX_SECURITY_VERSION = 3;
-  private final ImmutableList<OobChannelType> supportedOobChannels;
-
-  private enum State {
-    UNKNOWN,
-    WAITING_FOR_VERSION_EXCHANGE,
-    WAITING_FOR_CAPABILITIES_EXCHANGE,
-    DONE
-  };
-
-  private ErrorListener errorListener;
-  private State state = State.WAITING_FOR_VERSION_EXCHANGE;
-  private int messagingVersion;
-  private int securityVersion;
-
-  private final List<ConnectionResolvedListener> listeners;
-  private final DeviceMessageStream deviceMessageStream;
-  private final boolean isCapabilitiesEligible;
-
-  public ConnectionResolver(
-      DeviceMessageStream deviceMessageStream,
-      boolean isCapabilitiesEligible) {
-    this(deviceMessageStream, isCapabilitiesEligible, ImmutableList.of(OobChannelType.BT_RFCOMM));
-  }
-
-  public ConnectionResolver(
-      DeviceMessageStream deviceMessageStream,
-      boolean isCapabilitiesEligible,
-      ImmutableList<OobChannelType> supportedOobChannels) {
-    listeners = new ArrayList<>();
-    this.deviceMessageStream = deviceMessageStream;
-    this.isCapabilitiesEligible = isCapabilitiesEligible;
-    this.supportedOobChannels = supportedOobChannels;
-  }
-
-  private void initializeListeners() {
-    setErrorListener(deviceMessageStream::notifyDataReceivedErrorListener);
-    registerListener((resolvedConnection) -> deviceMessageStream.setConnectionResolved(true));
-
-    logd(TAG, "Setting data received listener");
-    deviceMessageStream.setDataReceivedListener(this::onMessageReceived);
-  }
-
-  public void resolveConnection(@Nullable ConnectionResolvedListener listener) {
-    initializeListeners();
-
-    if (listener != null) {
-      registerListener(listener);
-    }
-  }
-
-  public void registerListener(ConnectionResolvedListener listener) {
-    listeners.add(listener);
-  }
-
-  public void unregisterListener(ConnectionResolvedListener listener) {
-    listeners.remove(listener);
-  }
-
-  public void setErrorListener(@Nullable ErrorListener errorListener) {
-    this.errorListener = errorListener;
-  }
-
-  public void onMessageReceived(byte[] message) {
-    logd(TAG, "Message received for state " + state);
-    switch (state) {
-      case WAITING_FOR_VERSION_EXCHANGE:
-        exchangeVersion(message);
-        break;
-      case WAITING_FOR_CAPABILITIES_EXCHANGE:
-        exchangeCapabilities(message);
-        break;
-      case DONE:
-      case UNKNOWN:
-        notifyError(null);
-    }
-  }
-
-  private void exchangeVersion(byte[] message) {
-    logd(TAG, "Exchanging version.");
-    VersionExchange versionExchange;
-    try {
-      versionExchange =
-          VersionExchange.parseFrom(message, ExtensionRegistryLite.getEmptyRegistry());
-    } catch (IOException e) {
-      loge(TAG, "Could not parse version exchange message", e);
-      notifyError(e);
-
-      return;
-    }
-    int minMessagingVersion = versionExchange.getMinSupportedMessagingVersion();
-    int maxMessagingVersion = versionExchange.getMaxSupportedMessagingVersion();
-    int minSecurityVersion = versionExchange.getMinSupportedSecurityVersion();
-    int maxSecurityVersion = versionExchange.getMaxSupportedSecurityVersion();
-    if (minMessagingVersion > MESSAGING_VERSION
-        || maxMessagingVersion < MESSAGING_VERSION
-        || minSecurityVersion > MAX_SECURITY_VERSION
-        || maxSecurityVersion < MIN_SECURITY_VERSION) {
-      loge(
-          TAG,
-          "Unsupported message version for min "
-              + minMessagingVersion
-              + " and max "
-              + maxMessagingVersion
-              + " or security version for "
-              + minSecurityVersion
-              + " and max "
-              + maxSecurityVersion
-              + ".");
-      notifyError(new IllegalStateException("Unsupported version."));
-      return;
-    }
-
-    VersionExchange headunitVersion =
-        VersionExchange.newBuilder()
-            .setMinSupportedMessagingVersion(MESSAGING_VERSION)
-            .setMaxSupportedMessagingVersion(MESSAGING_VERSION)
-            .setMinSupportedSecurityVersion(MIN_SECURITY_VERSION)
-            .setMaxSupportedSecurityVersion(MAX_SECURITY_VERSION)
-            .build();
-    deviceMessageStream.writeRawBytes(headunitVersion.toByteArray());
-    logd(TAG, "Sent supported versions to the phone.");
-
-    int maxSharedSecurityVersion = min(maxSecurityVersion, MAX_SECURITY_VERSION);
-    messagingVersion = MESSAGING_VERSION;
-    securityVersion = maxSharedSecurityVersion;
-    logd(
-        TAG,
-        "Resolved to messaging v"
-            + MESSAGING_VERSION + " and security v"
-            + maxSharedSecurityVersion
-            + ".");
-
-    if (shouldDoCapabilitiesExchange()) {
-      state = State.WAITING_FOR_CAPABILITIES_EXCHANGE;
-    } else {
-      notifyConnectionResolved(
-          ResolvedConnection.create(
-              messagingVersion, securityVersion, /* oobChannelTypes= */ null));
-    }
-  }
-
-  private boolean shouldDoCapabilitiesExchange() {
-    return isCapabilitiesEligible
-        && securityVersion >= MIN_SECURITY_VERSION_FOR_CAPABILITIES_EXCHANGE;
-  }
-
-  private void exchangeCapabilities(byte[] message) {
-    CapabilitiesExchange capabilitiesExchange;
-    try {
-      capabilitiesExchange =
-          CapabilitiesExchange.parseFrom(message, ExtensionRegistryLite.getEmptyRegistry());
-    } catch (IOException e) {
-      loge(TAG, "Could not parse capabilities exchange message", e);
-      notifyError(e);
-      return;
-    }
-
-    // Must wrap in an ArrayList to prevent retainAll from throwing an
-    // UnsupportedOperationException.
-    List<OobChannelType> sharedSupportedOobChannels =
-        new ArrayList<>(capabilitiesExchange.getSupportedOobChannelsList());
-    logd(
-        TAG,
-        "Received " + sharedSupportedOobChannels.size() + " supported OOB channels from device.");
-    sharedSupportedOobChannels.retainAll(supportedOobChannels);
-    logd(TAG, "Found " + sharedSupportedOobChannels.size() + " common OOB channels.");
-    CapabilitiesExchange headunitCapabilities = CapabilitiesExchange.newBuilder()
-      .addAllSupportedOobChannels(sharedSupportedOobChannels)
-      .build();
-
-    deviceMessageStream.writeRawBytes(headunitCapabilities.toByteArray());
-
-    logd(TAG, "Sent supported capabilities to the phone.");
-
-    notifyConnectionResolved(
-        ResolvedConnection.create(
-            messagingVersion, securityVersion, ImmutableList.copyOf(sharedSupportedOobChannels)));
-  }
-
-  private void notifyConnectionResolved(ResolvedConnection resolvedConnection) {
-    state = State.DONE;
-
-    for (ConnectionResolvedListener listener : listeners) {
-      listener.onConnectionResolved(resolvedConnection);
-    }
-  }
-
-  private void notifyError(@Nullable Exception e) {
-    if (errorListener != null) {
-      errorListener.onError(e);
-    }
-  }
-
-  /** Listener to be invoked when version and capabilities exchange is complete. */
-  public interface ConnectionResolvedListener {
-    void onConnectionResolved(ResolvedConnection resolvedConnection);
-  }
-
-  /** Listener to be invoked when there is an error with the version or capabilities exchange. */
-  public interface ErrorListener {
-    void onError(Exception e);
-  }
-
-  /** Holds the values of the version and capabilities exchange. */
-  @AutoValue
-  public abstract static class ResolvedConnection {
-    public static ResolvedConnection create(
-        int messagingVersion,
-        int securityVersion,
-        @Nullable ImmutableList<OobChannelType> oobChannelTypes) {
-      return new AutoValue_ConnectionResolver_ResolvedConnection(
-          messagingVersion, securityVersion, oobChannelTypes);
-    }
-
-    public abstract int messagingVersion();
-
-    public abstract int securityVersion();
-
-    @Nullable
-    public abstract ImmutableList<OobChannelType> oobChannelTypes();
-  }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/DeviceMessageStream.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/DeviceMessageStream.java
deleted file mode 100644
index 9bed0c5..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/DeviceMessageStream.java
+++ /dev/null
@@ -1,374 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static com.google.android.connecteddevice.util.SafeLog.logw;
-
-import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import com.google.android.companionprotos.DeviceMessageProto.Message;
-import com.google.android.companionprotos.OperationProto.OperationType;
-import com.google.android.companionprotos.PacketProto.Packet;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.android.connecteddevice.util.EventLog;
-import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.ArrayDeque;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/** Abstract class which includes common logic of different types of {@link DeviceMessageStream}. */
-public abstract class DeviceMessageStream {
-
-  private static final String TAG = "DeviceMessageStream";
-
-  private final ArrayDeque<Packet> packetQueue = new ArrayDeque<>();
-
-  private final Map<Integer, ByteArrayOutputStream> pendingData = new HashMap<>();
-
-  // messageId -> nextExpectedPacketNumber
-  private final Map<Integer, Integer> pendingPacketNumber = new HashMap<>();
-
-  private final MessageIdGenerator messageIdGenerator = new MessageIdGenerator();
-
-  private final AtomicBoolean isSendingInProgress = new AtomicBoolean(false);
-
-  // TODO(b/169869570): Remove when Protocol interface is available
-  private final AtomicBoolean isConnectionResolved = new AtomicBoolean(false);
-
-  private int maxWriteSize;
-
-  /** Listener which will be notified when there is new {@link DeviceMessage} received. */
-  private MessageReceivedListener messageReceivedListener;
-
-  private DataReceivedListener dataReceivedListener;
-
-  /** Listener which will be notified when there is error parsing the received message. */
-  private MessageReceivedErrorListener messageReceivedErrorListener;
-
-  protected DeviceMessageStream(int defaultMaxWriteSize) {
-    maxWriteSize = defaultMaxWriteSize;
-  }
-
-  /**
-   * Send data to the connected device. Note: {@link #sendCompleted()} must be called when the bytes
-   * have successfully been sent to indicate the stream is ready to send more data.
-   */
-  protected abstract void send(byte[] data);
-
-  /**
-   * Set the given listener to be notified when a new message was received from the client. If
-   * listener is {@code null}, clear.
-   */
-  public final void setMessageReceivedListener(@Nullable MessageReceivedListener listener) {
-    messageReceivedListener = listener;
-  }
-
-  public final void setDataReceivedListener(@Nullable DataReceivedListener listener) {
-    logd(TAG, "Setting dataReceivedListener");
-    dataReceivedListener = listener;
-  }
-
-  /**
-   * Set the given listener to be notified when there was an error during receiving message from the
-   * client. If listener is {@code null}, clear.
-   */
-  public final void setMessageReceivedErrorListener(
-      @Nullable MessageReceivedErrorListener listener) {
-    messageReceivedErrorListener = listener;
-  }
-
-  public final void setConnectionResolved(boolean isResolved) {
-    isConnectionResolved.set(isResolved);
-  }
-
-  /**
-   * Notify the {@code messageReceivedListener} about the message received if it is not {@code
-   * null}.
-   *
-   * @param deviceMessage The message received.
-   */
-  protected final void notifyMessageReceivedListener(@NonNull DeviceMessage deviceMessage) {
-    if (messageReceivedListener != null) {
-      messageReceivedListener.onMessageReceived(deviceMessage);
-    }
-  }
-
-  // TODO(b/169869570): Remove when Protocol interface is available
-  public final void notifyDataReceivedErrorListener(Exception e) {
-    notifyMessageReceivedErrorListener(e);
-  }
-
-  private void notifyDataReceivedListener(byte[] data) {
-    logd(TAG, "Notifying dataReceivedListener");
-    if (dataReceivedListener != null) {
-      dataReceivedListener.onDataReceived(data);
-    }
-  }
-
-  /**
-   * Notify the {@code messageReceivedErrorListener} about the message received if it is not {@code
-   * null}.
-   *
-   * @param e The exception happened when parsing the received message.
-   */
-  protected final void notifyMessageReceivedErrorListener(Exception e) {
-    if (messageReceivedErrorListener != null) {
-      messageReceivedErrorListener.onMessageReceivedError(e);
-    }
-  }
-
-  /**
-   * Send {@link DeviceMessage} to remote connected devices.
-   *
-   * @param deviceMessage The message which need to be sent
-   */
-  public void writeMessage(@NonNull DeviceMessage deviceMessage) {
-    Message.Builder builder =
-        Message.newBuilder()
-            .setOperation(OperationType.forNumber(deviceMessage.getOperationType().getValue()))
-            .setIsPayloadEncrypted(deviceMessage.isMessageEncrypted())
-            .setPayload(ByteString.copyFrom(deviceMessage.getMessage()))
-            .setOriginalSize(deviceMessage.getOriginalMessageSize());
-
-    UUID recipient = deviceMessage.getRecipient();
-    if (recipient != null) {
-      builder.setRecipient(ByteString.copyFrom(ByteUtils.uuidToBytes(recipient)));
-    }
-
-    Message message = builder.build();
-    byte[] rawBytes = message.toByteArray();
-    List<Packet> packets;
-    try {
-      packets = PacketFactory.makePackets(rawBytes, messageIdGenerator.next(), maxWriteSize);
-    } catch (PacketFactoryException e) {
-      loge(TAG, "Error while creating message packets.", e);
-      return;
-    }
-    packetQueue.addAll(packets);
-    writeNextMessageInQueue();
-  }
-
-  private void writeNextMessageInQueue() {
-    if (packetQueue.isEmpty()) {
-      logd(TAG, "No more packets to send.");
-      return;
-    }
-    boolean isLockAcquired = isSendingInProgress.compareAndSet(false, true);
-    if (!isLockAcquired) {
-      logd(TAG, "Unable to send packet at this time.");
-      return;
-    }
-
-    Packet packet = packetQueue.remove();
-    logd(
-        TAG,
-        "Writing packet "
-            + packet.getPacketNumber()
-            + " of "
-            + packet.getTotalPackets()
-            + " for "
-            + packet.getMessageId()
-            + ".");
-    send(packet.toByteArray());
-  }
-
-  /**
-   * Send raw data to remote connected devices.
-   *
-   * @param data message to send
-   * @throws IllegalArgumentException if {@code data} is larger than {@code maxWriteSize}
-   */
-  // TODO(b/169869570): Move to Protocol interface when available
-  public void writeRawBytes(@NonNull byte[] data) {
-    if (data.length > maxWriteSize) {
-      throw new IllegalArgumentException(
-          "Length of data is greater than max write size, send as a Message instead.");
-    }
-
-    send(data);
-  }
-
-  /** Process incoming data from stream. */
-  protected final synchronized void onDataReceived(byte[] data) {
-    logd(TAG, "Data was received, isConnectionResolved = " + isConnectionResolved.get());
-    if (isConnectionResolved.get()) {
-      Packet packet;
-        try {
-          packet = Packet.parseFrom(data, ExtensionRegistryLite.getEmptyRegistry());
-        } catch (IOException e) {
-          loge(TAG, "Can not parse packet from client.", e);
-          notifyMessageReceivedErrorListener(e);
-          return;
-        }
-        processPacket(packet);
-      return;
-    }
-
-    // The version and capabilities exchange use raw bytes rather than device messages, so notify
-    // raw data listener rather than message listener.
-    notifyDataReceivedListener(data);
-  }
-
-  protected final void processPacket(@NonNull Packet packet) {
-    int messageId = packet.getMessageId();
-    int packetNumber = packet.getPacketNumber();
-    int expectedPacket = pendingPacketNumber.getOrDefault(messageId, 1);
-    if (packetNumber == expectedPacket - 1) {
-      logw(
-          TAG,
-          "Received duplicate packet "
-              + packet.getPacketNumber()
-              + " for message "
-              + messageId
-              + ". Ignoring.");
-      return;
-    }
-    if (packetNumber != expectedPacket) {
-      loge(TAG, "Received unexpected packet " + packetNumber + " for message " + messageId + ".");
-      notifyMessageReceivedErrorListener(
-          new IllegalStateException("Packet received out of order."));
-      return;
-    }
-    pendingPacketNumber.put(messageId, packetNumber + 1);
-
-    ByteArrayOutputStream currentPayloadStream =
-        pendingData.getOrDefault(messageId, new ByteArrayOutputStream());
-    pendingData.putIfAbsent(messageId, currentPayloadStream);
-
-    byte[] payload = packet.getPayload().toByteArray();
-    try {
-      currentPayloadStream.write(payload);
-    } catch (IOException e) {
-      loge(TAG, "Error writing packet to stream.", e);
-      notifyMessageReceivedErrorListener(e);
-      return;
-    }
-    logd(
-        TAG,
-        "Parsed packet "
-            + packet.getPacketNumber()
-            + " of "
-            + packet.getTotalPackets()
-            + " for message "
-            + messageId
-            + ". Writing "
-            + payload.length
-            + ".");
-
-    if (packetNumber == 1) {
-      EventLog.onMessageStarted(messageId);
-    }
-
-    if (packet.getPacketNumber() != packet.getTotalPackets()) {
-      return;
-    }
-
-    byte[] messageBytes = currentPayloadStream.toByteArray();
-    EventLog.onMessageFullyReceived(messageId, messageBytes.length);
-    pendingData.remove(messageId);
-
-    logd(
-        TAG,
-        "Received complete device message " + messageId + " of " + messageBytes.length + " bytes.");
-    Message message;
-    try {
-      message = Message.parseFrom(messageBytes, ExtensionRegistryLite.getEmptyRegistry());
-    } catch (IOException e) {
-      loge(TAG, "Cannot parse device message from client.", e);
-      notifyMessageReceivedErrorListener(e);
-      return;
-    }
-
-    DeviceMessage deviceMessage =
-        DeviceMessage.createIncomingMessage(
-            ByteUtils.bytesToUUID(message.getRecipient().toByteArray()),
-            message.getIsPayloadEncrypted(),
-            DeviceMessage.OperationType.fromValue(message.getOperation().getNumber()),
-            message.getPayload().toByteArray(),
-            message.getOriginalSize());
-    notifyMessageReceivedListener(deviceMessage);
-  }
-
-  /** The maximum amount of bytes that can be written in a single packet. */
-  public final void setMaxWriteSize(@IntRange(from = 1) int maxWriteSize) {
-    if (maxWriteSize <= 0) {
-      return;
-    }
-    this.maxWriteSize = maxWriteSize;
-  }
-
-  /** Indicate current send operation has completed. */
-  public void sendCompleted() {
-    isSendingInProgress.set(false);
-    writeNextMessageInQueue();
-  }
-
-  /** A generator of unique IDs for messages. */
-  private static class MessageIdGenerator {
-    private final AtomicInteger messageId = new AtomicInteger(0);
-
-    int next() {
-      int current = messageId.getAndIncrement();
-      messageId.compareAndSet(Integer.MAX_VALUE, 0);
-      return current;
-    }
-  }
-
-  /** Listener to be invoked when a complete message is received from the client. */
-  public interface MessageReceivedListener {
-
-    /**
-     * Called when a complete message is received from the client.
-     *
-     * @param deviceMessage The message received from the client.
-     */
-    void onMessageReceived(@NonNull DeviceMessage deviceMessage);
-  }
-
-  /** Listener to be invoked when a message of raw data is received from the client. */
-  // TODO(b/169869570): Move to Protocol interface when available
-  public interface DataReceivedListener {
-    /**
-     * Called when a message of raw data is received from the client.
-     *
-     * @param data the data received from the client
-     */
-    void onDataReceived(@NonNull byte[] data);
-  }
-
-  /** Listener to be invoked when there was an error during receiving message from the client. */
-  public interface MessageReceivedErrorListener {
-    /**
-     * Called when there was an error during receiving message from the client.
-     *
-     * @param exception The error.
-     */
-    void onMessageReceivedError(@NonNull Exception exception);
-  }
-
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannel.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannel.kt
index b5b9836..e908d5c 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannel.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannel.kt
@@ -16,6 +16,8 @@
 package com.google.android.connecteddevice.connection
 
 import androidx.annotation.VisibleForTesting
+import com.google.android.companionprotos.VerificationCode
+import com.google.android.companionprotos.VerificationCodeState
 import com.google.android.connecteddevice.model.DeviceMessage
 import com.google.android.connecteddevice.model.DeviceMessage.OperationType
 import com.google.android.connecteddevice.oob.OobRunner
@@ -27,6 +29,7 @@
 import com.google.android.encryptionrunner.HandshakeException
 import com.google.android.encryptionrunner.HandshakeMessage.HandshakeState
 import com.google.android.encryptionrunner.Key
+import com.google.protobuf.ByteString
 import java.security.SignatureException
 import java.util.UUID
 import java.util.concurrent.ConcurrentHashMap
@@ -40,7 +43,7 @@
  * Establishes a secure channel with [EncryptionRunner] over [ProtocolStream]s as server side, sends
  * and receives messages securely after the secure channel has been established.
  */
-abstract class MultiProtocolSecureChannel(
+open class MultiProtocolSecureChannel(
   stream: ProtocolStream,
   private val storage: ConnectedDeviceStorage,
   private val encryptionRunner: EncryptionRunner,
@@ -94,7 +97,7 @@
    * The out of band verification code get from the [EncryptionRunner], will be set during out of
    * band association.
    */
-  protected var oobCode: ByteArray? = null
+  private var oobCode: ByteArray? = null
 
   @HandshakeState private var state: Int = HandshakeState.UNKNOWN
 
@@ -112,7 +115,6 @@
       HandshakeState.UNKNOWN -> processHandshakeInitialization(message)
       HandshakeState.IN_PROGRESS -> processHandshakeInProgress(message)
       HandshakeState.VERIFICATION_NEEDED -> processVerificationCodeMessage(message)
-      HandshakeState.OOB_VERIFICATION_NEEDED -> confirmOobVerificationCode(message)
       HandshakeState.FINISHED ->
         // Should not reach this line.
         loge(TAG, "Received handshake message after handshake is completed. Ignored.")
@@ -152,27 +154,12 @@
         visualVerificationCode = handshakeMessage.verificationCode
         val fullVerificationCode = handshakeMessage.fullVerificationCode
         if (fullVerificationCode != null) {
-          processOobVerificationCode(fullVerificationCode)
+          this.oobCode = fullVerificationCode
         } else {
           loge(TAG, "Full verification is null. Notify channel error")
           notifySecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_STATE)
         }
       }
-      HandshakeState.OOB_VERIFICATION_NEEDED -> {
-        val oobCode = handshakeMessage.fullVerificationCode
-        // Unreachable error, the [oobCode] will always be nonnull if the status is
-        // [OOB_VERIFICATION_NEEDED].
-        if (oobCode == null) {
-          loge(
-            TAG,
-            "Handshake state is OOB_VERIFICATION_NEEDED, but handshake message does " +
-              "not contain out of band verification code. Notifying callback of failure."
-          )
-          notifySecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_VERIFICATION)
-          return
-        }
-        this.oobCode = oobCode
-      }
       else -> {
         loge(
           TAG,
@@ -184,11 +171,19 @@
     }
   }
 
-  /** Will be called when OOB verification code is available from [EncryptionRunner]. */
-  protected abstract fun processOobVerificationCode(oobCode: ByteArray)
-
   /** Process the message received from remote device during verification stage. */
-  protected abstract fun processVerificationCodeMessage(message: ByteArray)
+  private fun processVerificationCodeMessage(message: ByteArray) {
+    val verificationMessage = VerificationCode.parseFrom(message)
+    when (verificationMessage.state) {
+      VerificationCodeState.OOB_VERIFICATION ->
+        confirmOobVerificationCode(verificationMessage.payload.toByteArray())
+      VerificationCodeState.VISUAL_VERIFICATION -> invokeVerificationCodeListener()
+      else -> {
+        loge(TAG, "Unexpected verification message received, issue error callback.")
+        notifySecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_STATE)
+      }
+    }
+  }
 
   /** Should be called when OOB verification code is received from remote device. */
   protected fun confirmOobVerificationCode(encryptedCode: ByteArray) {
@@ -229,7 +224,14 @@
   }
 
   /** Generate understandable OOB data response which will be sent to remote device. */
-  protected abstract fun createOobResponse(code: ByteArray): ByteArray
+  private fun createOobResponse(code: ByteArray) =
+    VerificationCode.newBuilder()
+      .run {
+        setState(VerificationCodeState.OOB_VERIFICATION)
+        setPayload(ByteString.copyFrom(code))
+        build()
+      }
+      .toByteArray()
 
   /** Communicate with other components that the visual verification code is available. */
   protected fun invokeVerificationCodeListener() {
@@ -313,7 +315,12 @@
    * confirmation, and send confirmation signals to remote bluetooth device.
    */
   open fun notifyVerificationCodeAccepted() {
-    processVisualVerificationConfirmed()
+    val confirmationMessage =
+      VerificationCode.newBuilder().run {
+        setState(VerificationCodeState.VISUAL_CONFIRMATION)
+        build()
+      }
+    sendHandshakeMessage(confirmationMessage.toByteArray())
     verificationCodeAcceptedInternal()
   }
 
@@ -348,9 +355,6 @@
     notifyCallback { it.onSecureChannelEstablished() }
   }
 
-  /** A confirmation message should be sent to remote device according to the version. */
-  protected abstract fun processVisualVerificationConfirmed()
-
   /** Add a protocol stream to this channel. */
   fun addStream(stream: ProtocolStream) {
     streams.add(stream)
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelPreV4.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelPreV4.kt
deleted file mode 100644
index 9d23d06..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelPreV4.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2021 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 com.google.android.connecteddevice.connection
-
-import com.google.android.connecteddevice.oob.OobRunner
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
-import com.google.android.connecteddevice.util.SafeLog.loge
-import com.google.android.encryptionrunner.EncryptionRunner
-import java.util.zip.Inflater
-
-/** Handles specific handshake logic before security V4. */
-open class MultiProtocolSecureChannelPreV4(
-  stream: ProtocolStream,
-  storage: ConnectedDeviceStorage,
-  encryptionRunner: EncryptionRunner,
-  oobRunner: OobRunner? = null,
-  deviceId: String? = null,
-  inflater: Inflater = Inflater(),
-) : MultiProtocolSecureChannel(stream, storage, encryptionRunner, oobRunner, deviceId, inflater) {
-
-  override fun processOobVerificationCode(oobCode: ByteArray) {
-    invokeVerificationCodeListener()
-  }
-
-  override fun processVerificationCodeMessage(message: ByteArray) {
-    loge(
-      TAG,
-      "Verification code message will only be received under security version 4. notify callback" +
-        " of failure."
-    )
-    notifySecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_STATE)
-  }
-
-  override fun createOobResponse(code: ByteArray): ByteArray {
-    // Raw bytes of the OOB code is expected by remote device before V4.
-    return code
-  }
-
-  override fun processVisualVerificationConfirmed() {
-    // Verification confirmation message is not needed before V4.
-  }
-
-  companion object {
-    private const val TAG = "MultiProtocolSecureChannelPreV4"
-  }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelV4.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelV4.kt
deleted file mode 100644
index f2f69e4..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelV4.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2021 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 com.google.android.connecteddevice.connection
-
-import com.google.android.companionprotos.VerificationCode
-import com.google.android.companionprotos.VerificationCodeState
-import com.google.android.connecteddevice.oob.OobRunner
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
-import com.google.android.connecteddevice.util.SafeLog.logd
-import com.google.android.connecteddevice.util.SafeLog.loge
-import com.google.android.encryptionrunner.EncryptionRunner
-import com.google.protobuf.ByteString
-
-/** Handles specific handshake logic for security V4 and plus. */
-class MultiProtocolSecureChannelV4(
-  stream: ProtocolStream,
-  storage: ConnectedDeviceStorage,
-  encryptionRunner: EncryptionRunner,
-  oobRunner: OobRunner? = null,
-  deviceId: String? = null,
-) : MultiProtocolSecureChannel(stream, storage, encryptionRunner, oobRunner, deviceId) {
-  override fun processVerificationCodeMessage(message: ByteArray) {
-    val verificationMessage = VerificationCode.parseFrom(message)
-    when (verificationMessage.state) {
-      VerificationCodeState.OOB_VERIFICATION ->
-        confirmOobVerificationCode(verificationMessage.payload.toByteArray())
-      VerificationCodeState.VISUAL_VERIFICATION -> invokeVerificationCodeListener()
-      else -> {
-        loge(TAG, "Unexpected verification message received, issue error callback.")
-        notifySecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_STATE)
-      }
-    }
-  }
-
-  override fun createOobResponse(code: ByteArray) =
-    VerificationCode.newBuilder()
-      .run {
-        setState(VerificationCodeState.OOB_VERIFICATION)
-        setPayload(ByteString.copyFrom(code))
-        build()
-      }
-      .toByteArray()
-
-  override fun processVisualVerificationConfirmed() {
-    val confirmationMessage =
-      VerificationCode.newBuilder().run {
-        setState(VerificationCodeState.VISUAL_CONFIRMATION)
-        build()
-      }
-    sendHandshakeMessage(confirmationMessage.toByteArray())
-  }
-
-  override fun processOobVerificationCode(oobCode: ByteArray) {
-    this.oobCode = oobCode
-    logd(TAG, "Waiting for verification code message.")
-  }
-
-  companion object {
-    private const val TAG = "MultiProtocolSecureChannelV4"
-  }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/OobAssociationSecureChannel.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/OobAssociationSecureChannel.java
deleted file mode 100644
index 957c3e1..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/OobAssociationSecureChannel.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection;
-
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import androidx.annotation.NonNull;
-import com.google.android.connecteddevice.oob.OobConnectionManager;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.encryptionrunner.EncryptionRunner;
-import com.google.android.encryptionrunner.EncryptionRunnerFactory;
-import com.google.android.encryptionrunner.HandshakeException;
-import com.google.android.encryptionrunner.HandshakeMessage;
-import com.google.android.encryptionrunner.HandshakeMessage.HandshakeState;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.util.Arrays;
-import javax.crypto.BadPaddingException;
-import javax.crypto.IllegalBlockSizeException;
-
-/** A secure channel established with the association flow with an out-of-band verification. */
-public class OobAssociationSecureChannel extends AssociationSecureChannel {
-
-  private static final String TAG = "OobAssociationSecureChannel";
-
-  private final OobConnectionManager oobConnectionManager;
-
-  private byte[] oobCode;
-
-  public OobAssociationSecureChannel(
-      DeviceMessageStream stream,
-      ConnectedDeviceStorage storage,
-      OobConnectionManager oobConnectionManager) {
-    this(
-        stream,
-        storage,
-        oobConnectionManager,
-        EncryptionRunnerFactory.newRunner(EncryptionRunnerFactory.EncryptionRunnerType.OOB_UKEY2));
-  }
-
-  OobAssociationSecureChannel(
-      DeviceMessageStream stream,
-      ConnectedDeviceStorage storage,
-      OobConnectionManager oobConnectionManager,
-      EncryptionRunner encryptionRunner) {
-    super(stream, storage, encryptionRunner);
-    this.oobConnectionManager = oobConnectionManager;
-  }
-
-  @Override
-  void processHandshake(@NonNull byte[] message) throws HandshakeException {
-    switch (getState()) {
-      case HandshakeState.IN_PROGRESS:
-        processHandshakeInProgress(message);
-        break;
-      case HandshakeState.OOB_VERIFICATION_NEEDED:
-        processHandshakeOobVerificationNeeded(message);
-        break;
-      default:
-        super.processHandshake(message);
-    }
-  }
-
-  private void processHandshakeInProgress(@NonNull byte[] message) throws HandshakeException {
-    HandshakeMessage handshakeMessage = getEncryptionRunner().continueHandshake(message);
-    setState(handshakeMessage.getHandshakeState());
-    int state = getState();
-    if (state != HandshakeState.OOB_VERIFICATION_NEEDED) {
-      loge(
-          TAG,
-          "processHandshakeInProgress: Encountered unexpected handshake state: " + state + ".");
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
-      return;
-    }
-
-    oobCode = handshakeMessage.getFullVerificationCode();
-    if (oobCode == null) {
-      loge(TAG, "Unable to get out of band verification code.");
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
-      return;
-    }
-  }
-
-  private void processHandshakeOobVerificationNeeded(@NonNull byte[] message) {
-    byte[] decryptedCode;
-    try {
-      decryptedCode = oobConnectionManager.decryptVerificationCode(message);
-    } catch (InvalidKeyException
-        | InvalidAlgorithmParameterException
-        | IllegalBlockSizeException
-        | BadPaddingException e) {
-      loge(TAG, "Decryption failed for verification code exchange", e);
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE);
-      return;
-    }
-
-    if (oobCode == null || !Arrays.equals(oobCode, decryptedCode)) {
-      loge(TAG, "Exchanged verification codes do not match. Aborting secure channel.");
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
-      return;
-    }
-
-    byte[] encryptedCode;
-    try {
-      encryptedCode = oobConnectionManager.encryptVerificationCode(oobCode);
-    } catch (InvalidKeyException
-        | InvalidAlgorithmParameterException
-        | IllegalBlockSizeException
-        | BadPaddingException e) {
-      loge(TAG, "Encryption failed for verification code exchange.", e);
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE);
-      return;
-    }
-
-    loge(TAG, "OOB accespted send encrpted code.");
-    sendHandshakeMessage(encryptedCode, /* isEncrypted= */ false);
-
-    notifyOutOfBandAccepted();
-  }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ProtocolStream.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ProtocolStream.kt
index b489d35..349e9a8 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ProtocolStream.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ProtocolStream.kt
@@ -19,7 +19,10 @@
 import com.google.android.companionprotos.OperationProto.OperationType
 import com.google.android.companionprotos.PacketProto.Packet
 import com.google.android.connecteddevice.model.DeviceMessage
-import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataReceivedListener
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDeviceDisconnectedListener
+import com.google.android.connecteddevice.transport.IDeviceMaxDataSizeChangedListener
 import com.google.android.connecteddevice.transport.ProtocolDevice
 import com.google.android.connecteddevice.util.ByteUtils
 import com.google.android.connecteddevice.util.EventLog.onMessageFullyReceived
@@ -32,18 +35,11 @@
 import java.io.ByteArrayOutputStream
 import java.io.IOException
 import java.util.ArrayDeque
-import java.util.concurrent.Executor
-import java.util.concurrent.Executors
 import java.util.concurrent.atomic.AtomicBoolean
 import java.util.concurrent.atomic.AtomicInteger
 
-/**
- * Data stream for a specified [protocol] and its corresponding device identified with [protocolId].
- */
-open class ProtocolStream(
-  private val device: ProtocolDevice,
-  callbackExecutor: Executor = Executors.newSingleThreadExecutor()
-) {
+/** Data stream for a specified [device]. */
+open class ProtocolStream(private val device: ProtocolDevice) {
   /** Listener which will be notified when there is new [DeviceMessage] received. */
   var messageReceivedListener: MessageReceivedListener? = null
 
@@ -69,31 +65,28 @@
     logd(TAG, "Creating new ProtocolStream for protocol device ${device.protocolId}.")
     device.protocol.registerDataReceivedListener(
       device.protocolId,
-      object : ConnectionProtocol.DataReceivedListener {
+      object : IDataReceivedListener.Stub() {
         override fun onDataReceived(protocolId: String, data: ByteArray) {
           onDataReceived(data)
         }
-      },
-      callbackExecutor
+      }
     )
     device.protocol.registerDeviceDisconnectedListener(
       device.protocolId,
-      object : ConnectionProtocol.DeviceDisconnectedListener {
+      object : IDeviceDisconnectedListener.Stub() {
         override fun onDeviceDisconnected(protocolId: String) {
           isConnected.set(false)
           protocolDisconnectListener?.onProtocolDisconnected()
         }
-      },
-      callbackExecutor
+      }
     )
     device.protocol.registerDeviceMaxDataSizeChangedListener(
       device.protocolId,
-      object : ConnectionProtocol.DeviceMaxDataSizeChangedListener {
+      object : IDeviceMaxDataSizeChangedListener.Stub() {
         override fun onDeviceMaxDataSizeChanged(protocolId: String, maxBytes: Int) {
           maxWriteSize = maxBytes
         }
-      },
-      callbackExecutor
+      }
     )
     maxWriteSize = device.protocol.getMaxWriteSize(device.protocolId)
   }
@@ -107,7 +100,7 @@
     device.protocol.sendData(
       device.protocolId,
       data,
-      object : ConnectionProtocol.DataSendCallback {
+      object : IDataSendCallback.Stub() {
         override fun onDataSentSuccessfully() {
           logd(TAG, "Data sent successfully. Sending next message in queue.")
           isSendingInProgress.set(false)
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ProtocolStreamFactory.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ProtocolStreamFactory.kt
index 738ce09..9bdadfa 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ProtocolStreamFactory.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ProtocolStreamFactory.kt
@@ -16,18 +16,16 @@
 package com.google.android.connecteddevice.connection
 
 import com.google.android.connecteddevice.transport.ProtocolDevice
-import java.util.concurrent.Executor
 
 /** Factory for creating [ProtocolStream]. */
 interface ProtocolStreamFactory {
 
   /** Creates a [ProtocolStream] with provided [device]. */
-  fun createProtocolStream(device: ProtocolDevice, executor: Executor): ProtocolStream
+  fun createProtocolStream(device: ProtocolDevice): ProtocolStream
 }
 
 /** Implementation of [ProtocolStreamFactory]. */
 class ProtocolStreamFactoryImpl : ProtocolStreamFactory {
 
-  override fun createProtocolStream(device: ProtocolDevice, executor: Executor): ProtocolStream =
-    ProtocolStream(device, executor)
+  override fun createProtocolStream(device: ProtocolDevice): ProtocolStream = ProtocolStream(device)
 }
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ReconnectSecureChannel.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ReconnectSecureChannel.java
deleted file mode 100644
index abf7b2d..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ReconnectSecureChannel.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.android.connecteddevice.util.EventLog;
-import com.google.android.encryptionrunner.EncryptionRunner;
-import com.google.android.encryptionrunner.EncryptionRunnerFactory;
-import com.google.android.encryptionrunner.EncryptionRunnerFactory.EncryptionRunnerType;
-import com.google.android.encryptionrunner.HandshakeException;
-import com.google.android.encryptionrunner.HandshakeMessage;
-import com.google.android.encryptionrunner.HandshakeMessage.HandshakeState;
-import com.google.android.encryptionrunner.Key;
-import java.util.Arrays;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/** A secure channel established with the reconnection flow. */
-public class ReconnectSecureChannel extends SecureChannel {
-
-  private static final String TAG = "ReconnectSecureChannel";
-
-  private final ConnectedDeviceStorage storage;
-
-  private final String deviceId;
-
-  private final byte[] expectedChallengeResponse;
-
-  private final AtomicBoolean hasVerifiedDevice = new AtomicBoolean(false);
-
-  @HandshakeState private int state = HandshakeState.UNKNOWN;
-
-
-  /**
-   * Create a new secure reconnection channel.
-   *
-   * @param stream The {@link DeviceMessageStream} for communication with the device.
-   * @param storage {@link ConnectedDeviceStorage} for secure storage.
-   * @param deviceId Id of the device being reconnected.
-   * @param expectedChallengeResponse Expected response to challenge issued in reconnect. Should
-   *     pass {@code null} when device verification is not needed during the reconnection process.
-   */
-  public ReconnectSecureChannel(
-      @NonNull DeviceMessageStream stream,
-      @NonNull ConnectedDeviceStorage storage,
-      @NonNull String deviceId,
-      @Nullable byte[] expectedChallengeResponse) {
-    super(stream, newReconnectRunner());
-    this.storage = storage;
-    this.deviceId = deviceId;
-    if (expectedChallengeResponse == null) {
-      // Skip the device verification step for spp reconnection
-      hasVerifiedDevice.set(true);
-    }
-    this.expectedChallengeResponse = expectedChallengeResponse;
-  }
-
-  private static EncryptionRunner newReconnectRunner() {
-    EncryptionRunner encryptionRunner =
-        EncryptionRunnerFactory.newRunner(EncryptionRunnerType.UKEY2);
-    encryptionRunner.setIsReconnect(true);
-    return encryptionRunner;
-  }
-
-  @Override
-  void processHandshake(byte[] message) throws HandshakeException {
-    switch (state) {
-      case HandshakeState.UNKNOWN:
-        if (!hasVerifiedDevice.get()) {
-          processHandshakeDeviceVerification(message);
-        } else {
-          processHandshakeInitialization(message);
-        }
-        break;
-      case HandshakeState.IN_PROGRESS:
-        processHandshakeInProgress(message);
-        break;
-      case HandshakeState.RESUMING_SESSION:
-        processHandshakeResumingSession(message);
-        break;
-      default:
-        loge(TAG, "Encountered unexpected handshake state: " + state + ".");
-        notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
-    }
-  }
-
-  private void processHandshakeDeviceVerification(byte[] message) {
-    EventLog.onDeviceChallengeReceived();
-    byte[] challengeResponse = Arrays.copyOf(message, expectedChallengeResponse.length);
-    byte[] deviceChallenge =
-        Arrays.copyOfRange(message, expectedChallengeResponse.length, message.length);
-    if (!Arrays.equals(expectedChallengeResponse, challengeResponse)) {
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
-      return;
-    }
-    logd(TAG, "Responding to challenge " + ByteUtils.byteArrayToHexString(deviceChallenge) + ".");
-    byte[] deviceChallengeResponse = storage.hashWithChallengeSecret(deviceId, deviceChallenge);
-    if (deviceChallengeResponse == null) {
-      notifySecureChannelFailure(CHANNEL_ERROR_STORAGE_ERROR);
-    }
-    sendHandshakeMessage(deviceChallengeResponse, /* isEncrypted= */ false);
-    hasVerifiedDevice.set(true);
-  }
-
-  private void processHandshakeInitialization(byte[] message) throws HandshakeException {
-    logd(TAG, "Responding to handshake init request.");
-    HandshakeMessage handshakeMessage = getEncryptionRunner().respondToInitRequest(message);
-    state = handshakeMessage.getHandshakeState();
-    sendHandshakeMessage(handshakeMessage.getNextMessage(), /* isEncrypted= */ false);
-  }
-
-  private void processHandshakeInProgress(@NonNull byte[] message) throws HandshakeException {
-    logd(TAG, "Continuing handshake.");
-    HandshakeMessage handshakeMessage = getEncryptionRunner().continueHandshake(message);
-    state = handshakeMessage.getHandshakeState();
-  }
-
-  private void processHandshakeResumingSession(@NonNull byte[] message) throws HandshakeException {
-    logd(TAG, "Start reconnection authentication.");
-
-    byte[] previousKey = storage.getEncryptionKey(deviceId);
-    if (previousKey == null) {
-      loge(TAG, "Unable to resume session, previous key is null.");
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
-      return;
-    }
-
-    HandshakeMessage handshakeMessage =
-        getEncryptionRunner().authenticateReconnection(message, previousKey);
-    state = handshakeMessage.getHandshakeState();
-    if (state != HandshakeState.FINISHED) {
-      loge(TAG, "Unable to resume session, unexpected next handshake state: " + state + ".");
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
-      return;
-    }
-
-    Key newKey = handshakeMessage.getKey();
-    if (newKey == null) {
-      loge(TAG, "Unable to resume session, new key is null.");
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
-      return;
-    }
-
-    storage.saveEncryptionKey(deviceId, newKey.asBytes());
-    logd(TAG, "Saved new key for reconnection.");
-    setEncryptionKey(newKey);
-    sendServerAuthToClient(handshakeMessage.getNextMessage());
-    notifyCallback(Callback::onSecureChannelEstablished);
-  }
-
-  private void sendServerAuthToClient(@Nullable byte[] message) {
-    if (message == null) {
-      loge(TAG, "Unable to send server authentication message to client, message is null.");
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_MSG);
-      return;
-    }
-
-    sendHandshakeMessage(message, /* isEncrypted= */ false);
-  }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/SecureChannel.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/SecureChannel.java
deleted file mode 100644
index deff7f9..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/SecureChannel.java
+++ /dev/null
@@ -1,380 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection;
-
-import static com.google.android.connecteddevice.model.DeviceMessage.OperationType.ENCRYPTION_HANDSHAKE;
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.model.DeviceMessage.OperationType;
-import com.google.android.connecteddevice.util.SafeConsumer;
-import com.google.android.encryptionrunner.EncryptionRunner;
-import com.google.android.encryptionrunner.HandshakeException;
-import com.google.android.encryptionrunner.Key;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.security.SignatureException;
-import java.util.Arrays;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.zip.DataFormatException;
-import java.util.zip.Deflater;
-import java.util.zip.Inflater;
-
-/**
- * Establishes a secure channel with {@link EncryptionRunner} over {@link DeviceMessageStream} as
- * server side, sends and receives messages securely after the secure channel has been established.
- */
-public abstract class SecureChannel {
-
-  private static final String TAG = "SecureChannel";
-
-  @Retention(RetentionPolicy.SOURCE)
-  @IntDef({
-    CHANNEL_ERROR_INVALID_HANDSHAKE,
-    CHANNEL_ERROR_INVALID_MSG,
-    CHANNEL_ERROR_INVALID_DEVICE_ID,
-    CHANNEL_ERROR_INVALID_VERIFICATION,
-    CHANNEL_ERROR_INVALID_STATE,
-    CHANNEL_ERROR_INVALID_ENCRYPTION_KEY,
-    CHANNEL_ERROR_STORAGE_ERROR
-  })
-  @interface ChannelError {}
-
-  /** Indicates an error during a Handshake of EncryptionRunner. */
-  static final int CHANNEL_ERROR_INVALID_HANDSHAKE = 0;
-  /** Received an invalid handshake message or has an invalid handshake message to send. */
-  static final int CHANNEL_ERROR_INVALID_MSG = 1;
-  /** Unable to retrieve a valid id. */
-  static final int CHANNEL_ERROR_INVALID_DEVICE_ID = 2;
-  /** Unable to get verification code or there's a error during pin verification. */
-  static final int CHANNEL_ERROR_INVALID_VERIFICATION = 3;
-  /** Encountered an unexpected handshake state. */
-  static final int CHANNEL_ERROR_INVALID_STATE = 4;
-  /** Failed to get a valid previous/new encryption key. */
-  static final int CHANNEL_ERROR_INVALID_ENCRYPTION_KEY = 5;
-  /** Failed to save or retrieve security keys. */
-  static final int CHANNEL_ERROR_STORAGE_ERROR = 6;
-
-  private final DeviceMessageStream stream;
-
-  private final EncryptionRunner encryptionRunner;
-
-  private final AtomicReference<Key> encryptionKey = new AtomicReference<>();
-
-  private final Inflater inflater;
-
-  private final Deflater deflater;
-
-  private boolean isCompressionEnabled = true;
-
-  private Callback callback;
-
-  SecureChannel(@NonNull DeviceMessageStream stream, @NonNull EncryptionRunner encryptionRunner) {
-    this(stream, encryptionRunner, new Inflater(), new Deflater(Deflater.BEST_COMPRESSION));
-  }
-
-  SecureChannel(
-      @NonNull DeviceMessageStream stream,
-      @NonNull EncryptionRunner encryptionRunner,
-      @NonNull Inflater inflater,
-      @NonNull Deflater deflater) {
-    this.stream = stream;
-    this.encryptionRunner = encryptionRunner;
-    this.stream.setMessageReceivedListener(this::onMessageReceived);
-    this.inflater = inflater;
-    this.deflater = deflater;
-  }
-
-  /** Logic for processing a handshake message from device. */
-  abstract void processHandshake(byte[] message) throws HandshakeException;
-
-  /**
-   * Sets whether outgoing messages will attempt to be compressed prior to sending. Default value is
-   * {@code true}.
-   */
-  public void setCompressionEnabled(boolean enabled) {
-    isCompressionEnabled = enabled;
-  }
-
-  void sendHandshakeMessage(@Nullable byte[] message, boolean isEncrypted) {
-    if (message == null) {
-      loge(TAG, "Unable to send next handshake message, message is null.");
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_MSG);
-      return;
-    }
-
-    logd(TAG, "Sending handshake message.");
-    DeviceMessage deviceMessage =
-        DeviceMessage.createOutgoingMessage(
-            /* recipient= */ null, isEncrypted, ENCRYPTION_HANDSHAKE, message);
-    sendMessage(deviceMessage);
-  }
-
-  /** Set the encryption key that secures this channel. */
-  void setEncryptionKey(@Nullable Key encryptionKey) {
-    this.encryptionKey.set(encryptionKey);
-  }
-
-  /**
-   * Send a client message.
-   *
-   * <p>Note: This should be called with an encrypted message only after the secure channel has been
-   * established.
-   *
-   * @param deviceMessage The {@link DeviceMessage} to send.
-   */
-  public void sendClientMessage(@NonNull DeviceMessage deviceMessage) {
-    sendMessage(deviceMessage);
-  }
-
-  private void sendMessage(@NonNull DeviceMessage deviceMessage) {
-    if (isCompressionEnabled) {
-      compressMessage(deviceMessage);
-    }
-    if (deviceMessage.isMessageEncrypted()) {
-      encryptMessage(deviceMessage);
-    }
-    stream.writeMessage(deviceMessage);
-  }
-
-  private void encryptMessage(@NonNull DeviceMessage deviceMessage) {
-    Key key = encryptionKey.get();
-    if (key == null) {
-      throw new IllegalStateException("Secure channel has not been established.");
-    }
-
-    byte[] encryptedMessage = key.encryptData(deviceMessage.getMessage());
-    deviceMessage.setMessage(encryptedMessage);
-  }
-
-  /** Get the BLE stream backing this channel. */
-  @NonNull
-  public DeviceMessageStream getStream() {
-    return stream;
-  }
-
-  /** Register a callback that notifies secure channel events. */
-  public void registerCallback(Callback callback) {
-    this.callback = callback;
-  }
-
-  /** Unregister a callback. */
-  void unregisterCallback(Callback callback) {
-    if (callback == this.callback) {
-      this.callback = null;
-    }
-  }
-
-  @VisibleForTesting
-  @Nullable
-  public Callback getCallback() {
-    return callback;
-  }
-
-  void notifyCallback(@NonNull SafeConsumer<Callback> notification) {
-    if (callback != null) {
-      notification.accept(callback);
-    }
-  }
-
-  /** Notify callbacks that an error has occurred. */
-  void notifySecureChannelFailure(@ChannelError int error) {
-    loge(TAG, "Secure channel error: " + error);
-    notifyCallback(callback -> callback.onEstablishSecureChannelFailure(error));
-  }
-
-  /** Return the {@link EncryptionRunner} for this channel. */
-  @NonNull
-  EncryptionRunner getEncryptionRunner() {
-    return encryptionRunner;
-  }
-
-  /**
-   * Process the inner message and replace with decrypted value if necessary. If an error occurs the
-   * inner message will be replaced with {@code null} and call {@link
-   * Callback#onMessageReceivedError(Exception)} on the registered callback.
-   *
-   * @param deviceMessage The message to process.
-   * @return {@code true} if message was successfully processed. {@code false} if an error occurred.
-   */
-  @VisibleForTesting
-  boolean processMessage(@NonNull DeviceMessage deviceMessage) {
-    if (!deviceMessage.isMessageEncrypted()) {
-      logd(TAG, "Message was not encrypted. No further action necessary.");
-      return true;
-    }
-    Key key = encryptionKey.get();
-    if (key == null) {
-      loge(TAG, "Received encrypted message before secure channel has been established.");
-      notifyCallback(callback -> callback.onMessageReceivedError(null));
-      deviceMessage.setMessage(null);
-      return false;
-    }
-    try {
-      byte[] decryptedMessage = key.decryptData(deviceMessage.getMessage());
-      deviceMessage.setMessage(decryptedMessage);
-      logd(TAG, "Decrypted secure message.");
-      return true;
-    } catch (SignatureException e) {
-      loge(TAG, "Could not decrypt secure message.", e);
-      notifyCallback(callback -> callback.onMessageReceivedError(e));
-      deviceMessage.setMessage(null);
-
-      return false;
-    }
-  }
-
-  @VisibleForTesting
-  void onMessageReceived(@NonNull DeviceMessage deviceMessage) {
-    boolean success = processMessage(deviceMessage);
-    if (success) {
-      success = decompressMessage(deviceMessage);
-    }
-    OperationType operationType = deviceMessage.getOperationType();
-    switch (operationType) {
-      case ENCRYPTION_HANDSHAKE:
-        if (!success) {
-          notifyCallback(
-              callback ->
-                  callback.onEstablishSecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE));
-          break;
-        }
-        logd(TAG, "Received handshake message.");
-        try {
-          processHandshake(deviceMessage.getMessage());
-        } catch (HandshakeException e) {
-          loge(TAG, "Handshake failed.", e);
-          notifyCallback(
-              callback ->
-                  callback.onEstablishSecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE));
-        }
-        break;
-      case CLIENT_MESSAGE:
-      case QUERY:
-      case QUERY_RESPONSE:
-        if (!success || deviceMessage.getMessage() == null) {
-          break;
-        }
-        logd(TAG, "Received client message.");
-        notifyCallback(callback -> callback.onMessageReceived(deviceMessage));
-        break;
-      default:
-        loge(TAG, "Received unexpected operation type: " + operationType.name() + ".");
-    }
-  }
-
-  @VisibleForTesting
-  void compressMessage(@NonNull DeviceMessage deviceMessage) {
-    byte[] originalMessage = deviceMessage.getMessage();
-    byte[] compressedMessage = new byte[originalMessage.length];
-    deflater.reset();
-    deflater.setInput(originalMessage);
-    deflater.finish();
-    int compressedSize = deflater.deflate(compressedMessage);
-    if (compressedSize >= originalMessage.length) {
-      logd(TAG, "Message compression resulted in no savings. Sending original message.");
-      deviceMessage.setOriginalMessageSize(0);
-      return;
-    }
-    deviceMessage.setOriginalMessageSize(originalMessage.length);
-    deviceMessage.setMessage(Arrays.copyOf(compressedMessage, compressedSize));
-    long compressionSavings =
-        Math.round((originalMessage.length - compressedSize) / (double) originalMessage.length)
-            * 100L;
-    logd(
-        TAG,
-        "Message compressed from "
-            + originalMessage.length
-            + " to "
-            + compressedSize
-            + " bytes saving "
-            + compressionSavings
-            + "%.");
-  }
-
-  @VisibleForTesting
-  boolean decompressMessage(@NonNull DeviceMessage deviceMessage) {
-    byte[] message = deviceMessage.getMessage();
-    int originalMessageSize = deviceMessage.getOriginalMessageSize();
-    if (originalMessageSize == 0) {
-      logd(TAG, "Incoming message was not compressed. No further action necessary.");
-      return true;
-    }
-    inflater.reset();
-    inflater.setInput(message);
-    byte[] decompressedMessage = new byte[originalMessageSize];
-    try {
-      inflater.inflate(decompressedMessage);
-      deviceMessage.setMessage(decompressedMessage);
-    } catch (DataFormatException e) {
-      loge(TAG, "An error occurred while decompressing the message.", e);
-      notifySecureChannelFailure(CHANNEL_ERROR_INVALID_MSG);
-      return false;
-    }
-    logd(
-        TAG,
-        "Message successfully decompressed from "
-            + message.length
-            + " to "
-            + originalMessageSize
-            + " bytes.");
-    return true;
-  }
-
-  /**
-   * Callbacks that will be invoked during establishing secure channel, sending and receiving
-   * messages securely.
-   */
-  public interface Callback {
-    /** Invoked when secure channel has been established successfully. */
-    default void onSecureChannelEstablished() {}
-
-    /**
-     * Invoked when a {@link ChannelError} has been encountered in attempting to establish a secure
-     * channel.
-     *
-     * @param error The failure indication.
-     */
-    default void onEstablishSecureChannelFailure(@SecureChannel.ChannelError int error) {}
-
-    /**
-     * Invoked when a complete message is received securely from the client and decrypted.
-     *
-     * @param deviceMessage The {@link DeviceMessage} with decrypted message.
-     */
-    default void onMessageReceived(@NonNull DeviceMessage deviceMessage) {}
-
-    /**
-     * Invoked when there was an error during a processing or decrypting of a client message.
-     *
-     * @param exception The error.
-     */
-    default void onMessageReceivedError(@Nullable Exception exception) {}
-
-    /**
-     * Invoked when the device id was received from the client.
-     *
-     * @param deviceId The unique device id of client.
-     */
-    default void onDeviceIdReceived(@NonNull String deviceId) {}
-  }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/BleCentralManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/BleCentralManager.java
deleted file mode 100644
index 5b78381..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/BleCentralManager.java
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection.ble;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static com.google.android.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.le.BluetoothLeScanner;
-import android.bluetooth.le.ScanCallback;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.Handler;
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/** Class that manages BLE scanning operations. */
-public class BleCentralManager {
-
-  private static final String TAG = "BleCentralManager";
-
-  private static final int RETRY_LIMIT = 5;
-
-  private static final int RETRY_INTERVAL_MS = 1000;
-
-  private final Context context;
-
-  private final Handler handler;
-
-  private final AtomicInteger scannerState = new AtomicInteger(STOPPED);
-
-  private final BluetoothManager bluetoothManager;
-
-  private List<ScanFilter> scanFilters;
-
-  private ScanSettings scanSettings;
-
-  private ScanCallback scanCallback;
-
-  private BluetoothLeScanner scanner;
-
-  private int scannerStartCount = 0;
-
-
-  @Retention(RetentionPolicy.SOURCE)
-  @IntDef({STOPPED, STARTED, SCANNING})
-  private @interface ScannerState {}
-
-  private static final int STOPPED = 0;
-  private static final int STARTED = 1;
-  private static final int SCANNING = 2;
-
-  public BleCentralManager(@NonNull Context context) {
-    this.context = context;
-    handler = new Handler(context.getMainLooper());
-    bluetoothManager = context.getSystemService(BluetoothManager.class);
-  }
-
-  /**
-   * Start the BLE scanning process.
-   *
-   * @param filters Optional list of {@link ScanFilter}s to apply to scan results.
-   * @param settings {@link ScanSettings} to apply to scanner.
-   * @param callback {@link ScanCallback} for scan events.
-   */
-  public void startScanning(
-      @Nullable List<ScanFilter> filters,
-      @NonNull ScanSettings settings,
-      @NonNull ScanCallback callback) {
-    if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
-      loge(TAG, "Attempted start scanning, but system does not support BLE. Ignoring");
-      return;
-    }
-    logd(TAG, "Request received to start scanning.");
-    scannerStartCount = 0;
-    scanFilters = filters;
-    scanSettings = settings;
-    scanCallback = callback;
-    updateScannerState(STARTED);
-    startScanningInternally();
-  }
-
-  /** Stop the scanner */
-  public void stopScanning() {
-    logd(TAG, "Attempting to stop scanning");
-    if (scanner != null) {
-      scanner.stopScan(internalScanCallback);
-    }
-    scanCallback = null;
-    updateScannerState(STOPPED);
-  }
-
-  /** Returns {@code true} if currently scanning, {@code false} otherwise. */
-  public boolean isScanning() {
-    return scannerState.get() == SCANNING;
-  }
-
-  /** Clean up the scanning process. */
-  public void cleanup() {
-    if (isScanning()) {
-      stopScanning();
-    }
-  }
-
-  private void startScanningInternally() {
-    logd(TAG, "Attempting to start scanning");
-    BluetoothAdapter adapter = bluetoothManager.getAdapter();
-    if (scanner == null && adapter != null) {
-      scanner = adapter.getBluetoothLeScanner();
-    }
-    if (scanner != null) {
-      scanner.startScan(scanFilters, scanSettings, internalScanCallback);
-      updateScannerState(SCANNING);
-    } else {
-      handler.postDelayed(
-          () -> {
-            // Keep trying
-            logd(TAG, "Scanner unavailable. Trying again.");
-            startScanningInternally();
-          },
-          RETRY_INTERVAL_MS);
-    }
-  }
-
-  private void updateScannerState(@ScannerState int newState) {
-    scannerState.set(newState);
-  }
-
-  private final ScanCallback internalScanCallback =
-      new ScanCallback() {
-        @Override
-        public void onScanResult(int callbackType, ScanResult result) {
-          if (scanCallback != null) {
-            scanCallback.onScanResult(callbackType, result);
-          }
-        }
-
-        @Override
-        public void onBatchScanResults(List<ScanResult> results) {
-          logd(TAG, "Batch scan found " + results.size() + " results.");
-          if (scanCallback != null) {
-            scanCallback.onBatchScanResults(results);
-          }
-        }
-
-        @Override
-        public void onScanFailed(int errorCode) {
-          if (scannerStartCount >= RETRY_LIMIT) {
-            loge(TAG, "Cannot start BLE Scanner. Scanning Retry count: " + scannerStartCount);
-            if (scanCallback != null) {
-              scanCallback.onScanFailed(errorCode);
-            }
-            return;
-          }
-
-          scannerStartCount++;
-          logw(
-              TAG,
-              "BLE Scanner failed to start. Error: " + errorCode + " Retry: " + scannerStartCount);
-          switch (errorCode) {
-            case SCAN_FAILED_ALREADY_STARTED:
-              // Scanner already started. Do nothing.
-              break;
-            case SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
-            case SCAN_FAILED_INTERNAL_ERROR:
-              handler.postDelayed(
-                  BleCentralManager.this::startScanningInternally, RETRY_INTERVAL_MS);
-              break;
-            default:
-              // Ignore other codes.
-          }
-        }
-      };
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/BleDeviceMessageStream.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/BleDeviceMessageStream.java
deleted file mode 100644
index 6b10448..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/BleDeviceMessageStream.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection.ble;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGattCharacteristic;
-import androidx.annotation.NonNull;
-import com.google.android.connecteddevice.connection.DeviceMessageStream;
-import com.google.android.connecteddevice.transport.ble.BlePeripheralManager;
-
-/** BLE message stream to a device. */
-public class BleDeviceMessageStream extends DeviceMessageStream {
-
-  private static final String TAG = "BleDeviceMessageStream";
-
-  private final BlePeripheralManager blePeripheralManager;
-
-  private final BluetoothDevice device;
-
-  private final BluetoothGattCharacteristic writeCharacteristic;
-
-  private final BluetoothGattCharacteristic readCharacteristic;
-
-  BleDeviceMessageStream(
-      @NonNull BlePeripheralManager blePeripheralManager,
-      @NonNull BluetoothDevice device,
-      @NonNull BluetoothGattCharacteristic writeCharacteristic,
-      @NonNull BluetoothGattCharacteristic readCharacteristic,
-      int defaultMaxWriteSize) {
-    super(defaultMaxWriteSize);
-    this.blePeripheralManager = blePeripheralManager;
-    this.device = device;
-    this.writeCharacteristic = writeCharacteristic;
-    this.readCharacteristic = readCharacteristic;
-    this.blePeripheralManager.addOnCharacteristicWriteListener(this::onCharacteristicWrite);
-    this.blePeripheralManager.addOnCharacteristicReadListener(this::onCharacteristicRead);
-  }
-
-  @Override
-  protected void send(byte[] data) {
-    writeCharacteristic.setValue(data);
-    blePeripheralManager.notifyCharacteristicChanged(
-        device, writeCharacteristic, /* confirm= */ false);
-  }
-
-  private void onCharacteristicRead(@NonNull BluetoothDevice device) {
-    if (!this.device.equals(device)) {
-      logw(
-          TAG,
-          "Received a read notification from a device ("
-              + device.getAddress()
-              + ") that is not the expected device ("
-              + this.device.getAddress()
-              + ") registered "
-              + "to this stream. Ignoring.");
-      return;
-    }
-
-    logd(TAG, "Releasing lock on characteristic.");
-    sendCompleted();
-  }
-
-  private void onCharacteristicWrite(
-      @NonNull BluetoothDevice device,
-      @NonNull BluetoothGattCharacteristic characteristic,
-      @NonNull byte[] value) {
-    logd(TAG, "Received a message from a device (" + device.getAddress() + ").");
-    if (!this.device.equals(device)) {
-      logw(
-          TAG,
-          "Received a message from a device ("
-              + device.getAddress()
-              + ") that is not "
-              + "the expected device ("
-              + this.device.getAddress()
-              + ") registered to this "
-              + "stream. Ignoring.");
-      return;
-    }
-
-    if (!characteristic.getUuid().equals(readCharacteristic.getUuid())) {
-      logw(
-          TAG,
-          "Received a write to a characteristic ("
-              + characteristic.getUuid()
-              + ") that"
-              + " is not the expected UUID ("
-              + readCharacteristic.getUuid()
-              + "). "
-              + "Ignoring.");
-      return;
-    }
-    onDataReceived(value);
-  }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/CarBleCentralManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/CarBleCentralManager.java
deleted file mode 100644
index 488eb0d..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/CarBleCentralManager.java
+++ /dev/null
@@ -1,382 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection.ble;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static com.google.android.connecteddevice.util.SafeLog.logw;
-import static com.google.android.connecteddevice.util.ScanDataAnalyzer.containsUuidsInOverflow;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCallback;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.BluetoothProfile;
-import android.bluetooth.le.ScanCallback;
-import android.bluetooth.le.ScanRecord;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
-import android.content.Context;
-import android.os.ParcelUuid;
-import androidx.annotation.NonNull;
-import com.google.android.connecteddevice.connection.AssociationCallback;
-import com.google.android.connecteddevice.connection.CarBluetoothManager;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import java.math.BigInteger;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.CopyOnWriteArraySet;
-
-/**
- * Communication manager for a car that maintains continuous connections with all devices in the car
- * for the duration of a drive.
- */
-public class CarBleCentralManager extends CarBluetoothManager {
-
-  private static final String TAG = "CarBleCentralManager";
-
-  // system/bt/internal_include/bt_target.h#GATT_MAX_PHY_CHANNEL
-  private static final int MAX_CONNECTIONS = 7;
-
-  private static final UUID CHARACTERISTIC_CONFIG =
-      UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
-
-  private static final int STATUS_FORCED_DISCONNECT = -1;
-
-  private final ScanSettings scanSettings =
-      new ScanSettings.Builder()
-          .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
-          .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
-          .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
-          .build();
-
-  private final CopyOnWriteArraySet<ConnectedRemoteDevice> ignoredDevices =
-      new CopyOnWriteArraySet<>();
-
-  private final Context context;
-
-  private final BleCentralManager bleCentralManager;
-
-  private final UUID serviceUuid;
-
-  private final UUID writeCharacteristicUuid;
-
-  private final UUID readCharacteristicUuid;
-
-  private final BigInteger parsedBgServiceBitMask;
-
-  /**
-   * Create a new manager.
-   *
-   * @param context The caller's [Context].
-   * @param bleCentralManager [BleCentralManager] for establishing connections.
-   * @param connectedDeviceStorage Shared [ConnectedDeviceStorage] for companion features.
-   * @param serviceUuid [UUID] of peripheral's service.
-   * @param bgServiceMask iOS overflow bit mask for service UUID.
-   * @param writeCharacteristicUuid [UUID] of characteristic the car will write to.
-   * @param readCharacteristicUuid [UUID] of characteristic the device will write to.
-   * @param enableCompression Enable compression on outgoing messages.
-   * @param isCapabilitiesEligible Association should attempt a capabilities exchange.
-   */
-  public CarBleCentralManager(
-      @NonNull Context context,
-      @NonNull BleCentralManager bleCentralManager,
-      @NonNull ConnectedDeviceStorage connectedDeviceStorage,
-      @NonNull UUID serviceUuid,
-      @NonNull String bgServiceMask,
-      @NonNull UUID writeCharacteristicUuid,
-      @NonNull UUID readCharacteristicUuid,
-      boolean enableCompression,
-      boolean isCapabilitiesEligible) {
-    super(connectedDeviceStorage, enableCompression, isCapabilitiesEligible);
-    this.context = context;
-    this.bleCentralManager = bleCentralManager;
-    this.serviceUuid = serviceUuid;
-    this.writeCharacteristicUuid = writeCharacteristicUuid;
-    this.readCharacteristicUuid = readCharacteristicUuid;
-    parsedBgServiceBitMask = new BigInteger(bgServiceMask, 16);
-  }
-
-  @Override
-  public void start() {
-    super.start();
-    bleCentralManager.startScanning(/* filters= */ null, scanSettings, scanCallback);
-  }
-
-  @Override
-  public void stop() {
-    super.stop();
-    bleCentralManager.stopScanning();
-  }
-
-  @Override
-  public void disconnectDevice(String deviceId) {
-    logd(TAG, "Request to disconnect from device " + deviceId + ".");
-    ConnectedRemoteDevice device = getConnectedDevice(deviceId);
-    if (device == null) {
-      return;
-    }
-
-    deviceDisconnected(device, STATUS_FORCED_DISCONNECT);
-  }
-
-  // TODO(b/141312136): Support car central role
-  @Override
-  public AssociationCallback getAssociationCallback() {
-    return null;
-  }
-
-  @Override
-  public void setAssociationCallback(AssociationCallback callback) {}
-
-  @Override
-  public void connectToDevice(UUID deviceId) {}
-
-  @Override
-  public void initiateConnectionToDevice(UUID deviceId) {}
-
-  @Override
-  public void startAssociation(byte[] nameForAssociation, AssociationCallback callback) {}
-
-  private void ignoreDevice(@NonNull ConnectedRemoteDevice device) {
-    ignoredDevices.add(device);
-  }
-
-  private boolean isDeviceIgnored(@NonNull BluetoothDevice device) {
-    for (ConnectedRemoteDevice connectedDevice : ignoredDevices) {
-      if (device.equals(connectedDevice.device)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private boolean shouldAttemptConnection(@NonNull ScanResult result) {
-    // Ignore any results that are not connectable.
-    if (!result.isConnectable()) {
-      return false;
-    }
-
-    // Do not attempt to connect if we have already hit our max. This should rarely happen
-    // and is protecting against a race condition of scanning stopped and new results coming in.
-    if (getConnectedDevicesCount() >= MAX_CONNECTIONS) {
-      return false;
-    }
-
-    BluetoothDevice device = result.getDevice();
-
-    // Do not connect if device has already been ignored.
-    if (isDeviceIgnored(device)) {
-      return false;
-    }
-
-    // Check if already attempting to connect to this device.
-    if (getConnectedDevice(device) != null) {
-      return false;
-    }
-
-    // Ignore any device without a scan record.
-    ScanRecord scanRecord = result.getScanRecord();
-    if (scanRecord == null) {
-      return false;
-    }
-
-    // Connect to any device that is advertising our service UUID.
-    List<ParcelUuid> serviceUuids = scanRecord.getServiceUuids();
-    if (serviceUuids != null) {
-      for (ParcelUuid serviceUuid : serviceUuids) {
-        if (serviceUuid.getUuid().equals(this.serviceUuid)) {
-          return true;
-        }
-      }
-    }
-    if (containsUuidsInOverflow(scanRecord.getBytes(), parsedBgServiceBitMask)) {
-      return true;
-    }
-
-    // Can safely ignore devices advertising unrecognized service uuids.
-    if (serviceUuids != null && !serviceUuids.isEmpty()) {
-      return false;
-    }
-
-    // TODO(b/139066293): Current implementation quickly exhausts connections resulting in
-    // greatly reduced performance for connecting to devices we know we want to connect to.
-    // Return true once fixed.
-    return false;
-  }
-
-  private void startDeviceConnection(@NonNull BluetoothDevice device) {
-    BluetoothGatt gatt =
-        device.connectGatt(
-            context, /* autoConnect= */ false, connectionCallback, BluetoothDevice.TRANSPORT_LE);
-    if (gatt == null) {
-      return;
-    }
-
-    ConnectedRemoteDevice bleDevice = new ConnectedRemoteDevice(device, gatt);
-    bleDevice.state = ConnectedDeviceState.CONNECTING;
-    addConnectedDevice(bleDevice);
-
-    // Stop scanning if we have reached the maximum number of connections.
-    if (getConnectedDevicesCount() >= MAX_CONNECTIONS) {
-      bleCentralManager.stopScanning();
-    }
-  }
-
-  private void deviceConnected(@NonNull ConnectedRemoteDevice device) {
-    if (device.gatt == null) {
-      loge(TAG, "Device connected with null gatt. Disconnecting.");
-      deviceDisconnected(device, BluetoothProfile.STATE_DISCONNECTED);
-      return;
-    }
-    device.state = ConnectedDeviceState.PENDING_VERIFICATION;
-    device.gatt.discoverServices();
-    logd(
-        TAG,
-        "New device connected: "
-            + device.gatt.getDevice().getAddress()
-            + ". Active connections: "
-            + getConnectedDevicesCount()
-            + ".");
-  }
-
-  private void deviceDisconnected(@NonNull ConnectedRemoteDevice device, int status) {
-    removeConnectedDevice(device);
-    if (device.gatt != null) {
-      device.gatt.close();
-    }
-    if (device.deviceId != null) {
-      callbacks.invoke(callback -> callback.onDeviceDisconnected(device.deviceId));
-    }
-    logd(
-        TAG,
-        "Device with id "
-            + device.deviceId
-            + " disconnected with state "
-            + status
-            + ". Remaining active connections: "
-            + getConnectedDevicesCount()
-            + ".");
-  }
-
-  private final ScanCallback scanCallback =
-      new ScanCallback() {
-        @Override
-        public void onScanResult(int callbackType, ScanResult result) {
-          super.onScanResult(callbackType, result);
-          if (shouldAttemptConnection(result)) {
-            startDeviceConnection(result.getDevice());
-          }
-        }
-
-        @Override
-        public void onScanFailed(int errorCode) {
-          super.onScanFailed(errorCode);
-          loge(TAG, "BLE scanning failed with error code: " + errorCode);
-        }
-      };
-
-  private final BluetoothGattCallback connectionCallback =
-      new BluetoothGattCallback() {
-        @Override
-        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
-          super.onConnectionStateChange(gatt, status, newState);
-          if (gatt == null) {
-            logw(TAG, "Null gatt passed to onConnectionStateChange. Ignoring.");
-            return;
-          }
-
-          ConnectedRemoteDevice connectedDevice = getConnectedDevice(gatt);
-          if (connectedDevice == null) {
-            return;
-          }
-
-          switch (newState) {
-            case BluetoothProfile.STATE_CONNECTED:
-              deviceConnected(connectedDevice);
-              break;
-            case BluetoothProfile.STATE_DISCONNECTED:
-              deviceDisconnected(connectedDevice, status);
-              break;
-            default:
-              logd(TAG, "Connection state changed. New state: " + newState + " status: " + status);
-          }
-        }
-
-        @Override
-        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
-          super.onServicesDiscovered(gatt, status);
-          if (gatt == null) {
-            logw(TAG, "Null gatt passed to onServicesDiscovered. Ignoring.");
-            return;
-          }
-
-          ConnectedRemoteDevice connectedDevice = getConnectedDevice(gatt);
-          if (connectedDevice == null) {
-            return;
-          }
-          BluetoothGattService service = gatt.getService(serviceUuid);
-          if (service == null) {
-            ignoreDevice(connectedDevice);
-            gatt.disconnect();
-            return;
-          }
-
-          connectedDevice.state = ConnectedDeviceState.CONNECTED;
-          BluetoothGattCharacteristic writeCharacteristic =
-              service.getCharacteristic(writeCharacteristicUuid);
-          BluetoothGattCharacteristic readCharacteristic =
-              service.getCharacteristic(readCharacteristicUuid);
-          if (writeCharacteristic == null || readCharacteristic == null) {
-            logw(TAG, "Unable to find expected characteristics on peripheral.");
-            gatt.disconnect();
-            return;
-          }
-
-          // Turn on notifications for read characteristic.
-          BluetoothGattDescriptor descriptor =
-              readCharacteristic.getDescriptor(CHARACTERISTIC_CONFIG);
-          descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
-          if (!gatt.writeDescriptor(descriptor)) {
-            loge(TAG, "Write descriptor to read characteristic failed.");
-            gatt.disconnect();
-            return;
-          }
-
-          if (!gatt.setCharacteristicNotification(readCharacteristic, /* enable= */ true)) {
-            loge(TAG, "Set notifications to read characteristic failed.");
-            gatt.disconnect();
-            return;
-          }
-
-          logd(TAG, "Service and characteristics successfully discovered.");
-        }
-
-        @Override
-        public void onDescriptorWrite(
-            BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
-          super.onDescriptorWrite(gatt, descriptor, status);
-          if (gatt == null) {
-            logw(TAG, "Null gatt passed to onDescriptorWrite. Ignoring.");
-            return;
-          }
-          // TODO(b/141312136): Create SecureBleChannel and assign to connectedDevice.
-        }
-      };
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/CarBlePeripheralManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/CarBlePeripheralManager.java
deleted file mode 100644
index e0fc747..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/CarBlePeripheralManager.java
+++ /dev/null
@@ -1,523 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection.ble;
-
-import static com.google.android.connecteddevice.model.Errors.DEVICE_ERROR_UNEXPECTED_DISCONNECTION;
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static com.google.android.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.le.AdvertiseCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.ParcelUuid;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.connecteddevice.connection.AssociationCallback;
-import com.google.android.connecteddevice.connection.AssociationSecureChannel;
-import com.google.android.connecteddevice.connection.CarBluetoothManager;
-import com.google.android.connecteddevice.connection.ConnectionResolver;
-import com.google.android.connecteddevice.connection.DeviceMessageStream;
-import com.google.android.connecteddevice.connection.ReconnectSecureChannel;
-import com.google.android.connecteddevice.model.OobData;
-import com.google.android.connecteddevice.model.StartAssociationResponse;
-import com.google.android.connecteddevice.oob.OobChannel;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.transport.ble.BlePeripheralManager;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.android.connecteddevice.util.EventLog;
-import java.time.Duration;
-import java.util.Arrays;
-import java.util.UUID;
-import java.util.concurrent.Future;
-
-/** Communication manager that allows for targeted connections to a specific device in the car. */
-public class CarBlePeripheralManager extends CarBluetoothManager {
-
-  private static final String TAG = "CarBlePeripheralManager";
-
-  // Attribute protocol bytes attached to message. Available write size is MTU size minus att
-  // bytes.
-  private static final int ATT_PROTOCOL_BYTES = 3;
-
-  private static final UUID CLIENT_CHARACTERISTIC_CONFIG =
-      UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
-
-  private static final int SALT_BYTES = 8;
-
-  private static final int TOTAL_AD_DATA_BYTES = 16;
-
-  private static final int TRUNCATED_BYTES = 3;
-
-  private static final String TIMEOUT_HANDLER_THREAD_NAME = "peripheralThread";
-
-  private final BlePeripheralManager blePeripheralManager;
-
-  private final UUID associationServiceUuid;
-
-  private final UUID reconnectServiceUuid;
-
-  private final UUID reconnectDataUuid;
-
-  private final BluetoothGattCharacteristic writeCharacteristic;
-
-  @VisibleForTesting final BluetoothGattCharacteristic readCharacteristic;
-
-  private final BluetoothGattCharacteristic advertiseDataCharacteristic;
-
-  private HandlerThread timeoutHandlerThread;
-
-  private Handler timeoutHandler;
-
-  private final Duration maxReconnectAdvertisementDuration;
-
-  private final int defaultMtuSize;
-
-  private String reconnectDeviceId;
-
-  private byte[] reconnectChallenge;
-
-  private AdvertiseCallback advertiseCallback;
-
-  private Future<?> bluetoothNameTask;
-
-  public CarBlePeripheralManager(
-      @NonNull BlePeripheralManager blePeripheralManager,
-      @NonNull ConnectedDeviceStorage connectedDeviceStorage,
-      @NonNull UUID associationServiceUuid,
-      @NonNull UUID reconnectServiceUuid,
-      @NonNull UUID reconnectDataUuid,
-      @NonNull UUID advertiseDataCharacteristicUuid,
-      @NonNull UUID writeCharacteristicUuid,
-      @NonNull UUID readCharacteristicUuid,
-      @NonNull Duration maxReconnectAdvertisementDuration,
-      int defaultMtuSize,
-      boolean enableCompression,
-      boolean isCapabilitiesEligible) {
-    this(
-        blePeripheralManager,
-        connectedDeviceStorage,
-        associationServiceUuid,
-        reconnectServiceUuid,
-        reconnectDataUuid,
-        advertiseDataCharacteristicUuid,
-        writeCharacteristicUuid,
-        readCharacteristicUuid,
-        maxReconnectAdvertisementDuration,
-        defaultMtuSize,
-        enableCompression,
-        /* oobChannel= */ null,
-        isCapabilitiesEligible);
-  }
-
-  /**
-   * Initialize a new instance of manager.
-   *
-   * @param blePeripheralManager {@link BlePeripheralManager} for establishing connection.
-   * @param connectedDeviceStorage Shared {@link ConnectedDeviceStorage} for companion features.
-   * @param associationServiceUuid {@link UUID} of association service.
-   * @param reconnectServiceUuid {@link UUID} of reconnect service.
-   * @param reconnectDataUuid {@link UUID} key of reconnect advertisement data.
-   * @param advertiseDataCharacteristicUuid {@link UUID} of characteristic that contains advertise
-   *     data.
-   * @param writeCharacteristicUuid {@link UUID} of characteristic the car will write to.
-   * @param readCharacteristicUuid {@link UUID} of characteristic the device will write to.
-   * @param maxReconnectAdvertisementDuration Maximum duration to advertise for reconnect before
-   *     restarting.
-   * @param defaultMtuSize Default MTU size for new channels.
-   * @param enableCompression Enable compression on outgoing messages.
-   * @param oobChannel Channel for exchanging out of band verification.
-   * @param isCapabilitiesEligible Association should attempt a capabilities exchange.
-   */
-  public CarBlePeripheralManager(
-      @NonNull BlePeripheralManager blePeripheralManager,
-      @NonNull ConnectedDeviceStorage connectedDeviceStorage,
-      @NonNull UUID associationServiceUuid,
-      @NonNull UUID reconnectServiceUuid,
-      @NonNull UUID reconnectDataUuid,
-      @NonNull UUID advertiseDataCharacteristicUuid,
-      @NonNull UUID writeCharacteristicUuid,
-      @NonNull UUID readCharacteristicUuid,
-      @NonNull Duration maxReconnectAdvertisementDuration,
-      int defaultMtuSize,
-      boolean enableCompression,
-      @Nullable OobChannel oobChannel,
-      boolean isCapabilitiesEligible) {
-    super(connectedDeviceStorage, enableCompression, oobChannel, isCapabilitiesEligible);
-    this.blePeripheralManager = blePeripheralManager;
-    this.associationServiceUuid = associationServiceUuid;
-    this.reconnectServiceUuid = reconnectServiceUuid;
-    this.reconnectDataUuid = reconnectDataUuid;
-    writeCharacteristic =
-        new BluetoothGattCharacteristic(
-            writeCharacteristicUuid,
-            BluetoothGattCharacteristic.PROPERTY_NOTIFY | BluetoothGattCharacteristic.PROPERTY_READ,
-            BluetoothGattCharacteristic.PERMISSION_READ);
-    writeCharacteristic.addDescriptor(createBluetoothGattDescriptor());
-
-    readCharacteristic =
-        new BluetoothGattCharacteristic(
-            readCharacteristicUuid,
-            BluetoothGattCharacteristic.PROPERTY_WRITE
-                | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
-            BluetoothGattCharacteristic.PERMISSION_WRITE);
-    readCharacteristic.addDescriptor(createBluetoothGattDescriptor());
-
-    advertiseDataCharacteristic =
-        new BluetoothGattCharacteristic(
-            advertiseDataCharacteristicUuid,
-            BluetoothGattCharacteristic.PROPERTY_NOTIFY | BluetoothGattCharacteristic.PROPERTY_READ,
-            BluetoothGattCharacteristic.PERMISSION_READ);
-    advertiseDataCharacteristic.addDescriptor(createBluetoothGattDescriptor());
-
-    this.maxReconnectAdvertisementDuration = maxReconnectAdvertisementDuration;
-    this.defaultMtuSize = defaultMtuSize;
-  }
-
-  private BluetoothGattDescriptor createBluetoothGattDescriptor() {
-    BluetoothGattDescriptor descriptor =
-        new BluetoothGattDescriptor(
-            CLIENT_CHARACTERISTIC_CONFIG,
-            BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE);
-    descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
-    return descriptor;
-  }
-
-  @Override
-  public void start() {
-    super.start();
-    timeoutHandlerThread = new HandlerThread(TIMEOUT_HANDLER_THREAD_NAME);
-    timeoutHandlerThread.start();
-    timeoutHandler = new Handler(timeoutHandlerThread.getLooper());
-  }
-
-  @Override
-  public void stop() {
-    super.stop();
-    if (timeoutHandlerThread != null) {
-      timeoutHandlerThread.quit();
-    }
-    reset();
-  }
-
-  @Override
-  public void disconnectDevice(@NonNull String deviceId) {
-    if (deviceId.equals(reconnectDeviceId)) {
-      logd(TAG, "Reconnection canceled for device " + deviceId + ".");
-      reset();
-      return;
-    }
-    ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-    if (connectedDevice == null || !deviceId.equals(connectedDevice.deviceId)) {
-      return;
-    }
-    reset();
-  }
-
-  @Override
-  public void reset() {
-    super.reset();
-    logd(TAG, "Resetting state.");
-    if (timeoutHandler != null) {
-      timeoutHandler.removeCallbacks(timeoutRunnable);
-    }
-    blePeripheralManager.cleanup();
-    reconnectDeviceId = null;
-    reconnectChallenge = null;
-    if (bluetoothNameTask != null) {
-      bluetoothNameTask.cancel(true);
-    }
-    bluetoothNameTask = null;
-  }
-
-  @Override
-  public void initiateConnectionToDevice(@NonNull UUID deviceId) {
-    reconnectDeviceId = deviceId.toString();
-    advertiseCallback =
-        new AdvertiseCallback() {
-          @Override
-          public void onStartSuccess(AdvertiseSettings settingsInEffect) {
-            super.onStartSuccess(settingsInEffect);
-            timeoutHandler.postDelayed(
-                timeoutRunnable, maxReconnectAdvertisementDuration.toMillis());
-            logd(TAG, "Successfully started advertising for device " + deviceId + ".");
-          }
-        };
-    blePeripheralManager.unregisterCallback(associationPeripheralCallback);
-    blePeripheralManager.registerCallback(reconnectPeripheralCallback);
-    timeoutHandler.removeCallbacks(timeoutRunnable);
-    byte[] advertiseData = createReconnectData(reconnectDeviceId);
-    if (advertiseData == null) {
-      loge(TAG, "Unable to create advertisement data. Aborting reconnect.");
-      return;
-    }
-    startAdvertising(
-        reconnectServiceUuid,
-        advertiseCallback,
-        advertiseData,
-        reconnectDataUuid,
-        /* scanResponse= */ null,
-        /* scanResponseUuid= */ null);
-  }
-
-  /**
-   * Create data for reconnection advertisement.
-   *
-   * <p>
-   *
-   * <p>Process:
-   *
-   * <ol>
-   *   <li>Generate random {@value SALT_BYTES} byte salt and zero-pad to {@value
-   *       TOTAL_AD_DATA_BYTES} bytes.
-   *   <li>Hash with stored challenge secret and truncate to {@value TRUNCATED_BYTES} bytes.
-   *   <li>Concatenate hashed {@value TRUNCATED_BYTES} bytes with salt and return.
-   * </ol>
-   */
-  @Nullable
-  private byte[] createReconnectData(String deviceId) {
-    byte[] salt = ByteUtils.randomBytes(SALT_BYTES);
-    byte[] zeroPadded =
-        ByteUtils.concatByteArrays(salt, new byte[TOTAL_AD_DATA_BYTES - SALT_BYTES]);
-    reconnectChallenge = storage.hashWithChallengeSecret(deviceId, zeroPadded);
-    if (reconnectChallenge == null) {
-      return null;
-    }
-    return ByteUtils.concatByteArrays(Arrays.copyOf(reconnectChallenge, TRUNCATED_BYTES), salt);
-  }
-
-  @Override
-  public void startAssociation(
-      @NonNull byte[] nameForAssociation, @NonNull AssociationCallback callback) {
-    reset();
-    setAssociationCallback(callback);
-    blePeripheralManager.unregisterCallback(reconnectPeripheralCallback);
-    blePeripheralManager.registerCallback(associationPeripheralCallback);
-    advertiseCallback =
-        new AdvertiseCallback() {
-          @Override
-          public void onStartSuccess(AdvertiseSettings settingsInEffect) {
-            super.onStartSuccess(settingsInEffect);
-            callback.onAssociationStartSuccess(
-                new StartAssociationResponse(
-                    /* oobData= */ new OobData(new byte[0], new byte[0], new byte[0]),
-                    nameForAssociation,
-                    ByteUtils.byteArrayToHexString(nameForAssociation)));
-            logd(TAG, "Successfully started advertising for association.");
-          }
-
-          @Override
-          public void onStartFailure(int errorCode) {
-            super.onStartFailure(errorCode);
-            callback.onAssociationStartFailure();
-            logd(TAG, "Failed to start advertising for association. Error code: " + errorCode);
-          }
-        };
-
-    startAdvertising(
-        associationServiceUuid,
-        advertiseCallback,
-        /* advertiseData= */ null,
-        /* advertiseDataUuid= */ null,
-        nameForAssociation,
-        reconnectDataUuid);
-  }
-
-  /** Set the timeout handler for testing. This should be called after {@link #start()}. */
-  @VisibleForTesting
-  void setTimeoutHandler(Handler handler) {
-    timeoutHandler = handler;
-  }
-
-  private void startAdvertising(
-      @NonNull UUID serviceUuid,
-      @NonNull AdvertiseCallback callback,
-      @Nullable byte[] advertiseData,
-      @Nullable UUID advertiseDataUuid,
-      @Nullable byte[] scanResponse,
-      @Nullable UUID scanResponseUuid) {
-    BluetoothGattService gattService =
-        new BluetoothGattService(serviceUuid, BluetoothGattService.SERVICE_TYPE_PRIMARY);
-    gattService.addCharacteristic(writeCharacteristic);
-    gattService.addCharacteristic(readCharacteristic);
-
-    AdvertiseData.Builder advertisementBuilder = new AdvertiseData.Builder();
-    ParcelUuid uuid = new ParcelUuid(serviceUuid);
-    advertisementBuilder.addServiceUuid(uuid);
-    if (advertiseData != null) {
-      ParcelUuid dataUuid = uuid;
-      if (advertiseDataUuid != null) {
-        dataUuid = new ParcelUuid(advertiseDataUuid);
-      }
-      advertisementBuilder.addServiceData(dataUuid, advertiseData);
-
-      // Also embed the advertise data into a fixed GATT service characteristic.
-      advertiseDataCharacteristic.setValue(advertiseData);
-      gattService.addCharacteristic(advertiseDataCharacteristic);
-    }
-
-    AdvertiseData.Builder scanResponseBuilder = new AdvertiseData.Builder();
-    if (scanResponse != null && scanResponseUuid != null) {
-      ParcelUuid scanResponseParcelUuid = new ParcelUuid(scanResponseUuid);
-      scanResponseBuilder.addServiceData(scanResponseParcelUuid, scanResponse);
-    }
-
-    blePeripheralManager.startAdvertising(
-        gattService, advertisementBuilder.build(), scanResponseBuilder.build(), callback);
-  }
-
-  private void addConnectedDevice(BluetoothDevice device, boolean isReconnect) {
-    EventLog.onDeviceConnected();
-    blePeripheralManager.stopAdvertising(advertiseCallback);
-    if (timeoutHandler != null) {
-      timeoutHandler.removeCallbacks(timeoutRunnable);
-    }
-
-    setClientDeviceAddress(device.getAddress());
-
-    DeviceMessageStream secureStream =
-        new BleDeviceMessageStream(
-            blePeripheralManager,
-            device,
-            writeCharacteristic,
-            readCharacteristic,
-            defaultMtuSize - ATT_PROTOCOL_BYTES);
-
-    secureStream.setMessageReceivedErrorListener(
-        exception ->
-            disconnectWithError("Error occurred in stream: " + exception.getMessage(), exception));
-
-    ConnectedRemoteDevice connectedDevice = new ConnectedRemoteDevice(device, /* gatt= */ null);
-
-    if (isReconnect) {
-      // Capabilities are only exchanged during association so isCapabilitiesEligible is always
-      // false here.
-      ConnectionResolver connectionResolver =
-          new ConnectionResolver(secureStream, /* isCapabilitiesEligible= */ false);
-      connectionResolver.resolveConnection(
-          (resolvedConnection) -> {
-            ReconnectSecureChannel secureChannel =
-                new ReconnectSecureChannel(
-                    secureStream, storage, reconnectDeviceId, reconnectChallenge);
-            secureChannel.registerCallback(secureChannelCallback);
-            connectedDevice.secureChannel = secureChannel;
-            addConnectedDevice(connectedDevice);
-            setDeviceIdAndNotifyCallbacks(reconnectDeviceId);
-            reconnectDeviceId = null;
-            reconnectChallenge = null;
-          });
-    } else {
-      onDeviceConnectedForAssociation(secureStream, connectedDevice);
-    }
-  }
-
-  private void setMtuSize(int mtuSize) {
-    ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-    if (connectedDevice != null
-        && connectedDevice.secureChannel != null) {
-      connectedDevice.secureChannel.getStream().setMaxWriteSize(mtuSize - ATT_PROTOCOL_BYTES);
-    }
-  }
-
-  private final BlePeripheralManager.Callback reconnectPeripheralCallback =
-      new BlePeripheralManager.Callback() {
-        @Override
-        public void onMtuSizeChanged(int size) {
-          setMtuSize(size);
-        }
-
-        @Override
-        public void onRemoteDeviceConnected(BluetoothDevice device) {
-          addConnectedDevice(device, /* isReconnect= */ true);
-        }
-
-        @Override
-        public void onRemoteDeviceDisconnected(BluetoothDevice device) {
-          String deviceId = reconnectDeviceId;
-          ConnectedRemoteDevice connectedDevice = getConnectedDevice(device);
-          // Reset before invoking callbacks to avoid a race condition with reconnect
-          // logic.
-          reset();
-          if (connectedDevice != null) {
-            deviceId = connectedDevice.deviceId;
-          }
-          final String finalDeviceId = deviceId;
-          if (finalDeviceId == null) {
-            logw(
-                TAG,
-                "Callbacks were not issued for disconnect because the device id " + "was null.");
-            return;
-          }
-          logd(TAG, "Connected device " + finalDeviceId + " disconnected.");
-          callbacks.invoke(callback -> callback.onDeviceDisconnected(finalDeviceId));
-        }
-      };
-
-  private final BlePeripheralManager.Callback associationPeripheralCallback =
-      new BlePeripheralManager.Callback() {
-        @Override
-        public void onMtuSizeChanged(int size) {
-          setMtuSize(size);
-        }
-
-        @Override
-        public void onRemoteDeviceConnected(BluetoothDevice device) {
-          addConnectedDevice(device, /* isReconnect= */ false);
-          ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-          if (connectedDevice == null || connectedDevice.secureChannel == null) {
-            return;
-          }
-          ((AssociationSecureChannel) connectedDevice.secureChannel)
-              .setShowVerificationCodeListener(
-                  CarBlePeripheralManager.this::onVerificationCodeAvailable);
-        }
-
-        @Override
-        public void onRemoteDeviceDisconnected(BluetoothDevice device) {
-          logd(TAG, "Remote device disconnected.");
-          ConnectedRemoteDevice connectedDevice = getConnectedDevice(device);
-          if (isAssociating()) {
-            getAssociationCallback().onAssociationError(DEVICE_ERROR_UNEXPECTED_DISCONNECTION);
-          }
-          // Reset before invoking callbacks to avoid a race condition with reconnect
-          // logic.
-          reset();
-          if (connectedDevice == null || connectedDevice.deviceId == null) {
-            logw(TAG, "Callbacks were not issued for disconnect.");
-            return;
-          }
-          callbacks.invoke(callback -> callback.onDeviceDisconnected(connectedDevice.deviceId));
-        }
-      };
-
-  private final Runnable timeoutRunnable =
-      new Runnable() {
-        @Override
-        public void run() {
-          logd(TAG, "Timeout period expired without a connection. Restarting advertisement.");
-          blePeripheralManager.stopAdvertising(advertiseCallback);
-          connectToDevice(UUID.fromString(reconnectDeviceId));
-        }
-      };
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/spp/CarSppManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/spp/CarSppManager.java
deleted file mode 100644
index 322b2ff..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/spp/CarSppManager.java
+++ /dev/null
@@ -1,305 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection.spp;
-
-import static com.google.android.connecteddevice.model.Errors.DEVICE_ERROR_UNEXPECTED_DISCONNECTION;
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import android.bluetooth.BluetoothDevice;
-import android.os.ParcelUuid;
-import android.os.RemoteException;
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.connecteddevice.connection.AssociationCallback;
-import com.google.android.connecteddevice.connection.AssociationSecureChannel;
-import com.google.android.connecteddevice.connection.CarBluetoothManager;
-import com.google.android.connecteddevice.connection.ConnectionResolver;
-import com.google.android.connecteddevice.connection.DeviceMessageStream;
-import com.google.android.connecteddevice.connection.ReconnectSecureChannel;
-import com.google.android.connecteddevice.model.OobData;
-import com.google.android.connecteddevice.model.StartAssociationResponse;
-import com.google.android.connecteddevice.oob.BluetoothRfcommChannel;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder;
-import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder.OnErrorListener;
-import com.google.android.connecteddevice.transport.spp.Connection;
-import com.google.android.connecteddevice.transport.spp.PendingConnection;
-import com.google.android.connecteddevice.transport.spp.PendingConnection.OnConnectedListener;
-import com.google.android.connecteddevice.transport.spp.PendingConnection.OnConnectionErrorListener;
-import com.google.android.connecteddevice.util.EventLog;
-import com.google.common.base.Objects;
-import java.util.UUID;
-
-/**
- * Communication manager that allows for targeted connections to a specific device from the car
- * using {@link ConnectedDeviceSppDelegateBinder} .
- */
-public class CarSppManager extends CarBluetoothManager {
-
-  private static final String TAG = "CarSppManager";
-
-  private final ConnectedDeviceSppDelegateBinder sppServiceBinder;
-
-  private final UUID associationServiceUuid;
-
-  private final int packetMaxBytes;
-
-  private String reconnectDeviceId;
-
-  @VisibleForTesting Connection currentConnection;
-
-  private PendingConnection currentPendingConnection;
-
-  /**
-   * Initialize a new instance of manager.
-   *
-   * @param sppBinder {@link ConnectedDeviceSppDelegateBinder} for establishing connection.
-   * @param connectedDeviceStorage Shared {@link ConnectedDeviceStorage} for companion features.
-   * @param packetMaxBytes Maximum size in bytes to write in one packet.
-   * @param enableCompression Enable compression on outgoing messages.
-   * @param isCapabilitiesEligible Association should attempt a capabilities exchange.
-   */
-  public CarSppManager(
-      @NonNull ConnectedDeviceSppDelegateBinder sppBinder,
-      @NonNull ConnectedDeviceStorage connectedDeviceStorage,
-      @NonNull UUID associationServiceUuid,
-      int packetMaxBytes,
-      boolean enableCompression,
-      boolean isCapabilitiesEligible) {
-    super(
-        connectedDeviceStorage,
-        enableCompression,
-        new BluetoothRfcommChannel(sppBinder),
-        isCapabilitiesEligible);
-    this.sppServiceBinder = sppBinder;
-    this.associationServiceUuid = associationServiceUuid;
-    this.packetMaxBytes = packetMaxBytes;
-  }
-
-  @Override
-  public void stop() {
-    super.stop();
-    reset();
-  }
-
-  @Override
-  public void disconnectDevice(@NonNull String deviceId) {
-    ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-    if (connectedDevice == null || !deviceId.equals(connectedDevice.deviceId)) {
-      return;
-    }
-    reset();
-  }
-
-  @Override
-  public void initiateConnectionToDevice(@NonNull UUID deviceId) {
-    logd(TAG, "Start spp reconnection listening for device with id: " + deviceId);
-    currentConnection = null;
-    reconnectDeviceId = deviceId.toString();
-
-    sppServiceBinder.unregisterConnectionCallback(associationServiceUuid);
-    sppServiceBinder.registerConnectionCallback(deviceId, reconnectOnErrorListener);
-    try {
-      PendingConnection pendingConnection =
-          sppServiceBinder.connectAsServer(deviceId, /* isSecure= */ true);
-
-      if (pendingConnection == null) {
-        return;
-      }
-      currentPendingConnection = pendingConnection;
-      pendingConnection
-          .setOnConnectedListener(reconnectOnConnectedListener)
-          .setOnConnectionErrorListener(onConnectionErrorListener);
-    } catch (RemoteException e) {
-      loge(TAG, "Error when start connection with remote device as server.", e);
-    }
-  }
-
-  @Override
-  public void reset() {
-    super.reset();
-    reconnectDeviceId = null;
-    if (currentConnection != null) {
-      try {
-        sppServiceBinder.disconnect(currentConnection);
-      } catch (RemoteException e) {
-        loge(TAG, "Error when try to disconnect with remote device.", e);
-      }
-      currentConnection = null;
-    }
-
-    if (currentPendingConnection != null) {
-      try {
-        sppServiceBinder.cancelConnectionAttempt(currentPendingConnection);
-      } catch (RemoteException e) {
-        loge(TAG, "Error when try to disconnect with remote device.", e);
-      }
-      currentPendingConnection = null;
-    }
-  }
-
-  /** Start the association by listening to incoming connect request. */
-  @Override
-  public void startAssociation(
-      @NonNull byte[] nameForAssociation, @NonNull AssociationCallback callback) {
-    if (reconnectDeviceId != null) {
-      sppServiceBinder.unregisterConnectionCallback(UUID.fromString(reconnectDeviceId));
-    }
-    reset();
-    setAssociationCallback(callback);
-    sppServiceBinder.registerConnectionCallback(associationServiceUuid, associationOnErrorListener);
-    try {
-      PendingConnection pendingConnection =
-          sppServiceBinder.connectAsServer(associationServiceUuid, /* isSecure= */ true);
-      if (pendingConnection == null) {
-        callback.onAssociationStartFailure();
-      } else {
-        currentPendingConnection = pendingConnection;
-        pendingConnection
-            .setOnConnectedListener(associationOnConnectedListener)
-            .setOnConnectionErrorListener(onConnectionErrorListener);
-        callback.onAssociationStartSuccess(
-            new StartAssociationResponse(
-                /* oobData= */ new OobData(new byte[0], new byte[0], new byte[0]),
-                /* deviceIdentifier= */ new byte[0],
-                /* deviceName= */ ""));
-      }
-    } catch (RemoteException e) {
-      callback.onAssociationStartFailure();
-      loge(TAG, "Error when try to start associate with remote device.", e);
-    }
-  }
-
-  private void onDeviceConnected(Connection connection, boolean isReconnect) {
-    currentConnection = connection;
-    currentPendingConnection = null;
-    EventLog.onDeviceConnected();
-    BluetoothDevice device = connection.getRemoteDevice();
-    setClientDeviceAddress(device.getAddress());
-    setClientDeviceName(connection.getRemoteDeviceName());
-    DeviceMessageStream secureStream =
-        new SppDeviceMessageStream(sppServiceBinder, connection, packetMaxBytes);
-    secureStream.setMessageReceivedErrorListener(
-        exception ->
-          disconnectWithError("Error occurred in stream: " + exception.getMessage(), exception));
-
-    ConnectedRemoteDevice connectedDevice = new ConnectedRemoteDevice(device, /* gatt= */ null);
-
-    if (isReconnect) {
-      // Capabilities are only exchanged during association so isCapabilitiesEligible is always
-      // false here.
-      ConnectionResolver connectionResolver =
-          new ConnectionResolver(secureStream, /* isCapabilitiesEligible= */ false);
-      connectionResolver.resolveConnection(
-          (resolvedConnection) -> {
-            ReconnectSecureChannel secureChannel =
-                new ReconnectSecureChannel(
-                    secureStream,
-                    storage,
-                    reconnectDeviceId,
-                    /* expectedChallengeResponse= */ null);
-            secureChannel.registerCallback(secureChannelCallback);
-            connectedDevice.secureChannel = secureChannel;
-            addConnectedDevice(connectedDevice);
-            setDeviceIdAndNotifyCallbacks(reconnectDeviceId);
-            reconnectDeviceId = null;
-          });
-    } else {
-      onDeviceConnectedForAssociation(secureStream, connectedDevice);
-    }
-  }
-
-  private final OnConnectedListener reconnectOnConnectedListener =
-      new OnConnectedListener() {
-        @Override
-        public void onConnected(
-            UUID uuid, BluetoothDevice remoteDevice, boolean isSecure, String deviceName) {
-          Connection connection =
-              new Connection(new ParcelUuid(uuid), remoteDevice, isSecure, deviceName);
-          onDeviceConnected(connection, /* isReconnect= */ true);
-        }
-      };
-
-  @VisibleForTesting
-  final OnErrorListener reconnectOnErrorListener =
-      new OnErrorListener() {
-
-        @Override
-        public void onError(Connection connection) {
-          if (!Objects.equal(currentConnection, connection)) {
-            loge(TAG, "Receive connection error callback on a unrecognized connection, ignored.");
-            return;
-          }
-          currentConnection = null;
-          reset();
-        }
-      };
-
-  private final OnConnectedListener associationOnConnectedListener =
-      new OnConnectedListener() {
-        @Override
-        public void onConnected(
-            UUID uuid, BluetoothDevice remoteDevice, boolean isSecure, String deviceName) {
-          Connection connection =
-              new Connection(new ParcelUuid(uuid), remoteDevice, isSecure, deviceName);
-          onDeviceConnected(connection, /* isReconnect= */ false);
-          ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-          if (connectedDevice == null || connectedDevice.secureChannel == null) {
-            loge(TAG, "No connected device or secure channel found when trying to associate.");
-            return;
-          }
-          ((AssociationSecureChannel) connectedDevice.secureChannel)
-              .setShowVerificationCodeListener(
-                  code -> {
-                    if (!isAssociating()) {
-                      loge(TAG, "No valid callback for association.");
-                      return;
-                    }
-                    getAssociationCallback().onVerificationCodeAvailable(code);
-                  });
-        }
-      };
-
-  final OnConnectionErrorListener onConnectionErrorListener =
-      new OnConnectionErrorListener() {
-        @Override
-        public void onConnectionError() {
-          loge(TAG, "Connection attempt failed when performing a server role.");
-        }
-      };
-
-  @VisibleForTesting
-  final OnErrorListener associationOnErrorListener =
-      new OnErrorListener() {
-
-        @Override
-        public void onError(Connection connection) {
-          if (!Objects.equal(currentConnection, connection)) {
-            loge(TAG, "Receive connection error callback on a unrecognized connection, ignored.");
-            return;
-          }
-          currentConnection = null;
-          if (isAssociating()) {
-            getAssociationCallback().onAssociationError(DEVICE_ERROR_UNEXPECTED_DISCONNECTION);
-          } else {
-            loge(TAG, "Encounter association error with no association callback registered.");
-          }
-          reset();
-        }
-      };
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/spp/SppDeviceMessageStream.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/spp/SppDeviceMessageStream.java
deleted file mode 100644
index e4a4d93..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/spp/SppDeviceMessageStream.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection.spp;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import android.os.RemoteException;
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.connecteddevice.connection.DeviceMessageStream;
-import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder;
-import com.google.android.connecteddevice.transport.spp.Connection;
-
-/** Spp message stream to a device. */
-class SppDeviceMessageStream extends DeviceMessageStream {
-
-  private static final String TAG = "SppDeviceMessageStream";
-
-  private final ConnectedDeviceSppDelegateBinder sppServiceBinder;
-  private final Connection connection;
-
-  SppDeviceMessageStream(
-      @NonNull ConnectedDeviceSppDelegateBinder sppBinder,
-      @NonNull Connection connection,
-      int maxWriteSize) {
-    super(maxWriteSize);
-    this.sppServiceBinder = sppBinder;
-    this.connection = connection;
-    this.sppServiceBinder.setOnMessageReceivedListener(connection, this::onMessageReceived);
-  }
-
-  @Override
-  protected void send(byte[] data) {
-    try {
-      sppServiceBinder.sendMessage(connection, data);
-    } catch (RemoteException e) {
-      loge(TAG, "Error when sending message to remote device");
-    }
-    sendCompleted();
-  }
-
-  @VisibleForTesting
-  void onMessageReceived(@NonNull byte[] value) {
-    logd(
-        TAG,
-        "Received a message from remote device("
-            + connection.getRemoteDevice().getAddress()
-            + ").");
-    onDataReceived(value);
-  }
-}
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 73c8d9b..7044d39 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/core/MultiProtocolDeviceController.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/core/MultiProtocolDeviceController.kt
@@ -16,6 +16,8 @@
 package com.google.android.connecteddevice.core
 
 import android.database.sqlite.SQLiteCantOpenDatabaseException
+import android.os.ParcelUuid
+import androidx.annotation.GuardedBy
 import androidx.annotation.VisibleForTesting
 import com.google.android.connecteddevice.api.IAssociationCallback
 import com.google.android.connecteddevice.connection.ChannelResolver
@@ -32,9 +34,10 @@
 import com.google.android.connecteddevice.model.StartAssociationResponse
 import com.google.android.connecteddevice.oob.OobRunner
 import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
+import com.google.android.connecteddevice.transport.ConnectChallenge
 import com.google.android.connecteddevice.transport.ConnectionProtocol
-import com.google.android.connecteddevice.transport.ConnectionProtocol.ConnectChallenge
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DiscoveryCallback
+import com.google.android.connecteddevice.transport.IDeviceDisconnectedListener
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
 import com.google.android.connecteddevice.transport.ProtocolDevice
 import com.google.android.connecteddevice.util.ByteUtils
 import com.google.android.connecteddevice.util.SafeLog.logd
@@ -46,11 +49,12 @@
 import java.util.Objects
 import java.util.UUID
 import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.CopyOnWriteArrayList
 import java.util.concurrent.CopyOnWriteArraySet
 import java.util.concurrent.Executor
 import java.util.concurrent.Executors
 import java.util.concurrent.atomic.AtomicReference
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
 
 /**
  * The controller to manage all the connected devices and connected protocols of each connected
@@ -84,8 +88,10 @@
 
   @VisibleForTesting internal val associationPendingDeviceId = AtomicReference<UUID?>(null)
 
-  private val associatedDevices = CopyOnWriteArrayList<AssociatedDevice>()
-  private val driverDevices = CopyOnWriteArrayList<AssociatedDevice>()
+  private val lock = ReentrantLock()
+
+  @GuardedBy("lock") private val associatedDevices = mutableListOf<AssociatedDevice>()
+  @GuardedBy("lock") private val driverDevices = mutableListOf<AssociatedDevice>()
 
   private val storageCallback =
     object : ConnectedDeviceStorage.AssociatedDeviceCallback {
@@ -106,31 +112,33 @@
 
   override val connectedDevices: List<ConnectedDevice>
     get() {
-      val devices = mutableListOf<ConnectedDevice>()
-      for (device in connectedRemoteDevices.values) {
-        val associatedDevice =
-          associatedDevices.firstOrNull { it.deviceId == device.deviceId.toString() }
-        if (associatedDevice == null) {
-          logd(
-            TAG,
-            "Unable to find a device with id ${device.deviceId} in associated devices. Skipped " +
-              "mapping."
+      lock.withLock {
+        val devices = mutableListOf<ConnectedDevice>()
+        for (device in connectedRemoteDevices.values) {
+          val associatedDevice =
+            associatedDevices.firstOrNull { it.deviceId == device.deviceId.toString() }
+          if (associatedDevice == null) {
+            logd(
+              TAG,
+              "Unable to find a device with id ${device.deviceId} in associated devices. Skipped " +
+                "mapping."
+            )
+            continue
+          }
+          val belongsToDriver = driverDevices.any { it.deviceId == associatedDevice.deviceId }
+          val hasSecureChannel = device.secureChannel != null
+          devices.add(
+            ConnectedDevice(
+              associatedDevice.deviceId,
+              associatedDevice.deviceName,
+              belongsToDriver,
+              hasSecureChannel
+            )
           )
-          continue
         }
-        val belongsToDriver = driverDevices.any { it.deviceId == associatedDevice.deviceId }
-        val hasSecureChannel = device.secureChannel != null
-        devices.add(
-          ConnectedDevice(
-            associatedDevice.deviceId,
-            associatedDevice.deviceName,
-            belongsToDriver,
-            hasSecureChannel
-          )
-        )
-      }
 
-      return devices
+        return devices
+      }
     }
 
   init {
@@ -181,7 +189,7 @@
     }
     for (protocol in protocols) {
       val discoveryCallback = generateConnectionDiscoveryCallback(deviceId, protocol, challenge)
-      protocol.startConnectionDiscovery(deviceId, challenge, discoveryCallback)
+      protocol.startConnectionDiscovery(ParcelUuid(deviceId), challenge, discoveryCallback)
     }
   }
 
@@ -206,7 +214,11 @@
     for (protocol in protocols) {
       val discoveryCallback =
         generateAssociationDiscoveryCallback(protocol, callback, startAssociationResponse)
-      protocol.startAssociationDiscovery(nameForAssociation, discoveryCallback, associationUuid)
+      protocol.startAssociationDiscovery(
+        nameForAssociation,
+        ParcelUuid(associationUuid),
+        discoveryCallback
+      )
     }
   }
 
@@ -262,7 +274,7 @@
   override fun disconnectDevice(deviceId: UUID) {
     logd(TAG, "Disconnecting device with id $deviceId.")
     for (protocol in protocols) {
-      protocol.stopConnectionDiscovery(deviceId)
+      protocol.stopConnectionDiscovery(ParcelUuid(deviceId))
     }
     val device = connectedRemoteDevices.remove(deviceId)
     if (device == null) {
@@ -319,10 +331,16 @@
       while (true) {
         try {
           logd(TAG, "Populating associated devices from storage.")
-          associatedDevices.clear()
-          driverDevices.clear()
-          associatedDevices.addAll(storage.allAssociatedDevices)
-          driverDevices.addAll(storage.driverAssociatedDevices)
+
+          // Fetch devices prior to applying lock to reduce lock time.
+          val driverOnlyDevices = storage.driverAssociatedDevices
+          val allDevices = storage.allAssociatedDevices
+          lock.withLock {
+            associatedDevices.clear()
+            associatedDevices.addAll(allDevices)
+            driverDevices.clear()
+            driverDevices.addAll(driverOnlyDevices)
+          }
           logd(TAG, "Devices populated successfully.")
           break
         } catch (sqliteException: SQLiteCantOpenDatabaseException) {
@@ -368,7 +386,7 @@
     protocol: ConnectionProtocol,
     challenge: ConnectChallenge
   ) =
-    object : DiscoveryCallback {
+    object : IDiscoveryCallback.Stub() {
       override fun onDeviceConnected(protocolId: String) {
         logd(
           TAG,
@@ -376,8 +394,7 @@
         )
         protocol.registerDeviceDisconnectedListener(
           protocolId,
-          generateDeviceDisconnectedListener(deviceId, protocol),
-          callbackExecutor
+          generateDeviceDisconnectedListener(deviceId, protocol)
         )
         val protocolDevice = ProtocolDevice(protocol, protocolId)
         val device =
@@ -424,7 +441,7 @@
     associationCallback: IAssociationCallback,
     response: StartAssociationResponse
   ) =
-    object : DiscoveryCallback {
+    object : IDiscoveryCallback.Stub() {
       override fun onDeviceConnected(protocolId: String) {
         logd(TAG, "New connection protocol connected, id: $protocolId, protocol: $protocol")
         val protocolDevice = ProtocolDevice(protocol, protocolId)
@@ -440,8 +457,7 @@
         }
         protocol.registerDeviceDisconnectedListener(
           protocolId,
-          generateDeviceDisconnectedListener(pendingId, protocol),
-          callbackExecutor
+          generateDeviceDisconnectedListener(pendingId, protocol)
         )
         // The channel only needs to be resolved once for all protocols connected to one remote
         // device.
@@ -545,7 +561,7 @@
     deviceId: UUID,
     protocol: ConnectionProtocol,
   ) =
-    object : ConnectionProtocol.DeviceDisconnectedListener {
+    object : IDeviceDisconnectedListener.Stub() {
       override fun onDeviceDisconnected(protocolId: String) {
         logd(TAG, "Remote connect protocol disconnected, id: $protocolId, protocol: $protocol")
         connectedRemoteDevices.compute(deviceId) { deviceId, device ->
@@ -727,8 +743,7 @@
     for (protocolDevice in newDevice.protocolDevices) {
       protocolDevice.protocol.registerDeviceDisconnectedListener(
         protocolDevice.protocolId,
-        generateDeviceDisconnectedListener(deviceId, protocolDevice.protocol),
-        callbackExecutor
+        generateDeviceDisconnectedListener(deviceId, protocolDevice.protocol)
       )
     }
     return newDevice
@@ -742,15 +757,17 @@
         /* deviceName= */ null,
         /* isConnectionEnabled= */ true
       )
-    if (enablePassenger) {
-      logd(TAG, "Saving newly associated device $deviceId as unclaimed.")
-      storage.addAssociatedDeviceForUser(AssociatedDevice.UNCLAIMED_USER_ID, associatedDevice)
-    } else {
-      logd(TAG, "Saving newly associated device $deviceId as a driver's device.")
-      storage.addAssociatedDeviceForDriver(associatedDevice)
-      driverDevices.add(associatedDevice)
+    lock.withLock {
+      if (enablePassenger) {
+        logd(TAG, "Saving newly associated device $deviceId as unclaimed.")
+        storage.addAssociatedDeviceForUser(AssociatedDevice.UNCLAIMED_USER_ID, associatedDevice)
+      } else {
+        logd(TAG, "Saving newly associated device $deviceId as a driver's device.")
+        storage.addAssociatedDeviceForDriver(associatedDevice)
+        driverDevices.add(associatedDevice)
+      }
+      associatedDevices.add(associatedDevice)
     }
-    associatedDevices.add(associatedDevice)
   }
 
   /**
@@ -761,7 +778,7 @@
     device: ConnectedRemoteDevice,
     onCallback: (ConnectedDevice, Callback) -> Unit
   ) {
-    val connectedDevice = device.toConnectedDevice(driverDevices)
+    val connectedDevice = lock.withLock { device.toConnectedDevice(driverDevices) }
     callbacks.invoke { onCallback(connectedDevice, it) }
   }
 
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/model/AssociatedDeviceDetails.java b/libs/connecteddevice/src/com/google/android/connecteddevice/model/AssociatedDeviceDetails.java
deleted file mode 100644
index 2061543..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/model/AssociatedDeviceDetails.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.model;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-/** Class that contains the details of an associated device. */
-public class AssociatedDeviceDetails {
-  private final AssociatedDevice device;
-
-  private final boolean isConnected;
-
-  public AssociatedDeviceDetails(@NonNull AssociatedDevice device, boolean isConnected) {
-    this.device = device;
-    this.isConnected = isConnected;
-  }
-
-  /** Get the device id. */
-  @NonNull
-  public String getDeviceId() {
-    return device.getDeviceId();
-  }
-
-  /** Get the name of the associated device. */
-  @Nullable
-  public String getDeviceName() {
-    return device.getDeviceName();
-  }
-
-  /** Get the device address. */
-  @NonNull
-  public String getDeviceAddress() {
-    return device.getDeviceAddress();
-  }
-
-  /** {@code true} if the connection is enabled for the device. */
-  public boolean isConnectionEnabled() {
-    return device.isConnectionEnabled();
-  }
-
-  /** {@code true} if the device is connected. */
-  public boolean isConnected() {
-    return isConnected;
-  }
-
-  /** Returns the claiming user id, or {@value AssociatedDevice#UNCLAIMED_USER_ID} if unclaimed. */
-  public int getUserId() {
-    return device.getUserId();
-  }
-
-  /** Get {@link AssociatedDevice}. */
-  @NonNull
-  public AssociatedDevice getAssociatedDevice() {
-    return device;
-  }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.java b/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.java
deleted file mode 100644
index 3f21931..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.model;
-
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import java.lang.annotation.Retention;
-import java.util.Objects;
-
-/** Device that may be used for an out-of-band channel. */
-public class OobEligibleDevice implements Parcelable {
-
-  /** Device type */
-  @Retention(SOURCE)
-  @IntDef(value = {OOB_TYPE_BLUETOOTH})
-  public @interface OobType {}
-
-  public static final int OOB_TYPE_BLUETOOTH = 0;
-
-  private final String deviceAddress;
-
-  @OobType private final int oobType;
-
-  public OobEligibleDevice(@NonNull String deviceAddress, @OobType int oobType) {
-    this.deviceAddress = deviceAddress;
-    this.oobType = oobType;
-  }
-
-  private OobEligibleDevice(Parcel in) {
-    this(in.readString(), in.readInt());
-  }
-
-  @NonNull
-  public String getDeviceAddress() {
-    return deviceAddress;
-  }
-
-  @Override
-  public int describeContents() {
-    return 0;
-  }
-
-  @Override
-  public void writeToParcel(Parcel dest, int flags) {
-    dest.writeString(deviceAddress);
-    dest.writeInt(oobType);
-  }
-
-  @OobType
-  public int getOobType() {
-    return oobType;
-  }
-
-  @Override
-  public boolean equals(Object obj) {
-    if (obj == this) {
-      return true;
-    }
-    if (!(obj instanceof OobEligibleDevice)) {
-      return false;
-    }
-    OobEligibleDevice device = (OobEligibleDevice) obj;
-    return Objects.equals(device.deviceAddress, deviceAddress) && device.oobType == oobType;
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hash(deviceAddress, oobType);
-  }
-
-  public static final Parcelable.Creator<OobEligibleDevice> CREATOR =
-      new Parcelable.Creator<OobEligibleDevice>() {
-        @Override
-        public OobEligibleDevice createFromParcel(Parcel source) {
-          return new OobEligibleDevice(source);
-        }
-
-        @Override
-        public OobEligibleDevice[] newArray(int size) {
-          return new OobEligibleDevice[size];
-        }
-      };
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannel.java b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannel.java
index 51251bb..8f219bc 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannel.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannel.java
@@ -29,7 +29,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
-import com.google.android.connecteddevice.model.OobEligibleDevice;
 import com.google.android.connecteddevice.transport.BluetoothDeviceProvider;
 import com.google.android.connecteddevice.transport.ConnectionProtocol;
 import com.google.android.connecteddevice.transport.ProtocolDevice;
@@ -70,26 +69,16 @@
     BluetoothDevice remoteDevice =
         ((BluetoothDeviceProvider) protocol).getBluetoothDeviceById(device.getProtocolId());
     completeOobDataExchange(
-        new OobEligibleDevice(remoteDevice.getAddress(), OobEligibleDevice.OOB_TYPE_BLUETOOTH),
-        callback,
-        () -> BluetoothAdapter.getDefaultAdapter().getBondedDevices());
+        remoteDevice, callback, () -> BluetoothAdapter.getDefaultAdapter().getBondedDevices());
     return true;
   }
 
-  @Override
-  public void completeOobDataExchange(
-      @NonNull OobEligibleDevice device, @NonNull Callback callback) {
-    completeOobDataExchange(
-        device, callback, () -> BluetoothAdapter.getDefaultAdapter().getBondedDevices());
-  }
-
   @VisibleForTesting
   void completeOobDataExchange(
-      OobEligibleDevice device, Callback callback, BondedDevicesResolver bondedDevicesResolver) {
+      BluetoothDevice remoteDevice,
+      Callback callback,
+      BondedDevicesResolver bondedDevicesResolver) {
     this.callback = callback;
-
-    BluetoothDevice remoteDevice =
-        BluetoothAdapter.getDefaultAdapter().getRemoteDevice(device.getDeviceAddress());
     Set<BluetoothDevice> bondedDevices = bondedDevicesResolver.getBondedDevices();
     if (bondedDevices == null || !bondedDevices.contains(remoteDevice)) {
       notifyFailure(
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobChannel.java b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobChannel.java
index 1bf8a1a..51b867b 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobChannel.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobChannel.java
@@ -17,7 +17,6 @@
 package com.google.android.connecteddevice.oob;
 
 import androidx.annotation.NonNull;
-import com.google.android.connecteddevice.model.OobEligibleDevice;
 import com.google.android.connecteddevice.transport.ProtocolDevice;
 
 /**
@@ -44,14 +43,6 @@
       @NonNull ProtocolDevice protocolDevice, @NonNull Callback callback);
 
   /**
-   * Exchange out of band data with a remote device. This must be done prior to the start of the
-   * association with that device.
-   *
-   * @param device The remote device to exchange out of band data with
-   */
-  void completeOobDataExchange(@NonNull OobEligibleDevice device, @NonNull Callback callback);
-
-  /**
    * Send raw data over the out of band channel
    *
    * @param oobData to be sent
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobConnectionManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobConnectionManager.java
deleted file mode 100644
index 5d51221..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobConnectionManager.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.oob;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import android.security.keystore.KeyProperties;
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-import com.google.common.primitives.Bytes;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Arrays;
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.KeyGenerator;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * This is a class that manages a token--{@link OobConnectionManager#encryptionKey}-- passed via an
- * out of band {@link OobChannel} that is distinct from the channel that is currently being secured.
- *
- * <p>Intended usage:
- *
- * <pre>{@code
- * OobConnectionManager oobConncetionManager = new OobConnectionManager();
- * oobConnectionManager.startOobExchange(channel);
- *
- * }</pre>
- *
- * <pre>{@code When a message is received:
- *   verificationCode = OobConnectionManager#decryptVerificationCode(byte[])
- *   check that verification code is valid
- *   if it is:
- *     encryptedMessage =  OobConnectionManager#encryptVerificationCode(byte[])
- *     send encryptedMessage
- *     verify handshake
- *   otherwise:
- *     fail handshake
- * }</pre>
- *
- * <pre>{@code
- * when oobData is received via the out of band channel:
- *   OobConnectionManager#setOobData(byte[])
- *
- * encryptedMessage = OobConnectionManager#encryptVerificationCode(byte[])
- * sendMessage
- * when a message is received:
- *   verificationCode = OobConnectionManager#decryptVerificationCode(byte[])
- *   check that verification code is valid
- *   if it is:
- *     verify handshake
- *   otherwise:
- *     fail handshake
- * }</pre>
- */
-public class OobConnectionManager {
-  private static final String TAG = "OobConnectionManager";
-  private static final String ALGORITHM = "AES/GCM/NoPadding";
-  // The nonce length is chosen to be consistent with the standard specification:
-  // Section 8.2 of https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
-  @VisibleForTesting static final int NONCE_LENGTH_BYTES = 12;
-
-  private final Cipher cipher;
-  @VisibleForTesting byte[] encryptionIv = new byte[NONCE_LENGTH_BYTES];
-  @VisibleForTesting byte[] decryptionIv = new byte[NONCE_LENGTH_BYTES];
-  @VisibleForTesting SecretKey encryptionKey;
-
-  public OobConnectionManager() {
-    try {
-      cipher = Cipher.getInstance(ALGORITHM);
-    } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
-      loge(TAG, "Unable to create cipher with " + ALGORITHM + ".", e);
-      throw new IllegalStateException(e);
-    }
-  }
-
-  /** Encrypts {@code verificationCode} using {@link OobConnectionManager#encryptionKey} */
-  @NonNull
-  public byte[] encryptVerificationCode(@NonNull byte[] verificationCode)
-      throws InvalidAlgorithmParameterException, BadPaddingException, InvalidKeyException,
-          IllegalBlockSizeException {
-    cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new IvParameterSpec(encryptionIv));
-    return cipher.doFinal(verificationCode);
-  }
-
-  /** Decrypts {@code encryptedMessage} using {@link OobConnectionManager#encryptionKey} */
-  @NonNull
-  public byte[] decryptVerificationCode(@NonNull byte[] encryptedMessage)
-      throws InvalidAlgorithmParameterException, BadPaddingException, InvalidKeyException,
-          IllegalBlockSizeException {
-    cipher.init(Cipher.DECRYPT_MODE, encryptionKey, new IvParameterSpec(decryptionIv));
-    return cipher.doFinal(encryptedMessage);
-  }
-
-  void setOobData(@NonNull byte[] oobData) {
-    encryptionIv = Arrays.copyOf(oobData, NONCE_LENGTH_BYTES);
-    decryptionIv = Arrays.copyOfRange(oobData, NONCE_LENGTH_BYTES, NONCE_LENGTH_BYTES * 2);
-    encryptionKey =
-        new SecretKeySpec(
-            Arrays.copyOfRange(oobData, NONCE_LENGTH_BYTES * 2, oobData.length),
-            KeyProperties.KEY_ALGORITHM_AES);
-  }
-
-  /**
-   * Start the out of band exchange with a given {@link OobChannel}.
-   *
-   * @param oobChannel Channel to be used for exchange.
-   * @return {@code true} if exchange started successfully. {@code false} if an error occurred.
-   */
-  public boolean startOobExchange(@NonNull OobChannel oobChannel) {
-    KeyGenerator keyGenerator = null;
-    try {
-      keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES);
-    } catch (NoSuchAlgorithmException e) {
-      loge(TAG, "Unable to get AES key generator.", e);
-      return false;
-    }
-    encryptionKey = keyGenerator.generateKey();
-
-    SecureRandom secureRandom = new SecureRandom();
-    secureRandom.nextBytes(encryptionIv);
-    secureRandom.nextBytes(decryptionIv);
-
-    oobChannel.sendOobData(Bytes.concat(decryptionIv, encryptionIv, encryptionKey.getEncoded()));
-    return true;
-  }
-
-  /** Clears all stored encryption keys. */
-  public void reset() {
-    logd(TAG, "Resetting encryption keys.");
-    encryptionIv = new byte[NONCE_LENGTH_BYTES];
-    decryptionIv = new byte[NONCE_LENGTH_BYTES];
-    encryptionKey = null;
-  }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobRunner.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobRunner.kt
index fcfd44b..7a38881 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobRunner.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobRunner.kt
@@ -24,7 +24,6 @@
 import com.google.android.connecteddevice.transport.ProtocolDevice
 import com.google.android.connecteddevice.util.SafeLog.logd
 import com.google.android.connecteddevice.util.SafeLog.loge
-import com.google.android.connecteddevice.util.SafeLog.logw
 import com.google.protobuf.ByteString
 import java.security.NoSuchAlgorithmException
 import java.security.SecureRandom
@@ -81,30 +80,16 @@
   }
 
   /**
-   * Iterate through all available OOB channels, establish OOB channel and exchange OOB key with
-   * remote device, result will be returned through [callback].
+   * Iterate through all available OOB channels, establish OOB channels and send OOB data to remote
+   * device.
    */
-  open fun startOobDataExchange(
+  open fun sendOobData(
     protocolDevice: ProtocolDevice,
-    commonTypes: List<OobChannelType>,
-    securityVersion: Int,
-    callback: Callback? = null
   ): Boolean {
-    for (oobType in commonTypes) {
-      if (oobType.name !in supportedTypes) {
-        logw(TAG, "Unsupported OOB channel ${oobType.name}. Ignored.")
-        continue
-      }
+    for (oobType in supportedTypes.map { it.asOobChannelType() }) {
       logd(TAG, "Establish OOB channel with ${oobType.name}.")
       val oobChannel = oobChannelFactory.createOobChannel(oobType)
-      if (oobChannel.completeOobDataExchange(
-          protocolDevice,
-          generateOobChannelCallback(
-            oobChannel,
-            callback,
-            securityVersion >= MIN_SECURITY_VERSION_FOR_OOB_PROTO
-          )
-        )
+      if (oobChannel.completeOobDataExchange(protocolDevice, generateOobChannelCallback(oobChannel))
       ) {
         currentOobChannel = oobChannel
         return true
@@ -113,11 +98,11 @@
     return false
   }
 
-  private fun generateOobChannelCallback(
-    oobChannel: OobChannel,
-    callback: Callback?,
-    isProtoApplied: Boolean
-  ) =
+  private fun String.asOobChannelType(): OobChannelType =
+    OobChannelType.values().firstOrNull { it.name.equals(this) }
+      ?: OobChannelType.OOB_CHANNEL_UNKNOWN
+
+  private fun generateOobChannelCallback(oobChannel: OobChannel) =
     object : OobChannel.Callback {
       override fun onOobExchangeSuccess() {
         val data = oobData
@@ -127,14 +112,12 @@
             "OOB channel established successfully with invalid OOB data, issue failure " +
               "callback."
           )
-          callback?.onOobDataExchangeFailure()
           return
         }
-        oobChannel.sendOobData(if (isProtoApplied) toOobProto(data) else data.toRawBytes())
-        callback?.onOobDataExchangeSuccess()
+        oobChannel.sendOobData(toOobProto(data))
       }
       override fun onOobExchangeFailure() {
-        callback?.onOobDataExchangeFailure()
+        loge(TAG, "Failed to start OOB data exchange.")
       }
     }
 
@@ -182,21 +165,9 @@
       .toByteArray()
   }
 
-  private fun OobData.toRawBytes() = mobileIv + ihuIv + encryptionKey
-
-  /** Callbacks for [OobRunner.startOobDataExchange] */
-  interface Callback {
-    /** Called when [OobRunner.startOobDataExchange] finishes successfully. */
-    fun onOobDataExchangeSuccess()
-
-    /** Called when OOB data exchange fails. */
-    fun onOobDataExchangeFailure()
-  }
-
   companion object {
     private const val TAG = "OobRunner"
     private const val ALGORITHM = "AES/GCM/NoPadding"
-    @VisibleForTesting internal const val MIN_SECURITY_VERSION_FOR_OOB_PROTO = 4
 
     // The nonce length is chosen to be consistent with the standard specification:
     // Section 8.2 of https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/PassThroughChannel.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/PassThroughChannel.kt
index 70fb708..7ee2512 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/PassThroughChannel.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/PassThroughChannel.kt
@@ -33,11 +33,6 @@
     return true
   }
 
-  override fun completeOobDataExchange(
-    device: com.google.android.connecteddevice.model.OobEligibleDevice,
-    callback: OobChannel.Callback
-  ) {}
-
   override fun sendOobData(oobData: ByteArray) {}
 
   override fun interrupt() {}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/service/AssociationBinder.java b/libs/connecteddevice/src/com/google/android/connecteddevice/service/AssociationBinder.java
deleted file mode 100644
index e59e48f..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/AssociationBinder.java
+++ /dev/null
@@ -1,369 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.service;
-
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import android.os.IBinder;
-import android.os.RemoteException;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import com.google.android.connecteddevice.ConnectedDeviceManager;
-import com.google.android.connecteddevice.ConnectedDeviceManager.ConnectionCallback;
-import com.google.android.connecteddevice.ConnectedDeviceManager.DeviceAssociationCallback;
-import com.google.android.connecteddevice.api.IAssociatedDeviceManager;
-import com.google.android.connecteddevice.api.IAssociationCallback;
-import com.google.android.connecteddevice.api.IConnectionCallback;
-import com.google.android.connecteddevice.api.IDeviceAssociationCallback;
-import com.google.android.connecteddevice.api.IOnAssociatedDevicesRetrievedListener;
-import com.google.android.connecteddevice.connection.AssociationCallback;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.ConnectedDevice;
-import com.google.android.connecteddevice.model.StartAssociationResponse;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage.OnAssociatedDevicesRetrievedListener;
-import com.google.android.connecteddevice.util.RemoteCallbackBinder;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-/** Binder for exposing associated device actions to internal features. */
-public class AssociationBinder extends IAssociatedDeviceManager.Stub {
-
-  private static final String TAG = "AssociationBinder";
-
-  private final ConnectedDeviceManager connectedDeviceManager;
-
-  // Registered association callback
-  private IAssociationCallback iAssociationCallback;
-
-  // aidl callback binder -> device association callback
-  // Need to maintain a mapping in order to support unregistering callbacks.
-  private final Map<IBinder, DeviceAssociationCallback> deviceAssociationCallbacks =
-      new ConcurrentHashMap<>();
-
-  // aidl callback binder -> connection callback
-  // Need to maintain a mapping in order to support unregistering callbacks.
-  private final Map<IBinder, ConnectionCallback> connectionCallbacks = new ConcurrentHashMap<>();
-
-  private final Executor executor = Executors.newSingleThreadExecutor();
-  private final Set<RemoteCallbackBinder> callbackBinders = new CopyOnWriteArraySet<>();
-
-  public AssociationBinder(ConnectedDeviceManager connectedDeviceManager) {
-    this.connectedDeviceManager = connectedDeviceManager;
-  }
-
-  @Override
-  public void startAssociation(IAssociationCallback callback) {
-    RemoteCallbackBinder remoteBinder =
-        new RemoteCallbackBinder(callback.asBinder(), iBinder -> stopAssociation());
-    callbackBinders.add(remoteBinder);
-    iAssociationCallback = callback;
-    connectedDeviceManager.startAssociation(associationCallback);
-  }
-
-  @Override
-  public void stopAssociation() {
-    if (iAssociationCallback == null) {
-      return;
-    }
-    RemoteCallbackBinder remoteBinder = findRemoteCallbackBinder(iAssociationCallback.asBinder());
-    if (remoteBinder == null) {
-      return;
-    }
-    connectedDeviceManager.stopAssociation(associationCallback);
-    resetAssociationCallback();
-  }
-
-  @Override
-  public void retrievedActiveUserAssociatedDevices(
-      IOnAssociatedDevicesRetrievedListener remoteListener) {
-    OnAssociatedDevicesRetrievedListener listener =
-        devices -> {
-          try {
-            remoteListener.onAssociatedDevicesRetrieved(devices);
-          } catch (RemoteException exception) {
-            loge(TAG, "onAssociatedDevicesRetrieved failed.", exception);
-          }
-        };
-    connectedDeviceManager.retrieveActiveUserAssociatedDevices(listener);
-  }
-
-  @Override
-  public void acceptVerification() {
-    connectedDeviceManager.notifyOutOfBandAccepted();
-  }
-
-  @Override
-  public void removeAssociatedDevice(String deviceId) {
-    connectedDeviceManager.removeActiveUserAssociatedDevice(deviceId);
-  }
-
-  @Override
-  public void registerDeviceAssociationCallback(IDeviceAssociationCallback callback) {
-    DeviceAssociationCallback deviceAssociationCallback =
-        new DeviceAssociationCallback() {
-          @Override
-          public void onAssociatedDeviceAdded(AssociatedDevice device) {
-            try {
-              callback.onAssociatedDeviceAdded(device);
-            } catch (RemoteException exception) {
-              loge(TAG, "onAssociatedDeviceAdded failed.", exception);
-            }
-          }
-
-          @Override
-          public void onAssociatedDeviceRemoved(AssociatedDevice device) {
-            try {
-              callback.onAssociatedDeviceRemoved(device);
-            } catch (RemoteException exception) {
-              loge(TAG, "onAssociatedDeviceRemoved failed.", exception);
-            }
-          }
-
-          @Override
-          public void onAssociatedDeviceUpdated(AssociatedDevice device) {
-            try {
-              callback.onAssociatedDeviceUpdated(device);
-            } catch (RemoteException exception) {
-              loge(TAG, "onAssociatedDeviceUpdated failed.", exception);
-            }
-          }
-        };
-    connectedDeviceManager.registerDeviceAssociationCallback(deviceAssociationCallback, executor);
-    RemoteCallbackBinder remoteBinder =
-        new RemoteCallbackBinder(
-            callback.asBinder(), iBinder -> unregisterDeviceAssociationCallback(callback));
-    callbackBinders.add(remoteBinder);
-    deviceAssociationCallbacks.put(callback.asBinder(), deviceAssociationCallback);
-  }
-
-  @Override
-  public void unregisterDeviceAssociationCallback(IDeviceAssociationCallback callback) {
-    RemoteCallbackBinder remoteBinder = findRemoteCallbackBinder(callback.asBinder());
-    if (remoteBinder == null) {
-      loge(
-          TAG,
-          "Failed to unregister DeviceAssociationCallback "
-              + callback
-              + ", it has not been registered.");
-      return;
-    }
-    DeviceAssociationCallback deviceAssociationCallback =
-        deviceAssociationCallbacks.remove(callback.asBinder());
-    if (deviceAssociationCallback == null) {
-      loge(
-          TAG,
-          "Failed to unregister DeviceAssociationCallback "
-              + callback
-              + ", it has not been registered.");
-      return;
-    }
-    connectedDeviceManager.unregisterDeviceAssociationCallback(deviceAssociationCallback);
-    remoteBinder.cleanUp();
-    callbackBinders.remove(remoteBinder);
-  }
-
-  @Override
-  public List<ConnectedDevice> getActiveUserConnectedDevices() {
-    return connectedDeviceManager.getActiveUserConnectedDevices();
-  }
-
-  @Override
-  public void registerConnectionCallback(IConnectionCallback callback) {
-    ConnectionCallback connectionCallback =
-        new ConnectedDeviceManager.ConnectionCallback() {
-          @Override
-          public void onDeviceConnected(ConnectedDevice device) {
-            if (callback == null) {
-              loge(TAG, "No IConnectionCallback has been set, ignoring " + "onDeviceConnected.");
-              return;
-            }
-            try {
-              callback.onDeviceConnected(device);
-            } catch (RemoteException exception) {
-              loge(TAG, "onDeviceConnected failed.", exception);
-            }
-          }
-
-          @Override
-          public void onDeviceDisconnected(ConnectedDevice device) {
-            if (callback == null) {
-              loge(TAG, "No IConnectionCallback has been set, ignoring " + "onDeviceConnected.");
-              return;
-            }
-            try {
-              callback.onDeviceDisconnected(device);
-            } catch (RemoteException exception) {
-              loge(TAG, "onDeviceDisconnected failed.", exception);
-            }
-          }
-        };
-    connectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback, executor);
-    RemoteCallbackBinder remoteBinder =
-        new RemoteCallbackBinder(
-            callback.asBinder(), iBinder -> unregisterConnectionCallback(callback));
-    callbackBinders.add(remoteBinder);
-    connectionCallbacks.put(callback.asBinder(), connectionCallback);
-  }
-
-  @Override
-  public void unregisterConnectionCallback(@NonNull IConnectionCallback callback) {
-    RemoteCallbackBinder remoteBinder = findRemoteCallbackBinder(callback.asBinder());
-    if (remoteBinder == null) {
-      loge(
-          TAG,
-          "Failed to unregister ConnectionCallback "
-              + callback.asBinder()
-              + ", it was not been registered.");
-      return;
-    }
-    ConnectionCallback connectionCallback = connectionCallbacks.remove(callback.asBinder());
-    if (connectionCallback == null) {
-      loge(
-          TAG,
-          "Failed to unregister ConnectionCallback "
-              + callback.asBinder()
-              + ", it was not been registered.");
-      return;
-    }
-    connectedDeviceManager.unregisterConnectionCallback(connectionCallback);
-    remoteBinder.cleanUp();
-    callbackBinders.remove(remoteBinder);
-  }
-
-  @Override
-  public void enableAssociatedDeviceConnection(String deviceId) {
-    connectedDeviceManager.enableAssociatedDeviceConnection(deviceId);
-  }
-
-  @Override
-  public void disableAssociatedDeviceConnection(String deviceId) {
-    connectedDeviceManager.disableAssociatedDeviceConnection(deviceId);
-  }
-
-  @Nullable
-  private RemoteCallbackBinder findRemoteCallbackBinder(@NonNull IBinder binder) {
-    for (RemoteCallbackBinder remoteBinder : callbackBinders) {
-      if (remoteBinder.getCallbackBinder().equals(binder)) {
-        return remoteBinder;
-      }
-    }
-    return null;
-  }
-
-  private void resetAssociationCallback() {
-    RemoteCallbackBinder remoteBinder = findRemoteCallbackBinder(iAssociationCallback.asBinder());
-    if (remoteBinder != null) {
-      remoteBinder.cleanUp();
-      callbackBinders.remove(remoteBinder);
-    }
-    iAssociationCallback = null;
-  }
-
-  private final AssociationCallback associationCallback =
-      new AssociationCallback() {
-
-        @Override
-        public void onAssociationStartSuccess(StartAssociationResponse response) {
-          if (iAssociationCallback == null) {
-            loge(
-                TAG,
-                "No IAssociationCallback has been set, ignoring " + "onAssociationStartSuccess.");
-            return;
-          }
-          try {
-            iAssociationCallback.onAssociationStartSuccess(response);
-          } catch (RemoteException exception) {
-            loge(TAG, "onAssociationStartSuccess failed.", exception);
-          }
-        }
-
-        @Override
-        public void onAssociationStartFailure() {
-          if (iAssociationCallback == null) {
-            loge(
-                TAG,
-                "No IAssociationCallback has been set, ignoring " + "onAssociationStartFailure.");
-            return;
-          }
-          try {
-            iAssociationCallback.onAssociationStartFailure();
-          } catch (RemoteException exception) {
-            loge(TAG, "onAssociationStartFailure failed.", exception);
-          }
-          resetAssociationCallback();
-        }
-
-        @Override
-        public void onAssociationError(int error) {
-          if (iAssociationCallback == null) {
-            loge(
-                TAG,
-                "No IAssociationCallback has been set, ignoring "
-                    + "onAssociationError: "
-                    + error
-                    + ".");
-            return;
-          }
-          try {
-            iAssociationCallback.onAssociationError(error);
-          } catch (RemoteException exception) {
-            loge(TAG, "onAssociationError failed. Error: " + error + "", exception);
-          }
-          resetAssociationCallback();
-        }
-
-        @Override
-        public void onVerificationCodeAvailable(String code) {
-          if (iAssociationCallback == null) {
-            loge(
-                TAG,
-                "No IAssociationCallback has been set, ignoring "
-                    + "onVerificationCodeAvailable, code: "
-                    + code);
-            return;
-          }
-          try {
-            iAssociationCallback.onVerificationCodeAvailable(code);
-          } catch (RemoteException exception) {
-            loge(TAG, "onVerificationCodeAvailable failed. Code: " + code + "", exception);
-          }
-        }
-
-        @Override
-        public void onAssociationCompleted(String deviceId) {
-          if (iAssociationCallback == null) {
-            loge(
-                TAG,
-                "No IAssociationCallback has been set, ignoring "
-                    + "onAssociationCompleted, deviceId: "
-                    + deviceId);
-            return;
-          }
-          try {
-            iAssociationCallback.onAssociationCompleted();
-          } catch (RemoteException exception) {
-            loge(TAG, "onAssociationCompleted failed.", exception);
-          }
-          resetAssociationCallback();
-        }
-      };
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceFgUserService.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceFgUserService.kt
index 1fb95d0..5003a4f 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceFgUserService.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceFgUserService.kt
@@ -20,18 +20,19 @@
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
+import android.os.Handler
 import android.os.IBinder
+import android.os.Looper
 import android.os.UserManager
 import com.google.android.connecteddevice.api.CompanionConnector
 import com.google.android.connecteddevice.api.Connector
-import com.google.android.connecteddevice.api.IConnectedDeviceManager
 import com.google.android.connecteddevice.util.SafeLog.logd
 import com.google.android.connecteddevice.util.SafeLog.loge
+import java.time.Duration
 
 /**
  * Service responsible for starting services that must be run in the active foreground user.
- * External features running in the foreground user may bind with `ACTION_BIND_REMOTE_FEATURE` to
- * access the [IConnectedDeviceManager] instance.
+ * External features running in the foreground user must bind to this service.
  */
 class ConnectedDeviceFgUserService : TrunkService() {
   private lateinit var connector: CompanionConnector
@@ -41,17 +42,32 @@
   @SuppressLint("UnprotectedReceiver") // Broadcasts are protected.
   override fun onCreate() {
     super.onCreate()
+    logd(TAG, "Creating service.")
     connector = CompanionConnector(this)
     connector.callback =
       object : Connector.Callback {
+        override fun onConnected() {
+          logd(TAG, "Successfully connected to the companion platform.")
+          val userManager = getSystemService(UserManager::class.java)
+          if (userManager.isUserUnlocked) {
+            logd(TAG, "User was already unlocked. Starting unlock branch services.")
+            startBranchServices(META_UNLOCK_SERVICES)
+          }
+        }
+
         override fun onDisconnected() {
-          loge(TAG, "Lost connection to companion. Stopping service.")
+          loge(TAG, "Lost connection to the companion platform. Stopping service.")
           stopSelf()
         }
 
         override fun onFailedToConnect() {
-          loge(TAG, "Unable to establish connection with companion. Stopping service.")
-          stopSelf()
+          loge(
+            TAG,
+            "Unable to establish connection with the companion platform. Retrying in " +
+              "${RETRY_WAIT.toMillis()} ms."
+          )
+          Handler(Looper.myLooper() ?: Looper.getMainLooper())
+            .postDelayed({ connector.connect() }, RETRY_WAIT.toMillis())
         }
       }
     connector.connect()
@@ -62,12 +78,6 @@
     // Listen for user going to the background so we can clean up.
     registerReceiver(userBackgroundReceiver, IntentFilter(Intent.ACTION_USER_BACKGROUND))
     receiversRegistered = true
-
-    val userManager = getSystemService(UserManager::class.java)
-    if (userManager.isUserUnlocked) {
-      logd(TAG, "User was already unlocked on service start.")
-      onUserUnlocked()
-    }
   }
 
   override fun onDestroy() {
@@ -85,8 +95,11 @@
   }
 
   private fun onUserUnlocked() {
-    logd(TAG, "Starting unlock branch services.")
-    startBranchServices(META_UNLOCK_SERVICES)
+    logd(TAG, "User has been unlocked.")
+    if (connector.isConnected) {
+      logd(TAG, "Starting unlock branch services.")
+      startBranchServices(META_UNLOCK_SERVICES)
+    }
   }
 
   private fun unregisterReceivers() {
@@ -119,5 +132,7 @@
 
     /** `string-array` List of services to start after the user has unlocked. */
     private const val META_UNLOCK_SERVICES = "com.google.android.connecteddevice.unlock_services"
+
+    private val RETRY_WAIT = Duration.ofMillis(500)
   }
 }
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceManagerBinder.java b/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceManagerBinder.java
deleted file mode 100644
index 0afc74c..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceManagerBinder.java
+++ /dev/null
@@ -1,351 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.service;
-
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import android.os.IBinder;
-import android.os.ParcelUuid;
-import android.os.RemoteException;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import com.google.android.connecteddevice.ConnectedDeviceManager;
-import com.google.android.connecteddevice.ConnectedDeviceManager.ConnectionCallback;
-import com.google.android.connecteddevice.ConnectedDeviceManager.DeviceAssociationCallback;
-import com.google.android.connecteddevice.ConnectedDeviceManager.DeviceCallback;
-import com.google.android.connecteddevice.api.IConnectedDeviceManager;
-import com.google.android.connecteddevice.api.IConnectionCallback;
-import com.google.android.connecteddevice.api.IDeviceAssociationCallback;
-import com.google.android.connecteddevice.api.IDeviceCallback;
-import com.google.android.connecteddevice.api.IOnLogRequestedListener;
-import com.google.android.connecteddevice.logging.LoggingManager;
-import com.google.android.connecteddevice.logging.LoggingManager.OnLogRequestedListener;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.ConnectedDevice;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.util.RemoteCallbackBinder;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-/** Binder for exposing ConnectedDeviceManager to features. */
-public class ConnectedDeviceManagerBinder extends IConnectedDeviceManager.Stub {
-
-  private static final String TAG = "ConnectedDeviceManagerBinder";
-
-  private final ConnectedDeviceManager connectedDeviceManager;
-  private final LoggingManager loggingManager;
-
-  // aidl callback binder -> connection callback
-  // Need to maintain a mapping in order to support unregistering callbacks.
-  private final ConcurrentHashMap<IBinder, ConnectionCallback> connectionCallbacks =
-      new ConcurrentHashMap<>();
-
-  // aidl callback binder -> device association callback
-  // Need to maintain a mapping in order to support unregistering callbacks.
-  private final ConcurrentHashMap<IBinder, DeviceAssociationCallback> associationCallbacks =
-      new ConcurrentHashMap<>();
-
-  // aidl callback binder  -> device callback
-  // Need to maintain a mapping in order to support unregistering callbacks.
-  private final ConcurrentHashMap<IBinder, DeviceCallback> deviceCallbacks =
-      new ConcurrentHashMap<>();
-
-  // aidl listener binder -> log requested listener
-  // Need to maintain a mapping in order to support unregistering callbacks.
-  private final ConcurrentHashMap<IBinder, OnLogRequestedListener> logRequestedListeners =
-      new ConcurrentHashMap<>();
-
-  private final Executor callbackExecutor = Executors.newSingleThreadExecutor();
-  private final Set<RemoteCallbackBinder> callbackBinders = new CopyOnWriteArraySet<>();
-
-  public ConnectedDeviceManagerBinder(
-      ConnectedDeviceManager connectedDeviceManager, LoggingManager loggingManager) {
-    this.connectedDeviceManager = connectedDeviceManager;
-    this.loggingManager = loggingManager;
-  }
-
-  @Override
-  public List<ConnectedDevice> getActiveUserConnectedDevices() {
-    return connectedDeviceManager.getActiveUserConnectedDevices();
-  }
-
-  @Override
-  public void registerActiveUserConnectionCallback(@NonNull IConnectionCallback callback) {
-    ConnectionCallback connectionCallback =
-        new ConnectionCallback() {
-          @Override
-          public void onDeviceConnected(ConnectedDevice device) {
-            try {
-              callback.onDeviceConnected(device);
-            } catch (RemoteException exception) {
-              loge(TAG, "onDeviceConnected failed.", exception);
-            }
-          }
-
-          @Override
-          public void onDeviceDisconnected(ConnectedDevice device) {
-            try {
-              callback.onDeviceDisconnected(device);
-            } catch (RemoteException exception) {
-              loge(TAG, "onDeviceDisconnected failed.", exception);
-            }
-          }
-        };
-    connectedDeviceManager.registerActiveUserConnectionCallback(
-        connectionCallback, callbackExecutor);
-    RemoteCallbackBinder remoteBinder =
-        new RemoteCallbackBinder(
-            callback.asBinder(), iBinder -> unregisterConnectionCallback(callback));
-    callbackBinders.add(remoteBinder);
-    connectionCallbacks.put(callback.asBinder(), connectionCallback);
-  }
-
-  @Override
-  public void unregisterConnectionCallback(@NonNull IConnectionCallback callback) {
-    IBinder binder = callback.asBinder();
-    RemoteCallbackBinder remoteBinder = findRemoteCallbackBinder(binder);
-    if (remoteBinder == null) {
-      loge(
-          TAG,
-          "RemoteCallbackBinder is null, IConnectionCallback was not " + "previously registered.");
-      return;
-    }
-    ConnectionCallback connectionCallback = connectionCallbacks.get(binder);
-    if (connectionCallback == null) {
-      loge(TAG, "ConnectionCallback is null.");
-      return;
-    }
-    connectedDeviceManager.unregisterConnectionCallback(connectionCallback);
-    remoteBinder.cleanUp();
-    callbackBinders.remove(remoteBinder);
-    connectionCallbacks.remove(binder);
-  }
-
-  @Override
-  public void registerDeviceCallback(
-      @NonNull ConnectedDevice connectedDevice,
-      @NonNull ParcelUuid recipientId,
-      @NonNull IDeviceCallback callback) {
-    DeviceCallback deviceCallback =
-        new DeviceCallback() {
-          @Override
-          public void onSecureChannelEstablished(ConnectedDevice device) {
-            try {
-              callback.onSecureChannelEstablished(device);
-            } catch (RemoteException exception) {
-              loge(TAG, "onSecureChannelEstablished failed.", exception);
-            }
-          }
-
-          @Override
-          public void onMessageReceived(ConnectedDevice device, DeviceMessage message) {
-            try {
-              callback.onMessageReceived(device, message);
-            } catch (RemoteException exception) {
-              loge(TAG, "onMessageReceived failed.", exception);
-            }
-          }
-
-          @Override
-          public void onDeviceError(ConnectedDevice device, int error) {
-            try {
-              callback.onDeviceError(device, error);
-            } catch (RemoteException exception) {
-              loge(TAG, "onDeviceError failed.", exception);
-            }
-          }
-        };
-    connectedDeviceManager.registerDeviceCallback(
-        connectedDevice,
-        recipientId.getUuid(),
-        deviceCallback,
-        callbackExecutor);
-    deviceCallbacks.put(callback.asBinder(), deviceCallback);
-    RemoteCallbackBinder remoteBinder =
-        new RemoteCallbackBinder(
-            callback.asBinder(),
-            iBinder -> unregisterDeviceCallback(connectedDevice, recipientId, callback));
-    callbackBinders.add(remoteBinder);
-  }
-
-  @Override
-  public void unregisterDeviceCallback(
-      @NonNull ConnectedDevice connectedDevice,
-      @NonNull ParcelUuid recipientId,
-      @NonNull IDeviceCallback callback) {
-    IBinder binder = callback.asBinder();
-    RemoteCallbackBinder remoteBinder = findRemoteCallbackBinder(binder);
-    if (remoteBinder == null) {
-      loge(
-          TAG,
-          "RemoteCallbackBinder is null, IDeviceCallback was not previously "
-              + "registered. Ignoring call to unregister.");
-      return;
-    }
-    DeviceCallback deviceCallback = deviceCallbacks.remove(binder);
-    if (deviceCallback == null) {
-      loge(TAG, "No DeviceCallback associated with given callback. " + "Cannot unregister.");
-      return;
-    }
-    connectedDeviceManager.unregisterDeviceCallback(
-        connectedDevice, recipientId.getUuid(), deviceCallback);
-    remoteBinder.cleanUp();
-    callbackBinders.remove(remoteBinder);
-  }
-
-  @Override
-  public boolean sendMessage(
-      @NonNull ConnectedDevice connectedDevice,
-      @NonNull DeviceMessage message) {
-    try {
-      connectedDeviceManager.sendMessage(connectedDevice, message);
-      return true;
-    } catch (IllegalStateException exception) {
-      loge(TAG, "Attempted to send message prior to secure channel established.", exception);
-    }
-    return false;
-  }
-
-  @Override
-  public void registerDeviceAssociationCallback(@NonNull IDeviceAssociationCallback callback) {
-    DeviceAssociationCallback associationCallback =
-        new DeviceAssociationCallback() {
-          @Override
-          public void onAssociatedDeviceAdded(AssociatedDevice device) {
-            try {
-              callback.onAssociatedDeviceAdded(device);
-            } catch (RemoteException exception) {
-              loge(TAG, "onAssociatedDeviceAdded failed.", exception);
-            }
-          }
-
-          @Override
-          public void onAssociatedDeviceRemoved(AssociatedDevice device) {
-            try {
-              callback.onAssociatedDeviceRemoved(device);
-            } catch (RemoteException exception) {
-              loge(TAG, "onAssociatedDeviceRemoved failed.", exception);
-            }
-          }
-
-          @Override
-          public void onAssociatedDeviceUpdated(AssociatedDevice device) {
-            try {
-              callback.onAssociatedDeviceUpdated(device);
-            } catch (RemoteException exception) {
-              loge(TAG, "onAssociatedDeviceUpdated failed.", exception);
-            }
-          }
-        };
-
-    connectedDeviceManager.registerDeviceAssociationCallback(
-        associationCallback, callbackExecutor);
-    RemoteCallbackBinder remoteBinder =
-        new RemoteCallbackBinder(
-            callback.asBinder(), iBinder -> unregisterDeviceAssociationCallback(callback));
-    callbackBinders.add(remoteBinder);
-    associationCallbacks.put(callback.asBinder(), associationCallback);
-  }
-
-  @Override
-  public void unregisterDeviceAssociationCallback(@NonNull IDeviceAssociationCallback callback) {
-    IBinder binder = callback.asBinder();
-    RemoteCallbackBinder remoteBinder = findRemoteCallbackBinder(binder);
-    if (remoteBinder == null) {
-      loge(
-          TAG,
-          "RemoteCallbackBinder is null, IDeviceAssociationCallback was "
-              + "not previously registered.");
-      return;
-    }
-    DeviceAssociationCallback associationCallback = associationCallbacks.remove(binder);
-    if (associationCallback == null) {
-      loge(TAG, "DeviceAssociationCallback is null.");
-      return;
-    }
-    connectedDeviceManager.unregisterDeviceAssociationCallback(associationCallback);
-    remoteBinder.cleanUp();
-    callbackBinders.remove(remoteBinder);
-  }
-
-  @Override
-  public void registerOnLogRequestedListener(
-      int loggerId, @NonNull IOnLogRequestedListener listener) {
-    OnLogRequestedListener onLogRequestedListener =
-        () -> {
-          try {
-            listener.onLogRecordsRequested();
-          } catch (RemoteException exception) {
-            loge(TAG, "Failed to notify log records requested.", exception);
-          }
-        };
-
-    loggingManager.registerLogRequestedListener(loggerId, onLogRequestedListener, callbackExecutor);
-    IBinder listenerBinder = listener.asBinder();
-    RemoteCallbackBinder remoteBinder =
-        new RemoteCallbackBinder(
-            listenerBinder, iBinder -> unregisterOnLogRequestedListener(loggerId, listener));
-    callbackBinders.add(remoteBinder);
-    logRequestedListeners.put(listenerBinder, onLogRequestedListener);
-  }
-
-  @Override
-  public void unregisterOnLogRequestedListener(
-      int loggerId, @NonNull IOnLogRequestedListener listener) {
-    IBinder listenerBinder = listener.asBinder();
-    RemoteCallbackBinder remoteBinder = findRemoteCallbackBinder(listenerBinder);
-    if (remoteBinder == null) {
-      loge(
-          TAG,
-          "RemoteCallbackBinder is null, IOnLogRequestedListener "
-              + listener
-              + " is not previously registered.");
-      return;
-    }
-    OnLogRequestedListener onLogRequestedListener = logRequestedListeners.remove(listenerBinder);
-    if (onLogRequestedListener == null) {
-      loge(
-          TAG,
-          "OnLogRequestedListener is null, IOnLogRequestedListener "
-              + listener
-              + " is not previously registered.");
-      return;
-    }
-    loggingManager.unregisterLogRequestedListener(loggerId, onLogRequestedListener);
-    remoteBinder.cleanUp();
-    callbackBinders.remove(listenerBinder);
-  }
-
-  @Override
-  public void processLogRecords(int loggerId, @NonNull byte[] logRecords) {
-    loggingManager.prepareLocalLogRecords(loggerId, logRecords);
-  }
-
-  @Nullable
-  private RemoteCallbackBinder findRemoteCallbackBinder(@NonNull IBinder binder) {
-    for (RemoteCallbackBinder remoteBinder : callbackBinders) {
-      if (remoteBinder.getCallbackBinder().equals(binder)) {
-        return remoteBinder;
-      }
-    }
-    return null;
-  }
-}
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 3daa450..39cb491 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceService.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceService.java
@@ -28,32 +28,15 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.os.IBinder;
-import android.os.ParcelUuid;
 import androidx.annotation.NonNull;
-import com.google.android.connecteddevice.ConnectedDeviceManager;
 import com.google.android.connecteddevice.api.CompanionConnector;
 import com.google.android.connecteddevice.api.Connector;
-import com.google.android.connecteddevice.api.IAssociatedDeviceManager;
-import com.google.android.connecteddevice.api.IAssociationCallback;
-import com.google.android.connecteddevice.api.IConnectedDeviceManager;
-import com.google.android.connecteddevice.api.IConnectionCallback;
-import com.google.android.connecteddevice.api.IDeviceAssociationCallback;
-import com.google.android.connecteddevice.api.IDeviceCallback;
-import com.google.android.connecteddevice.api.IFeatureCoordinator;
-import com.google.android.connecteddevice.api.IOnAssociatedDevicesRetrievedListener;
-import com.google.android.connecteddevice.api.IOnLogRequestedListener;
-import com.google.android.connecteddevice.connection.CarBluetoothManager;
-import com.google.android.connecteddevice.connection.ble.CarBlePeripheralManager;
-import com.google.android.connecteddevice.connection.spp.CarSppManager;
 import com.google.android.connecteddevice.core.DeviceController;
 import com.google.android.connecteddevice.core.FeatureCoordinator;
 import com.google.android.connecteddevice.core.MultiProtocolDeviceController;
 import com.google.android.connecteddevice.logging.LoggingFeature;
 import com.google.android.connecteddevice.logging.LoggingManager;
-import com.google.android.connecteddevice.model.ConnectedDevice;
-import com.google.android.connecteddevice.model.DeviceMessage;
 import com.google.android.connecteddevice.model.TransportProtocols;
-import com.google.android.connecteddevice.oob.BluetoothRfcommChannel;
 import com.google.android.connecteddevice.oob.OobChannelFactory;
 import com.google.android.connecteddevice.oob.OobRunner;
 import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
@@ -78,10 +61,7 @@
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-/**
- * Early start service that holds a {@link ConnectedDeviceManager} reference to support companion
- * device features.
- */
+/** Early start service that hosts the core companion platform. */
 public final class ConnectedDeviceService extends TrunkService {
 
   private static final String TAG = "ConnectedDeviceService";
@@ -90,11 +70,7 @@
   // reconnect advertisement every 6 minutes to avoid crossing a rotation.
   private static final Duration MAX_ADVERTISEMENT_DURATION = Duration.ofMinutes(6);
 
-  /**
-   * {@code string-array} Supported transport protocols. If the {@link FeatureCoordinator} is not
-   * enabled, the first protocol in the list will be selected to initialize the
-   * {@link ConnectedDeviceManager}.
-   */
+  /** {@code string-array} Supported transport protocols. */
   private static final String META_SUPPORTED_TRANSPORT_PROTOCOLS =
       "com.google.android.connecteddevice.transport_protocols";
   /** {@code String} UUID for association advertisement. */
@@ -120,19 +96,8 @@
   /** {@code int} Maximum number of bytes each SPP packet can contain. */
   private static final String META_SPP_PACKET_BYTES =
       "com.google.android.connecteddevice.spp_packet_bytes";
-  /** {@code boolean} Whether to compress outgoing messages. */
-  private static final String META_COMPRESS_OUTGOING_MESSAGES =
-      "com.google.android.connecteddevice.compress_outgoing_messages";
   /** {@code boolean} Enable BLE proxy. */
   private static final String META_ENABLE_PROXY = "com.google.android.connecteddevice.enable_proxy";
-  /** {@code boolean} Enable a capabilities exchange during association. */
-  private static final String META_ENABLE_CAPABILITIES_EXCHANGE =
-      "com.google.android.connecteddevice.enable_capabilities_exchange";
-  /**
-   * {@code boolean} Use the {@link IFeatureCoordinator} instead of {@link IConnectedDeviceManager}
-   */
-  private static final String META_ENABLE_FEATURE_COORDINATOR =
-      "com.google.android.connecteddevice.enable_feature_coordinator";
 
   private static final String META_SUPPORTED_OOB_CHANNELS =
       "com.google.android.connecteddevice.supported_oob_channels";
@@ -157,12 +122,6 @@
 
   private static final int DEFAULT_SPP_PACKET_SIZE = 700;
 
-  private static final boolean ENABLE_COMPRESSION_BY_DEFAULT = true;
-
-  private static final boolean ENABLE_CAPABILITIES_EXCHANGE_BY_DEFAULT = false;
-
-  private static final boolean ENABLE_FEATURE_COORDINATOR_BY_DEFAULT = false;
-
   private static final boolean ENABLE_PASSENGER_BY_DEFAULT = false;
 
   private static final String[] DEFAULT_TRANSPORT_PROTOCOLS =
@@ -185,22 +144,14 @@
   private final ScheduledExecutorService scheduledExecutorService =
       Executors.newSingleThreadScheduledExecutor();
 
-  private ConnectedDeviceManager connectedDeviceManager;
-
   private LoggingManager loggingManager;
 
   private FeatureCoordinator featureCoordinator;
 
   private ConnectedDeviceStorage storage;
 
-  private IConnectedDeviceManager.Stub connectedDeviceManagerBinder;
-
-  private IAssociatedDeviceManager.Stub associationBinder;
-
   private ConnectedDeviceSppDelegateBinder sppDelegateBinder;
 
-  private boolean useFeatureCoordinator;
-
   private List<String> supportedTransportProtocols;
 
   private SystemFeature systemFeature;
@@ -232,17 +183,11 @@
               + "this service. Reverting to default values.");
       supportedTransportProtocols = Arrays.asList(DEFAULT_TRANSPORT_PROTOCOLS);
     }
-    useFeatureCoordinator =
-        getMetaBoolean(META_ENABLE_FEATURE_COORDINATOR, ENABLE_FEATURE_COORDINATOR_BY_DEFAULT);
 
     loggingManager = new LoggingManager(this);
     storage = new ConnectedDeviceStorage(this);
     sppDelegateBinder = new ConnectedDeviceSppDelegateBinder(onRemoteCallbackSetListener);
-    if (useFeatureCoordinator) {
-      initializeFeatureCoordinator();
-    } else {
-      initializeConnectedDeviceManager();
-    }
+    initializeFeatureCoordinator();
 
     populateFeatures();
 
@@ -286,8 +231,6 @@
             protocols, storage, oobRunner, associationUuid, enablePassenger);
     featureCoordinator = new FeatureCoordinator(deviceController, storage, loggingManager);
     logd(TAG, "Wrapping FeatureCoordinator in legacy binders for backwards compatibility.");
-    connectedDeviceManagerBinder = createConnectedDeviceManagerWrapper();
-    associationBinder = createAssociatedDeviceManagerWrapper();
   }
 
   private BlePeripheralProtocol createBlePeripheralProtocol() {
@@ -328,117 +271,20 @@
     return new SppProtocol(sppDelegateBinder, maxSppPacketSize);
   }
 
-  private void initializeConnectedDeviceManager() {
-    logd(TAG, "Initializing ConnectedDeviceManager version of the platform.");
-    boolean isCompressionEnabled =
-        getMetaBoolean(META_COMPRESS_OUTGOING_MESSAGES, ENABLE_COMPRESSION_BY_DEFAULT);
-    boolean isCapabilitiesEligible =
-        getMetaBoolean(META_ENABLE_CAPABILITIES_EXCHANGE, ENABLE_CAPABILITIES_EXCHANGE_BY_DEFAULT);
-    CarBluetoothManager carBluetoothManager;
-    String defaultProtocol = supportedTransportProtocols.get(0);
-    logd(TAG, "Setting " + defaultProtocol + " as the transport.");
-    switch (defaultProtocol) {
-      case TransportProtocols.PROTOCOL_SPP:
-        carBluetoothManager =
-            createSppManager(storage, isCompressionEnabled, isCapabilitiesEligible);
-        break;
-      case TransportProtocols.PROTOCOL_BLE_PERIPHERAL:
-      default:
-        carBluetoothManager =
-            createBleManager(storage, isCompressionEnabled, isCapabilitiesEligible);
-    }
-    connectedDeviceManager = new ConnectedDeviceManager(carBluetoothManager, storage);
-    connectedDeviceManagerBinder =
-        new ConnectedDeviceManagerBinder(connectedDeviceManager, loggingManager);
-    associationBinder = new AssociationBinder(connectedDeviceManager);
-  }
-
   private void populateFeatures() {
     logd(TAG, "Populating features.");
-    if (featureCoordinator != null) {
-      loggingFeature =
-          new LoggingFeature(
-              this,
-              loggingManager,
-              CompanionConnector.createLocalConnector(
-                  this, Connector.USER_TYPE_DRIVER, featureCoordinator));
-      systemFeature =
-          new SystemFeature(
-              this,
-              storage,
-              CompanionConnector.createLocalConnector(
-                  this, Connector.USER_TYPE_DRIVER, featureCoordinator));
-    } else {
-      loggingFeature =
-          new LoggingFeature(
-              this,
-              loggingManager,
-              CompanionConnector.createLocalConnector(
-                  this, Connector.USER_TYPE_ALL, connectedDeviceManagerBinder, associationBinder));
-      systemFeature =
-          new SystemFeature(
-              this,
-              storage,
-              CompanionConnector.createLocalConnector(
-                  this, Connector.USER_TYPE_ALL, connectedDeviceManagerBinder, associationBinder));
-    }
-  }
-
-  private CarBluetoothManager createSppManager(
-      @NonNull ConnectedDeviceStorage storage,
-      boolean isCompressionEnabled,
-      boolean isCapabilitiesEligible) {
-    UUID sppServiceUuid = UUID.fromString(requireMetaString(META_ASSOCIATION_SERVICE_UUID));
-    int maxSppPacketSize = getMetaInt(META_SPP_PACKET_BYTES, DEFAULT_SPP_PACKET_SIZE);
-    return new CarSppManager(
-        sppDelegateBinder,
-        storage,
-        sppServiceUuid,
-        maxSppPacketSize,
-        isCompressionEnabled,
-        isCapabilitiesEligible);
-  }
-
-  private CarBluetoothManager createBleManager(
-      @NonNull ConnectedDeviceStorage storage,
-      boolean isCompressionEnabled,
-      boolean isCapabilitiesEligible) {
-    UUID associationUuid = UUID.fromString(requireMetaString(META_ASSOCIATION_SERVICE_UUID));
-    UUID reconnectUuid =
-        UUID.fromString(getMetaString(META_RECONNECT_SERVICE_UUID, DEFAULT_RECONNECT_UUID));
-    UUID reconnectDataUuid =
-        UUID.fromString(getMetaString(META_RECONNECT_DATA_UUID, DEFAULT_RECONNECT_DATA_UUID));
-    UUID advertiseDataCharacteristicUuid =
-        UUID.fromString(
-            getMetaString(
-                META_ADVERTISE_DATA_CHARACTERISTIC_UUID,
-                DEFAULT_ADVERTISE_DATA_CHARACTERISTIC_UUID));
-    UUID writeUuid = UUID.fromString(getMetaString(META_WRITE_UUID, DEFAULT_WRITE_UUID));
-    UUID readUuid = UUID.fromString(getMetaString(META_READ_UUID, DEFAULT_READ_UUID));
-    int defaultMtuSize = getMetaInt(META_DEFAULT_MTU_BYTES, DEFAULT_MTU_SIZE);
-    boolean isProxyEnabled = getMetaBoolean(META_ENABLE_PROXY, PROXY_ENABLED_BY_DEFAULT);
-    BlePeripheralManager blePeripheralManager;
-    if (isProxyEnabled) {
-      logi(TAG, "Initializing with ProxyBlePeripheralManager");
-      blePeripheralManager =
-          new ProxyBlePeripheralManager(new NetworkSocketFactory(this), scheduledExecutorService);
-    } else {
-      blePeripheralManager = new OnDeviceBlePeripheralManager(this);
-    }
-    return new CarBlePeripheralManager(
-        blePeripheralManager,
-        storage,
-        associationUuid,
-        reconnectUuid,
-        reconnectDataUuid,
-        advertiseDataCharacteristicUuid,
-        writeUuid,
-        readUuid,
-        MAX_ADVERTISEMENT_DURATION,
-        defaultMtuSize,
-        isCompressionEnabled,
-        new BluetoothRfcommChannel(sppDelegateBinder),
-        isCapabilitiesEligible);
+    loggingFeature =
+        new LoggingFeature(
+            this,
+            loggingManager,
+            CompanionConnector.createLocalConnector(
+                this, Connector.USER_TYPE_DRIVER, featureCoordinator));
+    systemFeature =
+        new SystemFeature(
+            this,
+            storage,
+            CompanionConnector.createLocalConnector(
+                this, Connector.USER_TYPE_ALL, featureCoordinator));
   }
 
   @Override
@@ -449,10 +295,6 @@
     logd(TAG, "Service bound. Action: " + intent.getAction());
     String action = intent.getAction();
     switch (action) {
-      case CompanionConnector.ACTION_BIND_ASSOCIATION:
-        return associationBinder;
-      case CompanionConnector.ACTION_BIND_REMOTE_FEATURE:
-        return connectedDeviceManagerBinder;
       case ConnectedDeviceSppDelegateBinder.ACTION_BIND_SPP:
         return sppDelegateBinder;
       case CompanionConnector.ACTION_BIND_FEATURE_COORDINATOR:
@@ -498,11 +340,7 @@
       logd(TAG, "Features are already cleaned up. No need to clean up again.");
       return;
     }
-    if (useFeatureCoordinator) {
-      featureCoordinator.reset();
-    } else {
-      connectedDeviceManager.reset();
-    }
+    featureCoordinator.reset();
     loggingManager.reset();
     systemFeature.stop();
     loggingFeature.stop();
@@ -519,11 +357,7 @@
             () -> {
               logd(TAG, "Initializing features.");
               loggingManager.start();
-              if (useFeatureCoordinator) {
-                featureCoordinator.start();
-              } else {
-                connectedDeviceManager.start();
-              }
+              featureCoordinator.start();
               systemFeature.start();
               loggingFeature.start();
             })
@@ -533,131 +367,4 @@
   private boolean isTransportSupported(@NonNull String protocol) {
     return supportedTransportProtocols.contains(protocol);
   }
-
-  private IConnectedDeviceManager.Stub createConnectedDeviceManagerWrapper() {
-    return new IConnectedDeviceManager.Stub() {
-      @Override
-      public List<ConnectedDevice> getActiveUserConnectedDevices() {
-        return featureCoordinator.getConnectedDevicesForDriver();
-      }
-
-      @Override
-      public void registerActiveUserConnectionCallback(IConnectionCallback callback) {
-        featureCoordinator.registerDriverConnectionCallback(callback);
-      }
-
-      @Override
-      public void unregisterConnectionCallback(IConnectionCallback callback) {
-        featureCoordinator.unregisterConnectionCallback(callback);
-      }
-
-      @Override
-      public void registerDeviceCallback(
-          ConnectedDevice connectedDevice, ParcelUuid recipientId, IDeviceCallback callback) {
-        featureCoordinator.registerDeviceCallback(connectedDevice, recipientId, callback);
-      }
-
-      @Override
-      public void unregisterDeviceCallback(
-          ConnectedDevice connectedDevice, ParcelUuid recipientId, IDeviceCallback callback) {
-        featureCoordinator.unregisterDeviceCallback(connectedDevice, recipientId, callback);
-      }
-
-      @Override
-      public boolean sendMessage(ConnectedDevice connectedDevice, DeviceMessage message) {
-        return featureCoordinator.sendMessage(connectedDevice, message);
-      }
-
-      @Override
-      public void registerDeviceAssociationCallback(IDeviceAssociationCallback callback) {
-        featureCoordinator.registerDeviceAssociationCallback(callback);
-      }
-
-      @Override
-      public void unregisterDeviceAssociationCallback(IDeviceAssociationCallback callback) {
-        featureCoordinator.unregisterDeviceAssociationCallback(callback);
-      }
-
-      @Override
-      public void registerOnLogRequestedListener(int loggerId, IOnLogRequestedListener listener) {
-        featureCoordinator.registerOnLogRequestedListener(loggerId, listener);
-      }
-
-      @Override
-      public void unregisterOnLogRequestedListener(int loggerId, IOnLogRequestedListener listener) {
-        featureCoordinator.unregisterOnLogRequestedListener(loggerId, listener);
-      }
-
-      @Override
-      public void processLogRecords(int loggerId, byte[] logRecords) {
-        featureCoordinator.processLogRecords(loggerId, logRecords);
-      }
-    };
-  }
-
-  private IAssociatedDeviceManager.Stub createAssociatedDeviceManagerWrapper() {
-    return new IAssociatedDeviceManager.Stub() {
-
-      @Override
-      public void startAssociation(IAssociationCallback callback) {
-        featureCoordinator.startAssociation(callback);
-      }
-
-      @Override
-      public void stopAssociation() {
-        featureCoordinator.stopAssociation();
-      }
-
-      @Override
-      public void retrievedActiveUserAssociatedDevices(
-          IOnAssociatedDevicesRetrievedListener listener) {
-        featureCoordinator.retrieveAssociatedDevicesForDriver(listener);
-      }
-
-      @Override
-      public void acceptVerification() {
-        featureCoordinator.acceptVerification();
-      }
-
-      @Override
-      public void removeAssociatedDevice(String deviceId) {
-        featureCoordinator.removeAssociatedDevice(deviceId);
-      }
-
-      @Override
-      public void registerDeviceAssociationCallback(IDeviceAssociationCallback callback) {
-        featureCoordinator.registerDeviceAssociationCallback(callback);
-      }
-
-      @Override
-      public void unregisterDeviceAssociationCallback(IDeviceAssociationCallback callback) {
-        featureCoordinator.unregisterDeviceAssociationCallback(callback);
-      }
-
-      @Override
-      public List<ConnectedDevice> getActiveUserConnectedDevices() {
-        return featureCoordinator.getConnectedDevicesForDriver();
-      }
-
-      @Override
-      public void registerConnectionCallback(IConnectionCallback callback) {
-        featureCoordinator.registerDriverConnectionCallback(callback);
-      }
-
-      @Override
-      public void unregisterConnectionCallback(IConnectionCallback callback) {
-        featureCoordinator.unregisterConnectionCallback(callback);
-      }
-
-      @Override
-      public void enableAssociatedDeviceConnection(String deviceId) {
-        featureCoordinator.enableAssociatedDeviceConnection(deviceId);
-      }
-
-      @Override
-      public void disableAssociatedDeviceConnection(String deviceId) {
-        featureCoordinator.disableAssociatedDeviceConnection(deviceId);
-      }
-    };
-  }
 }
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/service/TrunkService.java b/libs/connecteddevice/src/com/google/android/connecteddevice/service/TrunkService.java
index 8736bf3..71ecd33 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/TrunkService.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/service/TrunkService.java
@@ -115,7 +115,7 @@
     if (attempts > MAX_BIND_ATTEMPTS) {
       loge(
           TAG,
-          "Failed to bind to " + flatComponentName + "after " + attempts + " attempts. Aborting.");
+          "Failed to bind to " + flatComponentName + " after " + attempts + " attempts. Aborting.");
       return;
     }
     logw(TAG, "Unable to bind to " + flatComponentName + ". Trying again.");
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectChallenge.aidl
similarity index 80%
rename from libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl
rename to libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectChallenge.aidl
index ca4f80b..a18116f 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectChallenge.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2022 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.
@@ -14,6 +14,6 @@
  * limitations under the License.
  */
 
-package com.google.android.connecteddevice.model;
+package com.google.android.connecteddevice.transport;
 
-parcelable OobEligibleDevice;
+parcelable ConnectChallenge;
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectChallenge.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectChallenge.kt
new file mode 100644
index 0000000..da2db6b
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectChallenge.kt
@@ -0,0 +1,25 @@
+package com.google.android.connecteddevice.transport
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+/** Container class to hold the connect challenge the salt that generated the challenge. */
+@Parcelize
+data class ConnectChallenge(val challenge: ByteArray, val salt: ByteArray) : Parcelable {
+  override fun equals(other: Any?): Boolean {
+    if (this === other) {
+      return true
+    }
+    if (other !is ConnectChallenge) {
+      return false
+    }
+
+    return challenge.contentEquals(other.challenge) && salt.contentEquals(other.salt)
+  }
+
+  override fun hashCode(): Int {
+    var result = challenge.contentHashCode()
+    result = 31 * result + salt.contentHashCode()
+    return result
+  }
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectionProtocol.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectionProtocol.kt
index 7fc6616..da7b0b4 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectionProtocol.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectionProtocol.kt
@@ -15,77 +15,80 @@
  */
 package com.google.android.connecteddevice.transport
 
+import android.os.ParcelUuid
 import androidx.annotation.CallSuper
+import com.google.android.connecteddevice.util.AidlThreadSafeCallbacks
 import com.google.android.connecteddevice.util.SafeLog.logd
 import com.google.android.connecteddevice.util.SafeLog.logw
-import com.google.android.connecteddevice.util.ThreadSafeCallbacks
-import java.util.UUID
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.CopyOnWriteArrayList
 import java.util.concurrent.Executor
+import java.util.concurrent.Executors
 
 /**
  * Representation of a communication protocol that provides actions and event notifications for
  * interacting with devices.
  */
-abstract class ConnectionProtocol {
+abstract class ConnectionProtocol(
+  private val callbackExecutor: Executor = Executors.newCachedThreadPool()
+) : IConnectionProtocol.Stub() {
   protected val dataReceivedListeners:
-    MutableMap<String, ThreadSafeCallbacks<DataReceivedListener>> =
+    MutableMap<String, AidlThreadSafeCallbacks<IDataReceivedListener>> =
     ConcurrentHashMap()
 
   protected val deviceDisconnectedListeners:
-    MutableMap<String, ThreadSafeCallbacks<DeviceDisconnectedListener>> =
+    MutableMap<String, AidlThreadSafeCallbacks<IDeviceDisconnectedListener>> =
     ConcurrentHashMap()
 
   protected val maxDataSizeChangedListeners:
-    MutableMap<String, ThreadSafeCallbacks<DeviceMaxDataSizeChangedListener>> =
+    MutableMap<String, AidlThreadSafeCallbacks<IDeviceMaxDataSizeChangedListener>> =
     ConcurrentHashMap()
 
-  protected val missedData: MutableMap<String, MutableList<ByteArray>> = ConcurrentHashMap()
+  private val missedData: MutableMap<String, MutableList<ByteArray>> = ConcurrentHashMap()
 
   /**
    * `true` if challenge exchange is required to verify the remote device for establishing a secure
    * channel over this [ConnectionProtocol].
    */
-  abstract val isDeviceVerificationRequired: Boolean
+  abstract override fun isDeviceVerificationRequired(): Boolean
 
   /**
    * Begin the discovery process with [name] and [identifier] for a new device to associate with.
    */
-  abstract fun startAssociationDiscovery(
+  abstract override fun startAssociationDiscovery(
     name: String,
-    callback: DiscoveryCallback,
-    identifier: UUID
+    identifier: ParcelUuid,
+    callback: IDiscoveryCallback
   )
 
   /**
    * Begin the discovery process for a device that will respond to the supplied [id] with
    * [challenge].
    */
-  abstract fun startConnectionDiscovery(
-    id: UUID,
+  abstract override fun startConnectionDiscovery(
+    id: ParcelUuid,
     challenge: ConnectChallenge,
-    callback: DiscoveryCallback
+    callback: IDiscoveryCallback
   )
 
   /** Stop an ongoing association discovery. */
-  abstract fun stopAssociationDiscovery()
+  abstract override fun stopAssociationDiscovery()
 
   /** Stop an ongoing connection discovery for the provided device. */
-  abstract fun stopConnectionDiscovery(id: UUID)
+  abstract override fun stopConnectionDiscovery(id: ParcelUuid)
 
   /** Send data to a device. */
-  abstract fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback? = null)
+  abstract override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?)
 
   /** Disconnect a specific device. */
-  abstract fun disconnectDevice(protocolId: String)
+  abstract override fun disconnectDevice(protocolId: String)
 
   /**
    * Disconnect all active connections, cancel any discoveries in progress, and clean up to a
    * neutral state.
    */
   @CallSuper
-  open fun reset() {
+  override fun reset() {
     deviceDisconnectedListeners.clear()
     dataReceivedListeners.clear()
     maxDataSizeChangedListeners.clear()
@@ -96,49 +99,43 @@
    * Returns the maximum number of bytes that can be written in a single message for the device
    * matching the [protocolId].
    */
-  abstract fun getMaxWriteSize(protocolId: String): Int
+  abstract override fun getMaxWriteSize(protocolId: String): Int
 
   /** Register a listener to be notified when data has been received on protocol [protocolId]. */
-  open fun registerDataReceivedListener(
-    protocolId: String,
-    listener: DataReceivedListener,
-    executor: Executor
-  ) {
+  override fun registerDataReceivedListener(protocolId: String, listener: IDataReceivedListener) {
     logd(TAG, "Registering a new DataReceivedListener.")
-    val listeners = dataReceivedListeners.computeIfAbsent(protocolId) { ThreadSafeCallbacks() }
-    listeners.add(listener, executor)
+    val listeners = dataReceivedListeners.computeIfAbsent(protocolId) { AidlThreadSafeCallbacks() }
+    listeners.add(listener, callbackExecutor)
     missedData.remove(protocolId)?.forEach { data ->
       listeners.invoke { it.onDataReceived(protocolId, data) }
     }
   }
 
   /** Register a listener to be notified when device has disconnected on protocol [protocolId]. */
-  open fun registerDeviceDisconnectedListener(
+  override fun registerDeviceDisconnectedListener(
     protocolId: String,
-    listener: DeviceDisconnectedListener,
-    executor: Executor
+    listener: IDeviceDisconnectedListener
   ) {
     deviceDisconnectedListeners
-      .computeIfAbsent(protocolId) { ThreadSafeCallbacks() }
-      .add(listener, executor)
+      .computeIfAbsent(protocolId) { AidlThreadSafeCallbacks() }
+      .add(listener, callbackExecutor)
   }
 
   /**
    * Register a listener to be notified when the protocol [protocolId] has negotiated a new maximum
    * data size.
    */
-  open fun registerDeviceMaxDataSizeChangedListener(
+  override fun registerDeviceMaxDataSizeChangedListener(
     protocolId: String,
-    listener: DeviceMaxDataSizeChangedListener,
-    executor: Executor
+    listener: IDeviceMaxDataSizeChangedListener
   ) {
     maxDataSizeChangedListeners
-      .computeIfAbsent(protocolId) { ThreadSafeCallbacks() }
-      .add(listener, executor)
+      .computeIfAbsent(protocolId) { AidlThreadSafeCallbacks() }
+      .add(listener, callbackExecutor)
   }
 
-  /** Unregister a previously registered [DataReceivedListener]. */
-  open fun unregisterDataReceivedListener(protocolId: String, listener: DataReceivedListener) {
+  /** Unregister a previously registered [listener]. */
+  override fun unregisterDataReceivedListener(protocolId: String, listener: IDataReceivedListener) {
     logd(TAG, "Removing data DataReceivedListener.")
     val listeners =
       dataReceivedListeners.getOrElse(protocolId) {
@@ -154,10 +151,10 @@
     }
   }
 
-  /** Unregister a previously registered [DeviceDisconnectedListener]. */
-  open fun unregisterDeviceDisconnectListener(
+  /** Unregister a previously registered [listener]. */
+  override fun unregisterDeviceDisconnectListener(
     protocolId: String,
-    listener: DeviceDisconnectedListener
+    listener: IDeviceDisconnectedListener
   ) {
     val listeners =
       deviceDisconnectedListeners.getOrElse(protocolId) {
@@ -173,10 +170,10 @@
     }
   }
 
-  /** Unregister a previously registered [DeviceMaxDataSizeChangedListener]. */
-  open fun unregisterDeviceMaxDataSizeChangedListener(
+  /** Unregister a previously registered [listener]. */
+  override fun unregisterDeviceMaxDataSizeChangedListener(
     protocolId: String,
-    listener: DeviceMaxDataSizeChangedListener
+    listener: IDeviceMaxDataSizeChangedListener
   ) {
     val listeners =
       maxDataSizeChangedListeners.getOrElse(protocolId) {
@@ -194,7 +191,7 @@
   }
 
   /** Removes registered listeners for connection [protocolId]. */
-  fun removeListeners(protocolId: String) {
+  override fun removeListeners(protocolId: String) {
     deviceDisconnectedListeners.remove(protocolId)
     dataReceivedListeners.remove(protocolId)
     maxDataSizeChangedListeners.remove(protocolId)
@@ -219,48 +216,6 @@
       }
   }
 
-  /** Container class to hold the connect challenge the salt that generated the challenge. */
-  data class ConnectChallenge(val challenge: ByteArray, val salt: ByteArray)
-
-  /** Event notifications on the discovery process. */
-  interface DiscoveryCallback {
-    /** Invoked when discovery for a device has started successfully. */
-    fun onDiscoveryStartedSuccessfully()
-
-    /** Invoked when discovery for a device failed to start. */
-    fun onDiscoveryFailedToStart()
-
-    /** Invoked when a device connection is established in response to the discovery. */
-    fun onDeviceConnected(protocolId: String)
-  }
-
-  /** Callback for the result of sending data. */
-  interface DataSendCallback {
-    /** Invoked when the data was successfully sent. */
-    fun onDataSentSuccessfully()
-
-    /** Invoked when the data failed to send. */
-    fun onDataFailedToSend()
-  }
-
-  /** Listener to be invoked when a device has disconnected. */
-  interface DeviceDisconnectedListener {
-    /** Called when the device has disconnected on protocol [protocolId]. */
-    fun onDeviceDisconnected(protocolId: String)
-  }
-
-  /** Listener to be invoked when data has been received from a device. */
-  interface DataReceivedListener {
-    /** Called when [data] is received from the remote device on protocol [protocolId]. */
-    fun onDataReceived(protocolId: String, data: ByteArray)
-  }
-
-  /** Listener to be invoked when the protocol has negotiated a new maximum data size. */
-  interface DeviceMaxDataSizeChangedListener {
-    /** Called when the protocol [protocolId] has negotiated a new maximum data size [maxBytes]. */
-    fun onDeviceMaxDataSizeChanged(protocolId: String, maxBytes: Int)
-  }
-
   companion object {
     private const val TAG = "ConnectionProtocol"
   }
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IConnectionProtocol.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IConnectionProtocol.aidl
new file mode 100644
index 0000000..40047e6
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IConnectionProtocol.aidl
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2022 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 com.google.android.connecteddevice.transport;
+
+import android.os.ParcelUuid;
+
+import com.google.android.connecteddevice.transport.ConnectChallenge;
+import com.google.android.connecteddevice.transport.IDataReceivedListener;
+import com.google.android.connecteddevice.transport.IDataSendCallback;
+import com.google.android.connecteddevice.transport.IDeviceDisconnectedListener;
+import com.google.android.connecteddevice.transport.IDeviceMaxDataSizeChangedListener;
+import com.google.android.connecteddevice.transport.IDiscoveryCallback;
+
+interface IConnectionProtocol {
+  /**
+   * Returns true if challenge exchange is required to verify the remote device for establishing a
+   * secure channel over this protocol.
+   */
+  boolean isDeviceVerificationRequired();
+
+  /**
+   * Begin the discovery process with the name and identifier for a new device to associate with.
+   */
+  void startAssociationDiscovery(
+      in String name,
+      in ParcelUuid identifier,
+      in IDiscoveryCallback callback);
+
+  /**
+   * Begin the discovery process for a device that will respond to the supplied id and challenge.
+   */
+  void startConnectionDiscovery(
+      in ParcelUuid id,
+      in ConnectChallenge challenge,
+      in IDiscoveryCallback callback);
+
+  /** Stop an ongoing association discovery. */
+  void stopAssociationDiscovery();
+
+  /** Stop an ongoing connection discovery for the provided device. */
+  void stopConnectionDiscovery(in ParcelUuid id);
+
+  /** Send data to a device. */
+  void sendData(in String protocolId, in byte[] data, in @nullable IDataSendCallback callback);
+
+  /** Disconnect a specific device. */
+  void disconnectDevice(in String protocolId);
+
+  /**
+   * Disconnect all active connections, cancel any discoveries in progress, and clean up to a
+   * neutral state.
+   */
+  void reset();
+
+  /**
+   * Returns the maximum number of bytes that can be written in a single message for the device
+   * matching the protocolId.
+   */
+  int getMaxWriteSize(in String protocolId);
+
+  /** Register a listener to be notified when data has been received on the specified  device. */
+  void registerDataReceivedListener(in String protocolId, in IDataReceivedListener listener);
+
+  /** Register a listener to be notified when device has disconnected. */
+  void registerDeviceDisconnectedListener(
+      in String protocolId,
+      in IDeviceDisconnectedListener listener);
+
+  /**
+   * Register a listener to be notified when the specified device has negotiated a new maximum
+   * data size.
+   */
+  void registerDeviceMaxDataSizeChangedListener(
+    in String protocolId,
+    in IDeviceMaxDataSizeChangedListener listener);
+
+  /** Unregister a previously registered listener. */
+  void unregisterDataReceivedListener(in String protocolId, in IDataReceivedListener listener);
+
+  /** Unregister a previously registered listener. */
+  void unregisterDeviceDisconnectListener(
+    in String protocolId,
+    in IDeviceDisconnectedListener listener);
+
+  /** Unregister a previously registered listener. */
+  void unregisterDeviceMaxDataSizeChangedListener(
+      in String protocolId,
+      in IDeviceMaxDataSizeChangedListener listener);
+
+  /** Removes registered listeners for the specified device. */
+  void removeListeners(in String protocolId);
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDataReceivedListener.aidl
similarity index 60%
copy from libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl
copy to libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDataReceivedListener.aidl
index ca4f80b..ab64f72 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDataReceivedListener.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2022 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.
@@ -14,6 +14,10 @@
  * limitations under the License.
  */
 
-package com.google.android.connecteddevice.model;
+package com.google.android.connecteddevice.transport;
 
-parcelable OobEligibleDevice;
+/** Listener to be invoked when data has been received from a device. */
+oneway interface IDataReceivedListener {
+  /** Called when [data] is received from the remote device on protocol protocolId. */
+  void onDataReceived(in String protocolId, in byte[] data);
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDataSendCallback.aidl
similarity index 60%
copy from libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl
copy to libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDataSendCallback.aidl
index ca4f80b..7add8ec 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDataSendCallback.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2022 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.
@@ -14,6 +14,13 @@
  * limitations under the License.
  */
 
-package com.google.android.connecteddevice.model;
+package com.google.android.connecteddevice.transport;
 
-parcelable OobEligibleDevice;
+/** Callback for the result of sending data. */
+oneway interface IDataSendCallback {
+  /** Invoked when the data was successfully sent. */
+  void onDataSentSuccessfully();
+
+  /** Invoked when the data failed to send. */
+  void onDataFailedToSend();
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDeviceDisconnectedListener.aidl
similarity index 62%
copy from libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl
copy to libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDeviceDisconnectedListener.aidl
index ca4f80b..239466a 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDeviceDisconnectedListener.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2022 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.
@@ -14,6 +14,10 @@
  * limitations under the License.
  */
 
-package com.google.android.connecteddevice.model;
+package com.google.android.connecteddevice.transport;
 
-parcelable OobEligibleDevice;
+/** Listener to be invoked when a device has disconnected. */
+oneway interface IDeviceDisconnectedListener {
+  /** Called when the device has disconnected on protocol protocolId. */
+  void onDeviceDisconnected(in String protocolId);
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDeviceMaxDataSizeChangedListener.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDeviceMaxDataSizeChangedListener.aidl
new file mode 100644
index 0000000..8dce7f1
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDeviceMaxDataSizeChangedListener.aidl
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 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 com.google.android.connecteddevice.transport;
+
+/** Listener to be invoked when the protocol has negotiated a new maximum data size. */
+oneway interface IDeviceMaxDataSizeChangedListener {
+  /** Called when the protocol [protocolId] has negotiated a new maximum data size [maxBytes]. */
+  void onDeviceMaxDataSizeChanged(in String protocolId, in int maxBytes);
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDiscoveryCallback.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDiscoveryCallback.aidl
new file mode 100644
index 0000000..b994385
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDiscoveryCallback.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 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 com.google.android.connecteddevice.transport;
+
+/** Event notifications on the discovery process. */
+oneway interface IDiscoveryCallback {
+  /** Invoked when discovery for a device has started successfully. */
+  void onDiscoveryStartedSuccessfully();
+
+  /** Invoked when discovery for a device failed to start. */
+  void onDiscoveryFailedToStart();
+
+  /** Invoked when a device connection is established in response to the discovery. */
+  void onDeviceConnected(in String protocolId);
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ble/BlePeripheralProtocol.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ble/BlePeripheralProtocol.kt
index 867191f..91d3ac9 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ble/BlePeripheralProtocol.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ble/BlePeripheralProtocol.kt
@@ -27,19 +27,26 @@
 import android.os.ParcelUuid
 import androidx.annotation.VisibleForTesting
 import com.google.android.connecteddevice.transport.BluetoothDeviceProvider
+import com.google.android.connecteddevice.transport.ConnectChallenge
 import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
 import com.google.android.connecteddevice.util.ByteUtils
 import com.google.android.connecteddevice.util.SafeLog.logd
 import com.google.android.connecteddevice.util.SafeLog.loge
 import com.google.android.connecteddevice.util.SafeLog.logw
 import java.time.Duration
 import java.util.UUID
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
 
 /**
  * A ble peripheral communication protocol that provides actions and event notifications for
  * interacting with devices.
  */
-class BlePeripheralProtocol(
+class BlePeripheralProtocol
+@JvmOverloads
+constructor(
   private val blePeripheralManager: BlePeripheralManager,
   private val reconnectServiceUuid: UUID,
   private val reconnectDataUuid: UUID,
@@ -48,9 +55,8 @@
   readCharacteristicUuid: UUID,
   private val maxReconnectAdvertisementDuration: Duration,
   defaultMtuSize: Int,
-) : ConnectionProtocol(), BluetoothDeviceProvider {
-  override val isDeviceVerificationRequired = true
-
+  callbackExecutor: Executor = Executors.newCachedThreadPool()
+) : ConnectionProtocol(callbackExecutor), BluetoothDeviceProvider {
   private val writeCharacteristic =
     BluetoothGattCharacteristic(
       writeCharacteristicUuid,
@@ -123,7 +129,11 @@
     val currentChallenge = connectChallenge
     if (currentDeviceId != null && currentDiscoveryCallback != null && currentChallenge != null) {
       reset()
-      startConnectionDiscovery(currentDeviceId, currentChallenge, currentDiscoveryCallback)
+      startConnectionDiscovery(
+        ParcelUuid(currentDeviceId),
+        currentChallenge,
+        currentDiscoveryCallback
+      )
     }
   }
 
@@ -134,9 +144,9 @@
   private var protocolId: String? = null
   private var connectChallenge: ConnectChallenge? = null
   private var advertiseCallback: AdvertiseCallback? = null
-  private var discoveryCallback: DiscoveryCallback? = null
+  private var discoveryCallback: IDiscoveryCallback? = null
   private var timeoutHandler: Handler? = null
-  private var dataSendCallback: DataSendCallback? = null
+  private var dataSendCallback: IDataSendCallback? = null
 
   init {
     writeCharacteristic.addDescriptor(createBluetoothGattDescriptor())
@@ -148,8 +158,8 @@
 
   override fun startAssociationDiscovery(
     name: String,
-    callback: DiscoveryCallback,
-    identifier: UUID
+    identifier: ParcelUuid,
+    callback: IDiscoveryCallback,
   ) {
     if (!isReadyToStartDiscovery()) {
       return
@@ -173,7 +183,7 @@
       }
     advertiseCallback = associationAdvertiseCallback
     startAdvertising(
-      identifier,
+      identifier.uuid,
       associationAdvertiseCallback,
       scanResponse = ByteUtils.hexStringToByteArray(name),
       scanResponseUuid = reconnectDataUuid
@@ -181,15 +191,15 @@
   }
 
   override fun startConnectionDiscovery(
-    id: UUID,
+    id: ParcelUuid,
     challenge: ConnectChallenge,
-    callback: DiscoveryCallback
+    callback: IDiscoveryCallback
   ) {
     if (!isReadyToStartDiscovery()) {
       return
     }
     reset()
-    deviceId = id
+    deviceId = id.uuid
     discoveryCallback = callback
     connectChallenge = challenge
     blePeripheralManager.registerCallback(peripheralCallback)
@@ -230,15 +240,15 @@
     reset()
   }
 
-  override fun stopConnectionDiscovery(id: UUID) {
-    if (id != deviceId || advertiseCallback == null) {
+  override fun stopConnectionDiscovery(id: ParcelUuid) {
+    if (id.uuid != deviceId || advertiseCallback == null) {
       logd(TAG, "No connection discovery is happening for device $id, ignoring.")
       return
     }
     reset()
   }
 
-  override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {
+  override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {
     val device: BluetoothDevice? = bluetoothDevice
     if (device == null) {
       loge(TAG, "Failed to send data, no connected device.")
@@ -309,6 +319,8 @@
   override fun getBluetoothDeviceById(protocolId: String) =
     if (protocolId == this.protocolId) bluetoothDevice else null
 
+  override fun isDeviceVerificationRequired(): Boolean = true
+
   private fun startAdvertising(
     serviceUuid: UUID,
     callback: AdvertiseCallback,
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/ConnectedDeviceSppDelegateBinder.java b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/ConnectedDeviceSppDelegateBinder.java
index 435a751..3a5f9d6 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/ConnectedDeviceSppDelegateBinder.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/ConnectedDeviceSppDelegateBinder.java
@@ -246,10 +246,12 @@
 
   public void registerConnectionCallback(
       @NonNull UUID serviceUuid, @Nullable OnErrorListener onErrorListener) {
+    logd(TAG, "Registering a new error listener for " + serviceUuid + ".");
     connectionErrorListeners.put(serviceUuid, onErrorListener);
   }
 
   public void unregisterConnectionCallback(@NonNull UUID serviceUuid) {
+    logd(TAG, "Removing error listener for " + serviceUuid + ".");
     connectionErrorListeners.remove(serviceUuid);
   }
 
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/SppProtocol.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/SppProtocol.kt
index 64714e6..f63e826 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/SppProtocol.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/SppProtocol.kt
@@ -19,13 +19,18 @@
 import android.os.ParcelUuid
 import android.os.RemoteException
 import com.google.android.connecteddevice.transport.BluetoothDeviceProvider
+import com.google.android.connecteddevice.transport.ConnectChallenge
 import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
 import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder.OnErrorListener
 import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder.OnMessageReceivedListener
 import com.google.android.connecteddevice.util.SafeLog.logd
 import com.google.android.connecteddevice.util.SafeLog.loge
 import com.google.android.connecteddevice.util.SafeLog.logw
 import java.util.UUID
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
 
 /**
  * Representation of a Serial Port Profile channel, which communicated with [SppService] via
@@ -34,12 +39,13 @@
  * @property sppBinder [ConnectedDeviceSppDelegateBinder] for communication with [SppService].
  * @property maxWriteSize Maximum size in bytes to write in one packet.
  */
-class SppProtocol(
+class SppProtocol
+@JvmOverloads
+constructor(
   private val sppBinder: ConnectedDeviceSppDelegateBinder,
   private val maxWriteSize: Int,
-) : ConnectionProtocol(), BluetoothDeviceProvider {
-  override val isDeviceVerificationRequired = false
-
+  callbackExecutor: Executor = Executors.newCachedThreadPool()
+) : ConnectionProtocol(callbackExecutor), BluetoothDeviceProvider {
   private val pendingConnections = mutableMapOf<UUID, PendingConnection>()
   private val connections = mutableMapOf<UUID, Connection>()
   private val connectedDevices = mutableMapOf<UUID, BluetoothDevice>()
@@ -47,24 +53,25 @@
 
   override fun startAssociationDiscovery(
     name: String,
-    callback: DiscoveryCallback,
-    identifier: UUID
+    identifier: ParcelUuid,
+    callback: IDiscoveryCallback
   ) {
-    associationIdentifier = identifier
-    logd(TAG, "Start association discovery for association with UUID $identifier")
-    startConnection(identifier, callback)
+    val uuidIdentifier = identifier.uuid
+    associationIdentifier = uuidIdentifier
+    logd(TAG, "Start association discovery for association with UUID $uuidIdentifier.")
+    startConnection(uuidIdentifier, callback)
   }
 
   override fun startConnectionDiscovery(
-    id: UUID,
+    id: ParcelUuid,
     challenge: ConnectChallenge,
-    callback: DiscoveryCallback
+    callback: IDiscoveryCallback
   ) {
     logd(TAG, "Starting connection discovery for device $id")
-    startConnection(id, callback)
+    startConnection(id.uuid, callback)
   }
 
-  private fun startConnection(id: UUID, callback: DiscoveryCallback) {
+  private fun startConnection(id: UUID, callback: IDiscoveryCallback) {
     try {
       val pendingConnection = sppBinder.connectAsServer(id, /* isSecure= */ true)
       if (pendingConnection == null) {
@@ -80,7 +87,7 @@
     }
   }
 
-  private fun generateOnConnectionListener(callback: DiscoveryCallback) =
+  private fun generateOnConnectionListener(callback: IDiscoveryCallback) =
     PendingConnection.OnConnectedListener { uuid, remoteDevice, isSecure, deviceName ->
       val protocolId = UUID.randomUUID()
       val connection = Connection(ParcelUuid(uuid), remoteDevice, isSecure, deviceName)
@@ -118,10 +125,11 @@
       val listeners = deviceDisconnectedListeners[protocolId.toString()]
       listeners?.invoke { it.onDeviceDisconnected(protocolId.toString()) }
       connectedDevices.remove(protocolId)
+      connections.remove(protocolId)
       logd(
         TAG,
-        "Inform device connection error with connection $protocolId to " +
-          "${listeners?.size()} listeners."
+        "Inform device connection error with connection $protocolId to ${listeners?.size()} " +
+          "listeners."
       )
       removeListeners(protocolId.toString())
     }
@@ -138,9 +146,9 @@
     associationIdentifier = null
   }
 
-  override fun stopConnectionDiscovery(id: UUID) {
-    logd(TAG, "Stop connection discovery with UUID $id.")
-    stopDiscovery(id)
+  override fun stopConnectionDiscovery(id: ParcelUuid) {
+    logd(TAG, "Stop connection discovery with UUID ${id.uuid}.")
+    stopDiscovery(id.uuid)
   }
 
   private fun stopDiscovery(id: UUID) {
@@ -152,7 +160,7 @@
     }
   }
 
-  override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {
+  override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {
     val connection = connections[UUID.fromString(protocolId)]
     if (connection == null) {
       callback?.onDataFailedToSend()
@@ -219,6 +227,8 @@
 
   override fun getMaxWriteSize(protocolId: String) = maxWriteSize
 
+  override fun isDeviceVerificationRequired() = false
+
   companion object {
     private const val TAG = "SppProtocol"
   }
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceAgentService.java b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceAgentService.java
index eee9729..01aca37 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceAgentService.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceAgentService.java
@@ -34,7 +34,9 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.service.trust.TrustAgentService;
+import androidx.annotation.VisibleForTesting;
 import com.google.android.connecteddevice.trust.api.ITrustedDeviceAgentDelegate;
 import com.google.android.connecteddevice.trust.api.ITrustedDeviceManager;
 import java.time.Duration;
@@ -60,15 +62,21 @@
 
   private Handler retryHandler;
 
-  private ITrustedDeviceManager trustedDeviceManager;
+  @VisibleForTesting ITrustedDeviceManager trustedDeviceManager;
 
   private int retries;
 
+  private UserManager userManager;
+
+  private KeyguardManager keyguardManager;
+
   @SuppressLint("UnprotectedReceiver") // Broadcast is protected.
   @Override
   public void onCreate() {
     super.onCreate();
     logd(TAG, "Starting trust agent service.");
+    userManager = (UserManager) getSystemService(Context.USER_SERVICE);
+    keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
     TrustedDeviceEventLog.onTrustAgentStarted();
     registerReceiver(userUnlockedReceiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
     retryThread = new HandlerThread(RETRY_HANDLER_THREAD_NAME);
@@ -130,20 +138,19 @@
     }
   }
 
-  private void deviceUnlocked() {
+  @VisibleForTesting
+  void maybeDismissLockscreen() {
     if (!isManagingTrust.compareAndSet(true, false)) {
-      logd(
-          TAG,
-          "User was unlocked via an alternative mechanism than an escrow token. No further "
-              + "action required.");
+      logd(TAG, "User was unlocked before receiving an escrow token.");
       return;
     }
-    logd(TAG, "User successfully unlocked with an escrow token. Dismissing the lock screen.");
+    logd(TAG, "Dismissing the lockscreen.");
     grantTrust(
         "Granting trust from escrow token for user.",
         TRUST_DURATION_MS,
         FLAG_GRANT_TRUST_DISMISS_KEYGUARD);
     TrustedDeviceEventLog.onUserUnlocked();
+    setManagingTrust(false);
     if (trustedDeviceManager == null) {
       loge(TAG, "Manager was null when device was unlocked. Ignoring.");
       return;
@@ -155,22 +162,33 @@
     }
   }
 
-  private void bindToService() {
+  @VisibleForTesting
+  boolean isUserUnlocked(int userId) {
+    return userManager.isUserUnlocked(UserHandle.of(userId));
+  }
+
+  @VisibleForTesting
+  void bindToService() {
     Intent intent = new Intent(this, TrustedDeviceManagerService.class);
     bindService(intent, serviceConnection, /* flags= */ 0);
   }
 
+  @VisibleForTesting
+  final void setupManager() {
+    try {
+      trustedDeviceManager.setTrustedDeviceAgentDelegate(trustedDeviceAgentDelegate);
+      logd(TAG, "Successfully connected to TrustedDeviceManager.");
+    } catch (RemoteException e) {
+      loge(TAG, "Error while establishing connection to TrustedDeviceManager.", e);
+    }
+  }
+
   private final ServiceConnection serviceConnection =
       new ServiceConnection() {
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
           trustedDeviceManager = ITrustedDeviceManager.Stub.asInterface(service);
-          try {
-            trustedDeviceManager.setTrustedDeviceAgentDelegate(trustedDeviceAgentDelegate);
-            logd(TAG, "Successfully connected to TrustedDeviceManager.");
-          } catch (RemoteException e) {
-            loge(TAG, "Error while establishing connection to TrustedDeviceManager.", e);
-          }
+          setupManager();
         }
 
         @Override
@@ -206,8 +224,19 @@
 
         @Override
         public void unlockUserWithToken(byte[] token, long handle, int userId) {
-          setManagingTrust(true);
+          if (!keyguardManager.isDeviceLocked()) {
+            logd(TAG, "Received an escrow token while no lockscreen was visible. Ignoring.");
+            return;
+          }
+          logd(TAG, "Received an escrow token for user " + userId + ".");
           isManagingTrust.set(true);
+          setManagingTrust(true);
+          if (isUserUnlocked(userId)) {
+            logd(TAG, "User was already unlocked when token was received.");
+            maybeDismissLockscreen();
+            return;
+          }
+          logd(TAG, "Unlocking user with token.");
           TrustedDeviceAgentService.this.unlockUserWithToken(handle, token, UserHandle.of(userId));
         }
 
@@ -222,7 +251,7 @@
       new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
-          deviceUnlocked();
+          maybeDismissLockscreen();
         }
       };
 }
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceManager.java
index 5e5caf1..12b3ffa 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceManager.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceManager.java
@@ -51,13 +51,17 @@
 import com.google.android.connecteddevice.trust.storage.TrustedDeviceDatabase;
 import com.google.android.connecteddevice.trust.storage.TrustedDeviceDatabaseProvider;
 import com.google.android.connecteddevice.trust.storage.TrustedDeviceEntity;
+import com.google.android.connecteddevice.trust.storage.TrustedDeviceTokenEntity;
 import com.google.android.connecteddevice.util.ByteUtils;
 import com.google.android.connecteddevice.util.SafeConsumer;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.ExtensionRegistryLite;
 import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.UUID;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -72,6 +76,8 @@
   /** Length of token generated on a trusted device. */
   private static final int ESCROW_TOKEN_LENGTH = 8;
 
+  private static final String SHA256 = "SHA-256";
+
   private final RemoteCallbackList<ITrustedDeviceCallback> remoteTrustedDeviceCallbacks =
       new RemoteCallbackList<>();
 
@@ -189,7 +195,6 @@
   @Override
   public void onEscrowTokenAdded(int userId, long handle) {
     logd(TAG, "Escrow token has been successfully added.");
-    pendingToken = null;
 
     if (remoteEnrollmentCallbacks.getRegisteredCallbackCount() == 0) {
       isWaitingForCredentials.set(true);
@@ -214,6 +219,11 @@
       loge(TAG, "Unable to complete device enrollment. Pending device was null.");
       return;
     }
+    byte[] hashedToken = hashToken(pendingToken, UUID.fromString(pendingDevice.getDeviceId()));
+    if (hashedToken == null) {
+      loge(TAG, "Unable to hash pending token. Aborting enrollment.");
+      return;
+    }
 
     logd(
         TAG,
@@ -225,13 +235,17 @@
     String deviceId = pendingDevice.getDeviceId();
 
     TrustedDeviceEntity entity = new TrustedDeviceEntity(deviceId, userId, handle);
+    TrustedDeviceTokenEntity tokenEntity =
+        new TrustedDeviceTokenEntity(deviceId, ByteUtils.byteArrayToHexString(hashedToken));
     databaseExecutor.execute(
         () -> {
           database.removeFeatureState(deviceId);
           database.addOrReplaceTrustedDevice(entity);
+          database.addOrReplaceTrustedDeviceHashedToken(tokenEntity);
         });
 
     pendingDevice = null;
+    pendingToken = null;
 
     TrustedDevice trustedDevice = new TrustedDevice(deviceId, userId, handle);
     notifyRemoteCallbackList(
@@ -274,7 +288,8 @@
     logd(TAG, "Received request to remove trusted device");
     databaseExecutor.execute(
         () -> {
-          TrustedDeviceEntity entity = database.getTrustedDevice(trustedDevice.getDeviceId());
+          TrustedDeviceEntity entity =
+              database.getTrustedDeviceIfValid(trustedDevice.getDeviceId());
           if (entity == null) {
             return;
           }
@@ -356,8 +371,11 @@
     // Remove invalid trusted devices.
     databaseExecutor.execute(
         () -> {
+          byte[] stateMessage = createDisabledStateSyncMessage();
           List<TrustedDeviceEntity> entities = database.getInvalidTrustedDevicesForUser(userId);
           for (TrustedDeviceEntity entity : entities) {
+            FeatureStateEntity stateEntity = new FeatureStateEntity(entity.id, stateMessage);
+            databaseExecutor.execute(() -> database.addOrReplaceFeatureState(stateEntity));
             removeTrustedDeviceInternal(entity);
           }
         });
@@ -419,6 +437,8 @@
     logd(TAG, "Marking device " + entity.id + " as invalid.");
     entity.isValid = false;
     database.addOrReplaceTrustedDevice(entity);
+    logd(TAG, "Removing hashed token for device " + entity.id + ".");
+    database.removeTrustedDeviceHashedToken(entity.id);
     sendStateDisabledMessage(entity.id);
     notifyRemoteCallbackList(
         remoteTrustedDeviceCallbacks,
@@ -467,8 +487,38 @@
     return trustedDevices;
   }
 
-  private static boolean areCredentialsValid(@Nullable PhoneCredentials credentials) {
-    return credentials != null;
+  @WorkerThread
+  private boolean areCredentialsValid(@Nullable PhoneCredentials credentials, String deviceId) {
+    if (credentials == null) {
+      return false;
+    }
+    TrustedDeviceTokenEntity entity = database.getTrustedDeviceHashedToken(deviceId);
+    if (entity == null) {
+      loge(TAG, "Unable to find hashed token for device " + deviceId + ".");
+      return false;
+    }
+    byte[] hashedToken =
+        hashToken(credentials.getEscrowToken().toByteArray(), UUID.fromString(deviceId));
+    if (hashedToken == null) {
+      return false;
+    }
+    return MessageDigest.isEqual(
+        hashedToken, ByteUtils.hexStringToByteArray(entity.getHashedToken()));
+  }
+
+  @Nullable
+  private static byte[] hashToken(byte[] token, UUID deviceId) {
+    MessageDigest digest;
+    try {
+      digest = MessageDigest.getInstance(SHA256);
+    } catch (NoSuchAlgorithmException e) {
+      loge(TAG, "Unable to find " + SHA256 + " algorithm. Token hash could not be generated.");
+      return null;
+    }
+
+    digest.update(token);
+    digest.update(ByteUtils.uuidToBytes(deviceId));
+    return digest.digest();
   }
 
   private void sendStateDisabledMessage(@NonNull String deviceId) {
@@ -513,7 +563,7 @@
     }
     byte[] message = payload.toByteArray();
 
-    PhoneCredentials credentials = null;
+    PhoneCredentials credentials;
     try {
       credentials = PhoneCredentials.parseFrom(message, ExtensionRegistryLite.getEmptyRegistry());
     } catch (InvalidProtocolBufferException e) {
@@ -521,12 +571,12 @@
       return;
     }
 
-    if (!areCredentialsValid(credentials)) {
+    if (!areCredentialsValid(credentials, device.getDeviceId())) {
       loge(TAG, "Received invalid credentials from device. Not unlocking head unit.");
       return;
     }
 
-    TrustedDeviceEntity entity = database.getTrustedDevice(device.getDeviceId());
+    TrustedDeviceEntity entity = database.getTrustedDeviceIfValid(device.getDeviceId());
 
     if (entity == null) {
       logw(TAG, "Received unlock request from an untrusted device.");
@@ -559,7 +609,7 @@
   private void processStatusSyncMessage(
       @NonNull ConnectedDevice device,
       @NonNull ByteString payload) {
-    TrustedDeviceState state = null;
+    TrustedDeviceState state;
     try {
       state = TrustedDeviceState.parseFrom(payload, ExtensionRegistryLite.getEmptyRegistry());
     } catch (InvalidProtocolBufferException e) {
@@ -578,7 +628,7 @@
       return;
     }
 
-    TrustedDeviceEntity entity = database.getTrustedDevice(device.getDeviceId());
+    TrustedDeviceEntity entity = database.getTrustedDeviceIfValid(device.getDeviceId());
 
     if (entity == null) {
       logw(TAG, "Received state sync message from an untrusted device.");
@@ -598,7 +648,7 @@
       logw(TAG, "Received error message with null payload. Ignoring.");
       return;
     }
-    TrustedDeviceError trustedDeviceError = null;
+    TrustedDeviceError trustedDeviceError;
     try {
       trustedDeviceError =
           TrustedDeviceError.parseFrom(payload, ExtensionRegistryLite.getEmptyRegistry());
@@ -705,7 +755,7 @@
       new TrustedDeviceFeature.Callback() {
         @Override
         public void onMessageReceived(ConnectedDevice device, byte[] message) {
-          TrustedDeviceMessage trustedDeviceMessage = null;
+          TrustedDeviceMessage trustedDeviceMessage;
           try {
             trustedDeviceMessage =
                 TrustedDeviceMessage.parseFrom(message, ExtensionRegistryLite.getEmptyRegistry());
@@ -770,7 +820,7 @@
         @Override
         public void onAssociatedDeviceRemoved(AssociatedDevice device) {
           List<TrustedDevice> devices = getTrustedDevicesForActiveUser();
-          if (devices == null || devices.isEmpty()) {
+          if (devices.isEmpty()) {
             return;
           }
           TrustedDevice deviceToRemove = null;
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDao.java b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDao.java
index 77314dd..ec8cdf6 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDao.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDao.java
@@ -12,9 +12,13 @@
 public interface TrustedDeviceDao {
 
   /** Get a {@link TrustedDeviceEntity} based on device id. */
-  @Query("SELECT * FROM trusted_devices WHERE id LIKE :deviceId AND isValid = 1 LIMIT 1")
+  @Query("SELECT * FROM trusted_devices WHERE id LIKE :deviceId LIMIT 1")
   TrustedDeviceEntity getTrustedDevice(String deviceId);
 
+  /** Get a {@link TrustedDeviceEntity} based on device id only if it is valid. */
+  @Query("SELECT * FROM trusted_devices WHERE id LIKE :deviceId AND isValid = 1 LIMIT 1")
+  TrustedDeviceEntity getTrustedDeviceIfValid(String deviceId);
+
   /** Get a {@link FeatureStateEntity} based on device id. */
   @Query("SELECT * FROM feature_state WHERE id = :deviceId")
   FeatureStateEntity getFeatureState(String deviceId);
@@ -47,4 +51,19 @@
   /** Remove any stored feature statue for a car with the given {@code deviceId}. */
   @Query("DELETE FROM feature_state WHERE id = :deviceId")
   void removeFeatureState(String deviceId);
+
+  /** Get a {@link TrustedDeviceTokenEntity} based on device id. */
+  @Query("SELECT * FROM trusted_device_tokens WHERE id = :deviceId LIMIT 1")
+  TrustedDeviceTokenEntity getTrustedDeviceHashedToken(String deviceId);
+
+  /**
+   * Add a {@link TrustedDeviceTokenEntity}. Replaces any previously stored hashed token with a
+   * matching device id.
+   */
+  @Insert(onConflict = OnConflictStrategy.REPLACE)
+  void addOrReplaceTrustedDeviceHashedToken(TrustedDeviceTokenEntity hashedToken);
+
+  /** Remove a {@link TrustedDeviceTokenEntity} belonging with the given {@code deviceId}. */
+  @Query("DELETE FROM trusted_device_tokens WHERE id = :deviceId")
+  void removeTrustedDeviceHashedToken(String deviceId);
 }
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDatabase.java b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDatabase.java
index f9bba20..fcb829e 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDatabase.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDatabase.java
@@ -5,8 +5,12 @@
 
 /** Database for trusted device feature. */
 @Database(
-    entities = {TrustedDeviceEntity.class, FeatureStateEntity.class},
-    version = 3,
+    entities = {
+      TrustedDeviceEntity.class,
+      FeatureStateEntity.class,
+      TrustedDeviceTokenEntity.class
+    },
+    version = 4,
     exportSchema = true)
 public abstract class TrustedDeviceDatabase extends RoomDatabase {
   /** Return the DAO for the trusted device table. */
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDatabaseProvider.java b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDatabaseProvider.java
index 7dda690..475ab3e 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDatabaseProvider.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDatabaseProvider.java
@@ -4,12 +4,13 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 import androidx.room.Room;
+import androidx.room.RoomDatabase;
 import androidx.room.migration.Migration;
 import androidx.sqlite.db.SupportSQLiteDatabase;
 
 /** Provider of the common database within this library. */
 public class TrustedDeviceDatabaseProvider {
-  private static final String DATABASE_NAME = "trusted-device-database";
+  @VisibleForTesting static final String DATABASE_NAME = "trusted-device-database";
 
   /**
    * Performs the database migration from version 1 to 2.
@@ -41,25 +42,48 @@
         }
       };
 
+  /**
+   * Performs the database migration from 3 to 4.
+   *
+   * <p>This migration creates a new table for hashed tokens and invalidates any existing trusted
+   * device entities for removal on next startup.
+   */
+  @VisibleForTesting
+  static final Migration MIGRATION_3_4 =
+      new Migration(3, 4) {
+        @Override
+        public void migrate(SupportSQLiteDatabase database) {
+          database.execSQL(
+              "CREATE TABLE IF NOT EXISTS trusted_device_tokens "
+                  + "(id TEXT NOT NULL, hashed_token TEXT NOT NULL, PRIMARY KEY(id))");
+          database.execSQL("UPDATE trusted_devices SET isValid = 0");
+        }
+      };
+
   private static TrustedDeviceDatabaseProvider instance;
 
-  private final TrustedDeviceDatabase database;
+  @VisibleForTesting final TrustedDeviceDatabase database;
 
-  private TrustedDeviceDatabaseProvider(@NonNull Context context) {
-    database =
+  @VisibleForTesting
+  TrustedDeviceDatabaseProvider(@NonNull Context context, boolean allowMainThreadQueries) {
+    RoomDatabase.Builder<TrustedDeviceDatabase> builder =
         Room.databaseBuilder(context, TrustedDeviceDatabase.class, DATABASE_NAME)
-            .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
-            .enableMultiInstanceInvalidation()
-            .fallbackToDestructiveMigration()
-            .build();
+            .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
+            .enableMultiInstanceInvalidation();
+    if (allowMainThreadQueries) {
+      builder.allowMainThreadQueries();
+    }
+    database = builder.build();
   }
 
   /** Returns an instance of the {@link TrustedDeviceDatabase} to run queries off of. */
   @NonNull
   public static synchronized TrustedDeviceDatabase get(@NonNull Context context) {
     if (instance == null) {
-      instance = new TrustedDeviceDatabaseProvider(
-        context.getApplicationContext().createDeviceProtectedStorageContext());
+      instance =
+          new TrustedDeviceDatabaseProvider(
+              context.getApplicationContext().createDeviceProtectedStorageContext(),
+              /* allowMainThreadQueries= */ false);
     }
     return instance.database;
   }
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceTokenEntity.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceTokenEntity.kt
new file mode 100644
index 0000000..e700e05
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceTokenEntity.kt
@@ -0,0 +1,14 @@
+package com.google.android.connecteddevice.trust.storage
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+/** Table entity representing the hashed escrow token for a trusted device. */
+@Entity(tableName = "trusted_device_tokens")
+data class TrustedDeviceTokenEntity(
+  /** Device id of the trusted device. */
+  @PrimaryKey val id: String,
+  /** Base64 encoding of hashed token. */
+  @ColumnInfo(name = "hashed_token") val hashedToken: String
+)
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceDetails.java b/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceDetails.java
new file mode 100644
index 0000000..bf41dfa
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceDetails.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2020 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 com.google.android.connecteddevice.ui;
+
+import android.app.ActivityManager;
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.android.connecteddevice.model.AssociatedDevice;
+import java.util.Objects;
+
+/**
+ * Class that contains the details of an associated device.
+ *
+ * <p>Two {@link AssociatedDeviceDetails} are considered equal if their backing
+ * {@link AssociatedDevice} are equal. The connection state is not taken into account.
+ */
+public class AssociatedDeviceDetails implements Parcelable {
+
+  /** States of a device connection. */
+  public enum ConnectionState {
+    NOT_DETECTED,
+    DETECTED,
+    CONNECTED
+  }
+
+  private final AssociatedDevice device;
+
+  private final ConnectionState state;
+
+  public AssociatedDeviceDetails(@NonNull AssociatedDevice device, ConnectionState state) {
+    this.device = device;
+    this.state = state;
+  }
+
+  private AssociatedDeviceDetails(Parcel in) {
+    this(
+        in.readParcelable(AssociatedDevice.class.getClassLoader()),
+        ConnectionState.values()[in.readInt()]);
+  }
+
+  /** Get the device id. */
+  @NonNull
+  public String getDeviceId() {
+    return device.getDeviceId();
+  }
+
+  /** Get the name of the associated device. */
+  @Nullable
+  public String getDeviceName() {
+    return device.getDeviceName();
+  }
+
+  /** Get the device address. */
+  @NonNull
+  public String getDeviceAddress() {
+    return device.getDeviceAddress();
+  }
+
+  /** {@code true} if the connection is enabled for the device. */
+  public boolean isConnectionEnabled() {
+    return device.isConnectionEnabled();
+  }
+
+  /** Returns the current state of the device's connection. */
+  @NonNull
+  public ConnectionState getConnectionState() {
+    return state;
+  }
+
+  /** Returns the claiming user id, or {@value AssociatedDevice#UNCLAIMED_USER_ID} if unclaimed. */
+  public int getUserId() {
+    return device.getUserId();
+  }
+
+  /** Returns whether this device belongs to the current driver. */
+  public boolean belongsToDriver() {
+    return device.getUserId() == ActivityManager.getCurrentUser();
+  }
+
+  /** Get {@link AssociatedDevice}. */
+  @NonNull
+  public AssociatedDevice getAssociatedDevice() {
+    return device;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == this) {
+      return true;
+    }
+    if (!(obj instanceof AssociatedDeviceDetails)) {
+      return false;
+    }
+
+    // The connection state does not factor into equality.
+    AssociatedDeviceDetails details = (AssociatedDeviceDetails) obj;
+    return Objects.equals(device, details.device);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(device);
+  }
+
+  @Override
+  public int describeContents() {
+    return 0;
+  }
+
+  @Override
+  public void writeToParcel(Parcel dest, int flags) {
+    dest.writeParcelable(device, /* flags= */ 0);
+    dest.writeInt(state.ordinal());
+  }
+
+  // Explicitly specifying the type within <> to support building with Java 8.
+  public static final Parcelable.Creator<AssociatedDeviceDetails> CREATOR =
+      new Parcelable.Creator<AssociatedDeviceDetails>() {
+        @Override
+        public AssociatedDeviceDetails createFromParcel(Parcel source) {
+          return new AssociatedDeviceDetails(source);
+        }
+
+        @Override
+        public AssociatedDeviceDetails[] newArray(int size) {
+          return new AssociatedDeviceDetails[size];
+        }
+      };
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModel.java b/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModel.java
index 171ba8c..4b09061 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModel.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModel.java
@@ -30,7 +30,6 @@
 import android.content.IntentFilter;
 import android.os.ParcelUuid;
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.lifecycle.AndroidViewModel;
 import androidx.lifecycle.LiveData;
@@ -41,11 +40,13 @@
 import com.google.android.connecteddevice.api.IAssociationCallback;
 import com.google.android.connecteddevice.api.IOnAssociatedDevicesRetrievedListener;
 import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.AssociatedDeviceDetails;
 import com.google.android.connecteddevice.model.ConnectedDevice;
 import com.google.android.connecteddevice.model.StartAssociationResponse;
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState;
 import java.time.Duration;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.UUID;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 /**
@@ -69,10 +70,9 @@
   }
 
   private final List<AssociatedDevice> associatedDevices = new CopyOnWriteArrayList<>();
-  private final List<ConnectedDevice> connectedDevices = new CopyOnWriteArrayList<>();
 
-  private final MutableLiveData<AssociatedDeviceDetails> currentDeviceDetails =
-      new MutableLiveData<>(null);
+  private final MutableLiveData<List<AssociatedDeviceDetails>> associatedDevicesDetails =
+      new MutableLiveData<>(new ArrayList<>());
   private final MutableLiveData<String> advertisedCarName = new MutableLiveData<>(null);
   private final MutableLiveData<StartAssociationResponse> associationResponse =
       new MutableLiveData<>(null);
@@ -84,6 +84,7 @@
   private final MutableLiveData<AssociatedDevice> removedDevice = new MutableLiveData<>(null);
   private final MutableLiveData<Boolean> isServiceConnected = new MutableLiveData<>(false);
   private final boolean isSppEnabled;
+  private final boolean isPassengerEnabled;
   private final String bleDeviceNamePrefix;
   private final BluetoothAdapter bluetoothAdapter;
 
@@ -92,12 +93,18 @@
   private ParcelUuid associationIdentifier;
 
   public AssociatedDeviceViewModel(
-      @NonNull Application application, boolean isSppEnabled, String bleDeviceNamePrefix) {
+      @NonNull Application application,
+      boolean isSppEnabled,
+      String bleDeviceNamePrefix,
+      boolean isPassengerEnabled
+  ) {
     this(
         application,
         isSppEnabled,
         bleDeviceNamePrefix,
-        new CompanionConnector(application, /* isForegroundProcess= */ true));
+        isPassengerEnabled,
+        new CompanionConnector(
+            application, /* isForegroundProcess= */ true, /* userType= */ Connector.USER_TYPE_ALL));
   }
 
   @VisibleForTesting
@@ -106,19 +113,28 @@
       @NonNull Application application,
       boolean isSppEnabled,
       String bleDeviceNamePrefix,
+      boolean isPassengerEnabled,
       Connector connector) {
     super(application);
     this.isSppEnabled = isSppEnabled;
     this.bleDeviceNamePrefix = bleDeviceNamePrefix;
     this.connector = connector;
+    this.isPassengerEnabled = isPassengerEnabled;
+
     connector.setCallback(connectorCallback);
+
     IntentFilter filter = new IntentFilter();
     filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
     getApplication().registerReceiver(receiver, filter);
     bluetoothAdapter =
         application.getApplicationContext().getSystemService(BluetoothManager.class).getAdapter();
     bluetoothState.postValue(BluetoothAdapter.STATE_ON);
-    connector.connect();
+
+    // Registers for device callbacks
+    this.connector.setFeatureId(new ParcelUuid(UUID.randomUUID()));
+
+    this.connector.setCallback(connectorCallback);
+    this.connector.connect();
   }
 
   @Override
@@ -153,21 +169,13 @@
     startAssociationInternal();
   }
 
-  /** Removes the current associated device. */
-  public void removeCurrentDevice() {
-    AssociatedDevice device = getAssociatedDevice();
-    if (device == null) {
-      return;
-    }
+  /** Removes the association of the given device. */
+  public void removeDevice(@NonNull AssociatedDevice device) {
     connector.removeAssociatedDevice(device.getDeviceId());
   }
 
-  /** Toggles connection on the current associated device. */
-  public void toggleConnectionStatusForCurrentDevice() {
-    AssociatedDevice device = getAssociatedDevice();
-    if (device == null) {
-      return;
-    }
+  /** Toggles connection of the given associated device. */
+  public void toggleConnectionStatusForDevice(@NonNull AssociatedDevice device) {
     if (device.isConnectionEnabled()) {
       connector.disableAssociatedDeviceConnection(device.getDeviceId());
     } else {
@@ -175,17 +183,29 @@
     }
   }
 
-  /** Gets the associated device details. */
-  public LiveData<AssociatedDeviceDetails> getCurrentDeviceDetails() {
-    return currentDeviceDetails;
+  /** Mark the given device as belonging to the active driver. */
+  public void claimDevice(@NonNull AssociatedDevice device) {
+    connector.claimAssociatedDevice(device.getDeviceId());
   }
 
-  /** Starts feature activity for the current associated device. */
-  public void startFeatureActivityForCurrentDevice(@NonNull String action) {
-    AssociatedDevice device = getAssociatedDevice();
-    if (device == null || action == null) {
-      return;
-    }
+  /** Mark the given device as unclaimed by any user. */
+  public void removeClaimOnDevice(@NonNull AssociatedDevice device) {
+    connector.removeAssociatedDeviceClaim(device.getDeviceId());
+  }
+
+  /**
+   * Gets a list of details for all associated device.
+   *
+   * <p>The list will always be non-{@code null}, and will be empty if there are no associated
+   * devices.
+   */
+  public LiveData<List<AssociatedDeviceDetails>> getAssociatedDevicesDetails() {
+    return associatedDevicesDetails;
+  }
+
+  /** Starts feature activity for the given associated device. */
+  public void startFeatureActivityForDevice(
+      @NonNull String action, @NonNull AssociatedDevice device) {
     Intent intent = new Intent(action);
     intent.putExtra(ASSOCIATED_DEVICE_DATA_NAME_EXTRA, device);
     intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -213,7 +233,7 @@
     return pairingCode;
   }
 
-  /** Value is {@code true} if the current associated device has been removed. */
+  /** Returns the value of a device whose association has been removed. */
   public LiveData<AssociatedDevice> getRemovedDevice() {
     return removedDevice;
   }
@@ -270,29 +290,26 @@
   }
 
   private void updateDeviceDetails() {
-    AssociatedDevice device = getAssociatedDevice();
-    if (device == null) {
-      return;
+    List<AssociatedDeviceDetails> associatedDevicesDetails = new ArrayList<>();
+    for (AssociatedDevice device : associatedDevices) {
+      associatedDevicesDetails.add(new AssociatedDeviceDetails(device, getConnectionState(device)));
     }
-    currentDeviceDetails.postValue(
-        new AssociatedDeviceDetails(getAssociatedDevice(), isConnected()));
+    this.associatedDevicesDetails.postValue(associatedDevicesDetails);
   }
 
-  @Nullable
-  private AssociatedDevice getAssociatedDevice() {
-    if (associatedDevices.isEmpty()) {
-      return null;
+  private ConnectionState getConnectionState(@NonNull AssociatedDevice device) {
+    logd(TAG, "Getting connection state for device " + device.getDeviceId() + ".");
+    ConnectedDevice connectedDevice = connector.getConnectedDeviceById(device.getDeviceId());
+    if (connectedDevice == null) {
+      logd(TAG, "Device is not detected.");
+      return ConnectionState.NOT_DETECTED;
     }
-    return associatedDevices.get(0);
-  }
-
-  private boolean isConnected() {
-    if (associatedDevices.isEmpty() || connectedDevices.isEmpty()) {
-      return false;
+    if (connectedDevice.hasSecureChannel()) {
+      logd(TAG, "Device is connected.");
+      return ConnectionState.CONNECTED;
     }
-    String associatedDeviceId = associatedDevices.get(0).getDeviceId();
-    String connectedDeviceId = connectedDevices.get(0).getDeviceId();
-    return associatedDeviceId.equals(connectedDeviceId);
+    logd(TAG, "Device is detected.");
+    return ConnectionState.DETECTED;
   }
 
   private void setAssociatedDevices(@NonNull List<AssociatedDevice> associatedDevices) {
@@ -301,12 +318,6 @@
     updateDeviceDetails();
   }
 
-  private void setConnectedDevices(@NonNull List<ConnectedDevice> connectedDevices) {
-    this.connectedDevices.clear();
-    this.connectedDevices.addAll(connectedDevices);
-    updateDeviceDetails();
-  }
-
   private void addOrUpdateAssociatedDevice(@NonNull AssociatedDevice device) {
     associatedDevices.remove(device);
     associatedDevices.add(device);
@@ -316,7 +327,7 @@
   private void removeAssociatedDevice(AssociatedDevice device) {
     if (associatedDevices.remove(device)) {
       removedDevice.postValue(device);
-      currentDeviceDetails.postValue(null);
+      updateDeviceDetails();
     }
   }
 
@@ -326,8 +337,14 @@
         public void onConnected() {
           logd(TAG, "Connected to platform.");
           isServiceConnected.postValue(true);
-          setConnectedDevices(connector.getConnectedDevices());
-          connector.retrieveAssociatedDevicesForDriver(associatedDevicesRetrievedListener);
+
+          if (isPassengerEnabled) {
+            connector.retrieveAssociatedDevices(associatedDevicesRetrievedListener);
+          } else {
+            // If passenger is disabled, then there should only be one device and that will belong
+            // to the driver.
+            connector.retrieveAssociatedDevicesForDriver(associatedDevicesRetrievedListener);
+          }
         }
 
         @Override
@@ -355,13 +372,19 @@
 
         @Override
         public void onDeviceConnected(ConnectedDevice connectedDevice) {
-          connectedDevices.add(connectedDevice);
+          logd(TAG, "Device " + connectedDevice.getDeviceId() + " has connected.");
           updateDeviceDetails();
         }
 
         @Override
         public void onDeviceDisconnected(ConnectedDevice connectedDevice) {
-          connectedDevices.remove(connectedDevice);
+          logd(TAG, "Device " + connectedDevice.getDeviceId() + " has disconnected.");
+          updateDeviceDetails();
+        }
+
+        @Override
+        public void onSecureChannelEstablished(@NonNull ConnectedDevice device) {
+          logd(TAG, "Device " + device.getDeviceId() + " has established a secure channel.");
           updateDeviceDetails();
         }
       };
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModelFactory.java b/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModelFactory.java
index 2d9c1aa..fae26e7 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModelFactory.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModelFactory.java
@@ -26,18 +26,24 @@
   private final Application application;
   private final boolean isSppEnabled;
   private final String bleDeviceNamePrefix;
+  private final boolean isPassengerEnabled;
 
   public AssociatedDeviceViewModelFactory(
-      Application application, boolean isSppEnabled, String bleDeviceNamePrefix) {
+      Application application,
+      boolean isSppEnabled,
+      String bleDeviceNamePrefix,
+      boolean isPassengerEnabled) {
     this.application = application;
     this.isSppEnabled = isSppEnabled;
     this.bleDeviceNamePrefix = bleDeviceNamePrefix;
+    this.isPassengerEnabled = isPassengerEnabled;
   }
 
   @NonNull
   @Override
   public <T extends ViewModel> T create(@NonNull Class<T> aClass) {
     return aClass.cast(
-        new AssociatedDeviceViewModel(application, isSppEnabled, bleDeviceNamePrefix));
+        new AssociatedDeviceViewModel(
+            application, isSppEnabled, bleDeviceNamePrefix, isPassengerEnabled));
   }
 }
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/util/ByteUtils.java b/libs/connecteddevice/src/com/google/android/connecteddevice/util/ByteUtils.java
index 8948d9b..d51b036 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/util/ByteUtils.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/util/ByteUtils.java
@@ -22,6 +22,7 @@
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.security.InvalidParameterException;
 import java.security.SecureRandom;
 import java.util.UUID;
 
@@ -82,6 +83,10 @@
    */
   public static byte[] hexStringToByteArray(String hex) {
     int len = hex.length();
+    if (len % 2 != 0) {
+      throw new InvalidParameterException(
+          "Hex string must have a length that is evenly divisible by two.");
+    }
     byte[] data = new byte[len / 2];
     for (int i = 0; i < len; i += 2) {
       data[i / 2] =
diff --git a/libs/connecteddevice/tests/unit/schemas/com.google.android.connecteddevice.trust.storage.TrustedDeviceDatabase/4.json b/libs/connecteddevice/tests/unit/schemas/com.google.android.connecteddevice.trust.storage.TrustedDeviceDatabase/4.json
new file mode 100644
index 0000000..bbe0bd5
--- /dev/null
+++ b/libs/connecteddevice/tests/unit/schemas/com.google.android.connecteddevice.trust.storage.TrustedDeviceDatabase/4.json
@@ -0,0 +1,104 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 4,
+    "identityHash": "89264c80333c0d361cd296170f2b814e",
+    "entities": [
+      {
+        "tableName": "trusted_devices",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `userId` INTEGER NOT NULL, `handle` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "userId",
+            "columnName": "userId",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "handle",
+            "columnName": "handle",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "isValid",
+            "columnName": "isValid",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "feature_state",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `state` BLOB NOT NULL, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "state",
+            "columnName": "state",
+            "affinity": "BLOB",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "trusted_device_tokens",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `hashed_token` TEXT NOT NULL, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "hashedToken",
+            "columnName": "hashed_token",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '89264c80333c0d361cd296170f2b814e')"
+    ]
+  }
+}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ConnectedDeviceManagerTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ConnectedDeviceManagerTest.java
deleted file mode 100644
index 873e42c..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ConnectedDeviceManagerTest.java
+++ /dev/null
@@ -1,659 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.connecteddevice.ConnectedDeviceManager.ConnectionCallback;
-import com.google.android.connecteddevice.ConnectedDeviceManager.DeviceAssociationCallback;
-import com.google.android.connecteddevice.ConnectedDeviceManager.DeviceCallback;
-import com.google.android.connecteddevice.ConnectedDeviceManager.MessageDeliveryDelegate;
-import com.google.android.connecteddevice.connection.CarBluetoothManager;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.ConnectedDevice;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.model.DeviceMessage.OperationType;
-import com.google.android.connecteddevice.model.Errors;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage.AssociatedDeviceCallback;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.common.collect.ImmutableList;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-public class ConnectedDeviceManagerTest {
-
-  private static final String TEST_DEVICE_ADDRESS = "00:11:22:33:44:55";
-
-  private static final String TEST_DEVICE_NAME = "TEST_DEVICE_NAME";
-
-  private final Executor directExecutor = directExecutor();
-
-  private final UUID recipientId = UUID.randomUUID();
-
-  private final List<String> userDeviceIds = new ArrayList<>();
-
-  private final List<AssociatedDevice> userDevices = new ArrayList<>();
-
-  @Mock private ConnectedDeviceStorage mockStorage;
-
-  @Mock private CarBluetoothManager mockCarBluetoothManager;
-  @Mock private ConnectionCallback mockConnectionCallback;
-  @Mock private DeviceCallback mockDeviceCallback;
-  @Mock private DeviceAssociationCallback mockDeviceAssociationCallback;
-
-  private ConnectedDeviceManager connectedDeviceManager;
-
-  private AssociatedDeviceCallback associatedDeviceCallback;
-
-  @Before
-  public void setUp() {
-    MockitoAnnotations.initMocks(this);
-    ArgumentCaptor<AssociatedDeviceCallback> callbackCaptor =
-        ArgumentCaptor.forClass(AssociatedDeviceCallback.class);
-    connectedDeviceManager =
-        new ConnectedDeviceManager(
-            mockCarBluetoothManager,
-            mockStorage,
-            directExecutor,
-            directExecutor,
-            directExecutor);
-    verify(mockStorage).registerAssociatedDeviceCallback(callbackCaptor.capture());
-    when(mockStorage.getDriverAssociatedDevices()).thenReturn(userDevices);
-    when(mockStorage.getDriverAssociatedDeviceIds()).thenReturn(userDeviceIds);
-    associatedDeviceCallback = callbackCaptor.getValue();
-    connectedDeviceManager.start();
-  }
-
-  @Test
-  public void getActiveUserConnectedDevices_initiallyShouldReturnEmptyList() {
-    assertThat(connectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
-  }
-
-  @Test
-  public void getActiveUserConnectedDevices_includesNewlyConnectedDevice() {
-    String deviceId = connectNewDevice();
-    List<ConnectedDevice> activeUserDevices =
-        connectedDeviceManager.getActiveUserConnectedDevices();
-    ConnectedDevice expectedDevice =
-        new ConnectedDevice(
-            deviceId,
-            /* deviceName= */ null,
-            /* belongsToActiveUser= */ true,
-            /* hasSecureChannel= */ false);
-    assertThat(activeUserDevices).containsExactly(expectedDevice);
-  }
-
-  @Test
-  public void getActiveUserConnectedDevices_excludesDevicesNotBelongingToActiveUser() {
-    String deviceId = UUID.randomUUID().toString();
-    String otherUserDeviceId = UUID.randomUUID().toString();
-    when(mockStorage.getDriverAssociatedDeviceIds())
-        .thenReturn(ImmutableList.of(otherUserDeviceId));
-    connectedDeviceManager.addConnectedDevice(deviceId);
-    assertThat(connectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
-  }
-
-  @Test
-  public void getActiveUserConnectedDevices_reflectsSecureChannelEstablished() {
-    String deviceId = connectNewDevice();
-    connectedDeviceManager.onSecureChannelEstablished(deviceId);
-    ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    assertThat(connectedDevice.hasSecureChannel()).isTrue();
-  }
-
-  @Test
-  public void getActiveUserConnectedDevices_excludesDisconnectedDevice() {
-    String deviceId = connectNewDevice();
-    connectedDeviceManager.removeConnectedDevice(deviceId);
-    assertThat(connectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
-  }
-
-  @Test
-  public void sendMessageSecurely_throwsIllegalStateExceptionIfNoSecureChannel() {
-    connectNewDevice();
-    ConnectedDevice device = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    UUID recipientId = UUID.randomUUID();
-    byte[] message = ByteUtils.randomBytes(10);
-    DeviceMessage deviceMessage =
-        DeviceMessage.createOutgoingMessage(
-            recipientId, /* isMessageEncrypted= */ true, OperationType.CLIENT_MESSAGE, message);
-    assertThrows(
-        IllegalStateException.class,
-        () -> connectedDeviceManager.sendMessage(device, deviceMessage));
-  }
-
-  @Test
-  public void sendMessageSecurely_sendsEncryptedMessage() {
-    String deviceId = connectNewDevice();
-    connectedDeviceManager.onSecureChannelEstablished(deviceId);
-    ConnectedDevice device = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    UUID recipientId = UUID.randomUUID();
-    byte[] message = ByteUtils.randomBytes(10);
-    DeviceMessage deviceMessage =
-        DeviceMessage.createOutgoingMessage(
-            recipientId, /* isMessageEncrypted= */ true, OperationType.CLIENT_MESSAGE, message);
-    connectedDeviceManager.sendMessage(device, deviceMessage);
-    ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
-    verify(mockCarBluetoothManager).sendMessage(eq(deviceId), messageCaptor.capture());
-    assertThat(messageCaptor.getValue().isMessageEncrypted()).isTrue();
-  }
-
-  @Test
-  public void sendMessageSecurely_doesNotSendIfDeviceDisconnected() {
-    String deviceId = connectNewDevice();
-    ConnectedDevice device = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    connectedDeviceManager.removeConnectedDevice(deviceId);
-    UUID recipientId = UUID.randomUUID();
-    byte[] message = ByteUtils.randomBytes(10);
-    DeviceMessage deviceMessage =
-        DeviceMessage.createOutgoingMessage(
-            recipientId, /* isMessageEncrypted= */ true, OperationType.CLIENT_MESSAGE, message);
-    connectedDeviceManager.sendMessage(device, deviceMessage);
-    verify(mockCarBluetoothManager, never()).sendMessage(eq(deviceId), any(DeviceMessage.class));
-  }
-
-  @Test
-  public void sendMessage_sendsMessageWithoutEncryption() {
-    String deviceId = connectNewDevice();
-    ConnectedDevice device = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    UUID recipientId = UUID.randomUUID();
-    byte[] message = ByteUtils.randomBytes(10);
-    DeviceMessage deviceMessage =
-        DeviceMessage.createOutgoingMessage(
-            recipientId, /* isMessageEncrypted= */ false, OperationType.CLIENT_MESSAGE, message);
-    connectedDeviceManager.sendMessage(device, deviceMessage);
-    ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
-    verify(mockCarBluetoothManager).sendMessage(eq(deviceId), messageCaptor.capture());
-    assertThat(messageCaptor.getValue().isMessageEncrypted()).isFalse();
-  }
-
-  @Test
-  public void connectionCallback_onDeviceConnectedInvokedForNewlyConnectedDevice()
-      throws InterruptedException {
-    connectedDeviceManager.registerActiveUserConnectionCallback(
-        mockConnectionCallback, directExecutor);
-    String deviceId = connectNewDevice();
-    ArgumentCaptor<ConnectedDevice> deviceCaptor = ArgumentCaptor.forClass(ConnectedDevice.class);
-    verify(mockConnectionCallback).onDeviceConnected(deviceCaptor.capture());
-    ConnectedDevice connectedDevice = deviceCaptor.getValue();
-    assertThat(connectedDevice.getDeviceId()).isEqualTo(deviceId);
-    assertThat(connectedDevice.hasSecureChannel()).isFalse();
-  }
-
-  @Test
-  public void connectionCallback_onDeviceConnectedNotInvokedDeviceConnectedForDifferentUser()
-      throws InterruptedException {
-    connectedDeviceManager.registerActiveUserConnectionCallback(
-        mockConnectionCallback, directExecutor);
-    String deviceId = UUID.randomUUID().toString();
-    String otherUserDeviceId = UUID.randomUUID().toString();
-    when(mockStorage.getDriverAssociatedDeviceIds())
-        .thenReturn(ImmutableList.of(otherUserDeviceId));
-    connectedDeviceManager.addConnectedDevice(deviceId);
-  }
-
-  @Test
-  public void connectionCallback_onDeviceConnectedNotInvokedForDifferentBleManager()
-      throws InterruptedException {
-    String deviceId = connectNewDevice();
-    connectedDeviceManager.registerActiveUserConnectionCallback(
-        mockConnectionCallback, directExecutor);
-    connectedDeviceManager.addConnectedDevice(deviceId);
-  }
-
-  @Test
-  public void connectionCallback_onDeviceDisconnectedInvokedForActiveUserDevice()
-      throws InterruptedException {
-    String deviceId = connectNewDevice();
-    connectedDeviceManager.registerActiveUserConnectionCallback(
-        mockConnectionCallback, directExecutor);
-    connectedDeviceManager.removeConnectedDevice(deviceId);
-    ArgumentCaptor<ConnectedDevice> deviceCaptor = ArgumentCaptor.forClass(ConnectedDevice.class);
-    verify(mockConnectionCallback).onDeviceDisconnected(deviceCaptor.capture());
-    assertThat(deviceCaptor.getValue().getDeviceId()).isEqualTo(deviceId);
-  }
-
-  @Test
-  public void connectionCallback_onDeviceDisconnectedNotInvokedDeviceForDifferentUser()
-      throws InterruptedException {
-    String deviceId = UUID.randomUUID().toString();
-    connectedDeviceManager.addConnectedDevice(deviceId);
-    connectedDeviceManager.registerActiveUserConnectionCallback(
-        mockConnectionCallback, directExecutor);
-    connectedDeviceManager.removeConnectedDevice(deviceId);
-  }
-
-  @Test
-  public void unregisterConnectionCallback_removesCallbackAndNotInvoked()
-      throws InterruptedException {
-    connectedDeviceManager.registerActiveUserConnectionCallback(
-        mockConnectionCallback, directExecutor);
-    connectedDeviceManager.unregisterConnectionCallback(mockConnectionCallback);
-    connectNewDevice();
-  }
-
-  @Test
-  public void registerDeviceCallback_denyListsDuplicateRecipientId() throws InterruptedException {
-    connectNewDevice();
-    ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    DeviceCallback firstDeviceCallback = mock(DeviceCallback.class);
-    DeviceCallback secondDeviceCallback = mock(DeviceCallback.class);
-    DeviceCallback thirdDeviceCallback = mock(DeviceCallback.class);
-
-    // Register three times for following chain of events:
-    // 1. First callback registered without issue.
-    // 2. Second callback with same recipientId triggers deny listing both callbacks and issues
-    //    error callbacks on both. Both callbacks should be unregistered at this point.
-    // 3. Third callback gets rejected at registration and issues error callback.
-
-    connectedDeviceManager.registerDeviceCallback(
-        connectedDevice, recipientId, firstDeviceCallback, directExecutor);
-    connectedDeviceManager.registerDeviceCallback(
-        connectedDevice, recipientId, secondDeviceCallback, directExecutor);
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            recipientId,
-            /* isMessageEncrypted= */ false,
-            OperationType.CLIENT_MESSAGE,
-            ByteUtils.randomBytes(10));
-    connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
-    verify(firstDeviceCallback)
-        .onDeviceError(connectedDevice, Errors.DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED);
-    verify(secondDeviceCallback)
-        .onDeviceError(connectedDevice, Errors.DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED);
-    verify(firstDeviceCallback, never()).onMessageReceived(any(), any());
-    verify(secondDeviceCallback, never()).onMessageReceived(any(), any());
-
-    connectedDeviceManager.registerDeviceCallback(
-        connectedDevice, recipientId, thirdDeviceCallback, directExecutor);
-    verify(thirdDeviceCallback)
-        .onDeviceError(connectedDevice, Errors.DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED);
-  }
-
-  @Test
-  public void deviceCallback_onSecureChannelEstablishedInvoked() throws InterruptedException {
-    connectNewDevice();
-    ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    connectedDeviceManager.registerDeviceCallback(
-        connectedDevice, recipientId, mockDeviceCallback, directExecutor);
-    connectedDeviceManager.onSecureChannelEstablished(connectedDevice.getDeviceId());
-    connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    verify(mockDeviceCallback).onSecureChannelEstablished(connectedDevice);
-  }
-
-  @Test
-  public void deviceCallback_onMessageReceivedInvokedForSameRecipientId()
-      throws InterruptedException {
-    connectNewDevice();
-    ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    connectedDeviceManager.registerDeviceCallback(
-        connectedDevice, recipientId, mockDeviceCallback, directExecutor);
-    byte[] payload = ByteUtils.randomBytes(10);
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            recipientId, /* isMessageEncrypted= */ false, OperationType.CLIENT_MESSAGE, payload);
-    connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
-    verify(mockDeviceCallback).onMessageReceived(connectedDevice, message);
-  }
-
-  @Test
-  public void deviceCallback_onMessageReceivedNotInvokedForDifferentRecipientId()
-      throws InterruptedException {
-    connectNewDevice();
-    ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    connectedDeviceManager.registerDeviceCallback(
-        connectedDevice, recipientId, mockDeviceCallback, directExecutor);
-    byte[] payload = ByteUtils.randomBytes(10);
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            UUID.randomUUID(),
-            /* isMessageEncrypted= */ false,
-            OperationType.CLIENT_MESSAGE,
-            payload);
-    connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
-  }
-
-  @Test
-  public void deviceCallback_onDeviceErrorInvokedOnChannelError() throws InterruptedException {
-    connectNewDevice();
-    ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    connectedDeviceManager.registerDeviceCallback(
-        connectedDevice, recipientId, mockDeviceCallback, directExecutor);
-    connectedDeviceManager.deviceErrorOccurred(connectedDevice.getDeviceId());
-    verify(mockDeviceCallback)
-        .onDeviceError(connectedDevice, Errors.DEVICE_ERROR_INVALID_SECURITY_KEY);
-  }
-
-  @Test
-  public void unregisterDeviceCallback_removesCallbackAndNotInvoked() throws InterruptedException {
-    connectNewDevice();
-    ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    connectedDeviceManager.registerDeviceCallback(
-        connectedDevice, recipientId, mockDeviceCallback, directExecutor);
-    connectedDeviceManager.unregisterDeviceCallback(
-        connectedDevice, recipientId, mockDeviceCallback);
-    connectedDeviceManager.onSecureChannelEstablished(connectedDevice.getDeviceId());
-  }
-
-  @Test
-  public void registerDeviceCallback_sendsMissedMessageAfterRegistration()
-      throws InterruptedException {
-    connectNewDevice();
-    ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    byte[] payload = ByteUtils.randomBytes(10);
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            recipientId, /* isMessageEncrypted= */ false, OperationType.CLIENT_MESSAGE, payload);
-    connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
-    connectedDeviceManager.registerDeviceCallback(
-        connectedDevice, recipientId, mockDeviceCallback, directExecutor);
-    verify(mockDeviceCallback).onMessageReceived(connectedDevice, message);
-  }
-
-  @Test
-  public void registerDeviceCallback_sendsMultipleMissedMessagesAfterRegistration()
-      throws InterruptedException {
-    connectNewDevice();
-    ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    byte[] payload1 = ByteUtils.randomBytes(10);
-    byte[] payload2 = ByteUtils.randomBytes(10);
-    DeviceMessage message1 =
-        DeviceMessage.createOutgoingMessage(
-            recipientId, /* isMessageEncrypted= */ false, OperationType.CLIENT_MESSAGE, payload1);
-    DeviceMessage message2 =
-        DeviceMessage.createOutgoingMessage(
-            recipientId, /* isMessageEncrypted= */ false, OperationType.CLIENT_MESSAGE, payload2);
-    connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message1);
-    connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message2);
-    connectedDeviceManager.registerDeviceCallback(
-        connectedDevice, recipientId, mockDeviceCallback, directExecutor);
-    verify(mockDeviceCallback).onMessageReceived(connectedDevice, message1);
-    verify(mockDeviceCallback).onMessageReceived(connectedDevice, message2);
-  }
-
-  @Test
-  public void registerDeviceCallback_doesNotSendMissedMessageForDifferentRecipient()
-      throws InterruptedException {
-    connectNewDevice();
-    ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    byte[] payload = ByteUtils.randomBytes(10);
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            UUID.randomUUID(),
-            /* isMessageEncrypted= */ false,
-            OperationType.CLIENT_MESSAGE,
-            payload);
-    connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
-    connectedDeviceManager.registerDeviceCallback(
-        connectedDevice, recipientId, mockDeviceCallback, directExecutor);
-  }
-
-  @Test
-  public void registerDeviceCallback_doesNotSendMissedMessageForDifferentDevice()
-      throws InterruptedException {
-    connectNewDevice();
-    connectNewDevice();
-    List<ConnectedDevice> connectedDevices = connectedDeviceManager.getActiveUserConnectedDevices();
-    ConnectedDevice connectedDevice = connectedDevices.get(0);
-    ConnectedDevice otherDevice = connectedDevices.get(1);
-    byte[] payload = ByteUtils.randomBytes(10);
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            recipientId, /* isMessageEncrypted= */ false, OperationType.CLIENT_MESSAGE, payload);
-    connectedDeviceManager.onMessageReceived(otherDevice.getDeviceId(), message);
-    connectedDeviceManager.registerDeviceCallback(
-        connectedDevice, recipientId, mockDeviceCallback, directExecutor);
-  }
-
-  @Test
-  public void onAssociationCompleted_disconnectsOriginalDeviceAndReconnectsAsActiveUser()
-      throws InterruptedException {
-    String deviceId = UUID.randomUUID().toString();
-    connectedDeviceManager.addConnectedDevice(deviceId);
-    connectedDeviceManager.registerActiveUserConnectionCallback(
-        mockConnectionCallback, directExecutor);
-    when(mockStorage.getDriverAssociatedDeviceIds()).thenReturn(ImmutableList.of(deviceId));
-    connectedDeviceManager.onAssociationCompleted(deviceId);
-  }
-
-  @Test
-  public void deviceAssociationCallback_onAssociatedDeviceAdded() throws InterruptedException {
-    connectedDeviceManager.registerDeviceAssociationCallback(
-        mockDeviceAssociationCallback, directExecutor);
-    String deviceId = UUID.randomUUID().toString();
-    AssociatedDevice testDevice =
-        new AssociatedDevice(
-            deviceId, TEST_DEVICE_ADDRESS, TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
-    associatedDeviceCallback.onAssociatedDeviceAdded(testDevice);
-    verify(mockDeviceAssociationCallback).onAssociatedDeviceAdded(eq(testDevice));
-  }
-
-  @Test
-  public void deviceAssociationCallback_onAssociationDeviceRemoved() throws InterruptedException {
-    connectedDeviceManager.registerDeviceAssociationCallback(
-        mockDeviceAssociationCallback, directExecutor);
-    String deviceId = UUID.randomUUID().toString();
-    AssociatedDevice testDevice =
-        new AssociatedDevice(
-            deviceId, TEST_DEVICE_ADDRESS, TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
-    associatedDeviceCallback.onAssociatedDeviceRemoved(testDevice);
-    verify(mockDeviceAssociationCallback).onAssociatedDeviceRemoved(eq(testDevice));
-  }
-
-  @Test
-  public void deviceAssociationCallback_onAssociatedDeviceUpdated() throws InterruptedException {
-    connectedDeviceManager.registerDeviceAssociationCallback(
-        mockDeviceAssociationCallback, directExecutor);
-    String deviceId = UUID.randomUUID().toString();
-    AssociatedDevice testDevice =
-        new AssociatedDevice(
-            deviceId, TEST_DEVICE_ADDRESS, TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
-    associatedDeviceCallback.onAssociatedDeviceUpdated(testDevice);
-    verify(mockDeviceAssociationCallback).onAssociatedDeviceUpdated(eq(testDevice));
-  }
-
-  @Test
-  public void removeConnectedDevice_startsAdvertisingForActiveUserDeviceOnActiveUserDisconnect() {
-    String deviceId = UUID.randomUUID().toString();
-    when(mockStorage.getDriverAssociatedDeviceIds()).thenReturn(ImmutableList.of(deviceId));
-    AssociatedDevice device =
-        new AssociatedDevice(
-            deviceId, TEST_DEVICE_ADDRESS, TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
-    when(mockStorage.getDriverAssociatedDevices()).thenReturn(ImmutableList.of(device));
-    connectedDeviceManager.addConnectedDevice(deviceId);
-    connectedDeviceManager.removeConnectedDevice(deviceId);
-    verify(mockCarBluetoothManager).connectToDevice(eq(UUID.fromString(deviceId)));
-  }
-
-  @Test
-  public void removeConnectedDevice_startsAdvertisingForActiveUserDeviceOnLastDeviceDisconnect() {
-    String deviceId = UUID.randomUUID().toString();
-    String userDeviceId = UUID.randomUUID().toString();
-    when(mockStorage.getDriverAssociatedDeviceIds()).thenReturn(ImmutableList.of(userDeviceId));
-    AssociatedDevice userDevice =
-        new AssociatedDevice(
-            userDeviceId, TEST_DEVICE_ADDRESS, TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
-    when(mockStorage.getDriverAssociatedDevices()).thenReturn(ImmutableList.of(userDevice));
-    connectedDeviceManager.addConnectedDevice(deviceId);
-    connectedDeviceManager.removeConnectedDevice(deviceId);
-    verify(mockCarBluetoothManager).connectToDevice(eq(UUID.fromString(userDeviceId)));
-  }
-
-  @Test
-  public void removeConnectedDevice_doesNotAdvertiseForNonActiveUserDeviceNotLastDevice() {
-    String deviceId = UUID.randomUUID().toString();
-    String userDeviceId = UUID.randomUUID().toString();
-    when(mockStorage.getDriverAssociatedDeviceIds()).thenReturn(ImmutableList.of(userDeviceId));
-    AssociatedDevice userDevice =
-        new AssociatedDevice(
-            userDeviceId, TEST_DEVICE_ADDRESS, TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
-    when(mockStorage.getDriverAssociatedDevices()).thenReturn(ImmutableList.of(userDevice));
-    connectedDeviceManager.addConnectedDevice(deviceId);
-    connectedDeviceManager.addConnectedDevice(userDeviceId);
-    connectedDeviceManager.removeConnectedDevice(deviceId);
-    verify(mockCarBluetoothManager, never()).connectToDevice(any());
-  }
-
-  @Test
-  public void removeConnectedDevice_startsAdvertisingForActiveUserDeviceWithNullDevice() {
-    String deviceId = UUID.randomUUID().toString();
-    when(mockStorage.getDriverAssociatedDeviceIds()).thenReturn(ImmutableList.of(deviceId));
-    AssociatedDevice device =
-        new AssociatedDevice(
-            deviceId, TEST_DEVICE_ADDRESS, TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
-    when(mockStorage.getDriverAssociatedDevices()).thenReturn(ImmutableList.of(device));
-    connectedDeviceManager.removeConnectedDevice(deviceId);
-    verify(mockCarBluetoothManager).connectToDevice(eq(UUID.fromString(deviceId)));
-  }
-
-  @Test
-  public void removeActiveUserAssociatedDevice_deletesAssociatedDeviceFromStorage() {
-    String deviceId = UUID.randomUUID().toString();
-    connectedDeviceManager.removeActiveUserAssociatedDevice(deviceId);
-    verify(mockStorage).removeAssociatedDevice(deviceId);
-  }
-
-  @Test
-  public void removeActiveUserAssociatedDevice_disconnectsIfConnected() {
-    String deviceId = connectNewDevice();
-    connectedDeviceManager.removeActiveUserAssociatedDevice(deviceId);
-    verify(mockCarBluetoothManager).disconnectDevice(deviceId);
-  }
-
-  @Test
-  public void enableAssociatedDeviceConnection_enableDeviceConnectionInStorage() {
-    String deviceId = UUID.randomUUID().toString();
-    connectedDeviceManager.enableAssociatedDeviceConnection(deviceId);
-    verify(mockStorage).updateAssociatedDeviceConnectionEnabled(deviceId, true);
-  }
-
-  @Test
-  public void enableAssociatedDeviceConnection_startsDiscoveryAfterBeingDisabled() {
-    UUID deviceId = UUID.randomUUID();
-    when(mockStorage.getDriverAssociatedDeviceIds())
-        .thenReturn(ImmutableList.of(deviceId.toString()));
-    AssociatedDevice device =
-        new AssociatedDevice(
-            deviceId.toString(),
-            TEST_DEVICE_ADDRESS,
-            TEST_DEVICE_NAME,
-            /* isConnectionEnabled= */ true);
-    when(mockStorage.getDriverAssociatedDevices()).thenReturn(ImmutableList.of(device));
-    connectedDeviceManager.connectToActiveUserDevice();
-    connectedDeviceManager.disableAssociatedDeviceConnection(deviceId.toString());
-    connectedDeviceManager.enableAssociatedDeviceConnection(deviceId.toString());
-    verify(mockCarBluetoothManager, times(2)).connectToDevice(deviceId);
-  }
-
-  @Test
-  public void disableAssociatedDeviceConnection_disableDeviceConnectionInStorage() {
-    String deviceId = UUID.randomUUID().toString();
-    connectedDeviceManager.disableAssociatedDeviceConnection(deviceId);
-    verify(mockStorage).updateAssociatedDeviceConnectionEnabled(deviceId, false);
-  }
-
-  @Test
-  public void disableAssociatedDeviceConnection_disconnectsIfConnected() {
-    String deviceId = connectNewDevice();
-    connectedDeviceManager.disableAssociatedDeviceConnection(deviceId);
-    verify(mockCarBluetoothManager).disconnectDevice(deviceId);
-  }
-
-  @Test
-  public void onMessageReceived_deliversMessageIfDelegateIsNull() throws InterruptedException {
-    connectNewDevice();
-    ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    connectedDeviceManager.registerDeviceCallback(
-        connectedDevice, recipientId, mockDeviceCallback, directExecutor);
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            recipientId,
-            /* isMessageEncrypted= */ false,
-            OperationType.CLIENT_MESSAGE,
-            ByteUtils.randomBytes(10));
-    connectedDeviceManager.setMessageDeliveryDelegate(null);
-    connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
-  }
-
-  @Test
-  public void onMessageReceived_deliversMessageIfDelegateAccepts() throws InterruptedException {
-    connectNewDevice();
-    ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    connectedDeviceManager.registerDeviceCallback(
-        connectedDevice, recipientId, mockDeviceCallback, directExecutor);
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            recipientId,
-            /* isMessageEncrypted= */ false,
-            OperationType.CLIENT_MESSAGE,
-            ByteUtils.randomBytes(10));
-    MessageDeliveryDelegate delegate = device -> true;
-    connectedDeviceManager.setMessageDeliveryDelegate(delegate);
-    connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
-  }
-
-  @Test
-  public void onMessageReceived_doesNotDeliverMessageIfDelegateRejects()
-      throws InterruptedException {
-    connectNewDevice();
-    ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
-    connectedDeviceManager.registerDeviceCallback(
-        connectedDevice, recipientId, mockDeviceCallback, directExecutor);
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            recipientId,
-            /* isMessageEncrypted= */ false,
-            OperationType.CLIENT_MESSAGE,
-            ByteUtils.randomBytes(10));
-    MessageDeliveryDelegate delegate = device -> false;
-    connectedDeviceManager.setMessageDeliveryDelegate(delegate);
-    connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
-  }
-
-  @NonNull
-  private String connectNewDevice() {
-    String deviceId = UUID.randomUUID().toString();
-    AssociatedDevice device =
-        new AssociatedDevice(
-            deviceId, TEST_DEVICE_ADDRESS, TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
-    userDeviceIds.add(deviceId);
-    userDevices.add(device);
-    connectedDeviceManager.addConnectedDevice(deviceId);
-    return deviceId;
-  }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/CompanionConnectorTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/CompanionConnectorTest.kt
index 6516e95..b09c7d4 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/CompanionConnectorTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/CompanionConnectorTest.kt
@@ -7,7 +7,6 @@
 import android.content.pm.PackageManager
 import android.content.pm.ResolveInfo
 import android.content.pm.ServiceInfo
-import android.os.IBinder
 import android.os.Looper
 import android.os.ParcelUuid
 import android.os.RemoteException
@@ -16,14 +15,12 @@
 import com.google.android.companionprotos.QueryResponse
 import com.google.android.companionprotos.SystemQuery
 import com.google.android.companionprotos.SystemQueryType
-import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_ASSOCIATION
 import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_FEATURE_COORDINATOR
 import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_FEATURE_COORDINATOR_FG
-import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_REMOTE_FEATURE
-import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_REMOTE_FEATURE_FG
 import com.google.android.connecteddevice.api.Connector.Companion.USER_TYPE_ALL
 import com.google.android.connecteddevice.api.Connector.Companion.USER_TYPE_DRIVER
 import com.google.android.connecteddevice.api.Connector.Companion.USER_TYPE_PASSENGER
+import com.google.android.connecteddevice.core.util.mockToBeAlive
 import com.google.android.connecteddevice.model.AssociatedDevice
 import com.google.android.connecteddevice.model.ConnectedDevice
 import com.google.android.connecteddevice.model.DeviceMessage
@@ -54,24 +51,14 @@
 
   private val mockCallback = mock<Connector.Callback>()
 
-  private val mockFeatureCoordinator = mock<IFeatureCoordinator.Stub>()
-
-  private val mockConnectedDeviceManager = mock<IConnectedDeviceManager.Stub>()
-
-  private val mockAssociatedDeviceManager = mock<IAssociatedDeviceManager.Stub>()
+  private val mockFeatureCoordinator = mockToBeAlive<IFeatureCoordinator>()
 
   private val context = FakeContext(mockPackageManager)
 
-  private val aliveBinder = mock<IBinder>()
-
   private lateinit var defaultConnector: CompanionConnector
 
   @Before
   fun setUp() {
-    whenever(aliveBinder.isBinderAlive).thenReturn(true)
-    whenever(mockFeatureCoordinator.asBinder()).thenReturn(aliveBinder)
-    whenever(mockConnectedDeviceManager.asBinder()).thenReturn(aliveBinder)
-    whenever(mockAssociatedDeviceManager.asBinder()).thenReturn(aliveBinder)
     defaultConnector =
       CompanionConnector(context).apply {
         featureCoordinator = mockFeatureCoordinator
@@ -82,7 +69,7 @@
 
   @Test
   fun connect_bindsWithFgActionWhenIsForegroundProcessIsTrue() {
-    whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
     val connector = CompanionConnector(context, isForegroundProcess = true)
 
     connector.connect()
@@ -92,7 +79,7 @@
 
   @Test
   fun connect_bindsWithBgActionWhenIsForegroundProcessIsFalse() {
-    whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
     val connector = CompanionConnector(context, isForegroundProcess = false)
 
     connector.connect()
@@ -102,65 +89,29 @@
 
   @Test
   fun connect_fgRetriesBindWithRemoteFeatureActionIfFeatureCoordinatorReturnsNullBinding() {
-    whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
     val connector = CompanionConnector(context, isForegroundProcess = true)
 
     connector.connect()
     context.serviceConnection.firstOrNull()?.onNullBinding(ComponentName(PACKAGE_NAME, FG_NAME))
 
-    assertThat(context.bindingActions)
-      .containsExactly(
-        ACTION_BIND_FEATURE_COORDINATOR_FG,
-        ACTION_BIND_REMOTE_FEATURE_FG,
-        ACTION_BIND_ASSOCIATION
-      )
+    assertThat(context.bindingActions).containsExactly(ACTION_BIND_FEATURE_COORDINATOR_FG)
   }
 
   @Test
   fun connect_bgRetriesBindWithRemoteFeatureActionIfFeatureCoordinatorReturnsNullBinding() {
-    whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
     val connector = CompanionConnector(context, isForegroundProcess = false)
 
     connector.connect()
     context.serviceConnection.firstOrNull()?.onNullBinding(ComponentName(PACKAGE_NAME, BG_NAME))
 
-    assertThat(context.bindingActions)
-      .containsExactly(
-        ACTION_BIND_FEATURE_COORDINATOR,
-        ACTION_BIND_REMOTE_FEATURE,
-        ACTION_BIND_ASSOCIATION
-      )
-  }
-
-  @Test
-  fun connect_fgRetriesBindWithRemoteFeatureActionIfFeatureCoordinatorActionMissing() {
-    whenever(mockPackageManager.queryIntentServices(any(), any()))
-      .thenAnswer(connectedDeviceManagerOnlyAnswer)
-    val connector = CompanionConnector(context, isForegroundProcess = true)
-
-    connector.connect()
-    context.serviceConnection.firstOrNull()?.onNullBinding(ComponentName(PACKAGE_NAME, BG_NAME))
-
-    assertThat(context.bindingActions)
-      .containsExactly(ACTION_BIND_REMOTE_FEATURE_FG, ACTION_BIND_ASSOCIATION)
-  }
-
-  @Test
-  fun connect_bgRetriesBindWithRemoteFeatureActionIfFeatureCoordinatorActionMissing() {
-    whenever(mockPackageManager.queryIntentServices(any(), any()))
-      .thenAnswer(connectedDeviceManagerOnlyAnswer)
-    val connector = CompanionConnector(context, isForegroundProcess = false)
-
-    connector.connect()
-    context.serviceConnection.firstOrNull()?.onNullBinding(ComponentName(PACKAGE_NAME, BG_NAME))
-
-    assertThat(context.bindingActions)
-      .containsExactly(ACTION_BIND_REMOTE_FEATURE, ACTION_BIND_ASSOCIATION)
+    assertThat(context.bindingActions).containsExactly(ACTION_BIND_FEATURE_COORDINATOR)
   }
 
   @Test
   fun disconnect_invokesOnDisconnectedWhenFeatureCoordinatorConnected() {
-    whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
     val connector =
       CompanionConnector(context, isForegroundProcess = false).apply { callback = mockCallback }
 
@@ -168,26 +119,7 @@
     context
       .serviceConnection
       .firstOrNull()
-      ?.onServiceConnected(ComponentName(PACKAGE_NAME, BG_NAME), mockFeatureCoordinator)
-    connector.disconnect()
-
-    verify(mockCallback).onDisconnected()
-  }
-
-  @Test
-  fun disconnect_invokesOnDisconnectedWhenConnectedDeviceManagerConnected() {
-    whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
-    val connector =
-      CompanionConnector(context, isForegroundProcess = false).apply {
-        callback = mockCallback
-        associatedDeviceManager = mockAssociatedDeviceManager
-      }
-
-    connector.connect()
-    context
-      .serviceConnection
-      .firstOrNull()
-      ?.onServiceConnected(ComponentName(PACKAGE_NAME, BG_NAME), mockConnectedDeviceManager)
+      ?.onServiceConnected(ComponentName(PACKAGE_NAME, BG_NAME), mockFeatureCoordinator.asBinder())
     connector.disconnect()
 
     verify(mockCallback).onDisconnected()
@@ -195,7 +127,7 @@
 
   @Test
   fun disconnect_notInvokedWithNoBoundService() {
-    whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
     val connector =
       CompanionConnector(context, isForegroundProcess = false).apply { callback = mockCallback }
 
@@ -206,25 +138,8 @@
   }
 
   @Test
-  fun disconnect_nullsOutFeatureCoordinatorAndConnectedDeviceManager() {
-    whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
-    val connector = CompanionConnector(context, isForegroundProcess = false)
-
-    connector.connect()
-    context
-      .serviceConnection
-      .firstOrNull()
-      ?.onServiceConnected(ComponentName(PACKAGE_NAME, BG_NAME), mockFeatureCoordinator)
-    connector.disconnect()
-
-    assertThat(connector.featureCoordinator).isNull()
-    assertThat(connector.connectedDeviceManager).isNull()
-    assertThat(connector.associatedDeviceManager).isNull()
-  }
-
-  @Test
   fun disconnect_doesNotThrowWhileUnbindingUnboundService() {
-    whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
     val connector =
       CompanionConnector(FailingContext(mockPackageManager), isForegroundProcess = false).apply {
         callback = mockCallback
@@ -235,7 +150,7 @@
 
   @Test
   fun onConnected_invokedWhenBindingSucceeds() {
-    whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
     val connector =
       CompanionConnector(context, isForegroundProcess = false).apply { callback = mockCallback }
 
@@ -243,14 +158,14 @@
     context
       .serviceConnection
       .firstOrNull()
-      ?.onServiceConnected(ComponentName(PACKAGE_NAME, BG_NAME), mockFeatureCoordinator)
+      ?.onServiceConnected(ComponentName(PACKAGE_NAME, BG_NAME), mockFeatureCoordinator.asBinder())
 
     verify(mockCallback).onConnected()
   }
 
   @Test
   fun onDisconnected_invokedWhenServiceDisconnects() {
-    whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
     val connector =
       CompanionConnector(context, isForegroundProcess = false).apply { callback = mockCallback }
 
@@ -259,7 +174,7 @@
     context
       .serviceConnection
       .firstOrNull()
-      ?.onServiceConnected(componentName, mockFeatureCoordinator)
+      ?.onServiceConnected(componentName, mockFeatureCoordinator.asBinder())
     context.serviceConnection.firstOrNull()?.onServiceDisconnected(componentName)
 
     verify(mockCallback).onDisconnected()
@@ -267,8 +182,7 @@
 
   @Test
   fun onFailedToConnect_invokedWhenServiceIsNotFound() {
-    whenever(mockPackageManager.queryIntentServices(any(), any()))
-      .thenAnswer(featureCoordinatorOnlyAnswer)
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
     val connector =
       CompanionConnector(context, isForegroundProcess = false).apply { callback = mockCallback }
 
@@ -280,7 +194,7 @@
 
   @Test
   fun onFailedToConnect_invokedWithNullBindingFromFeatureCoordinatorAndNoCDMService() {
-    whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(notFoundAnswer)
+    setQueryIntentServicesAnswer(notFoundAnswer)
     val connector =
       CompanionConnector(context, isForegroundProcess = false).apply { callback = mockCallback }
 
@@ -291,7 +205,7 @@
 
   @Test
   fun onFailedToConnect_invokedAfterBindingRetryLimitExceeded() {
-    whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
     val failingContext = FailingContext(mockPackageManager)
     val connector =
       CompanionConnector(failingContext, isForegroundProcess = false).apply {
@@ -307,143 +221,17 @@
   }
 
   @Test
-  fun featureCoordinatorAndConnectedDeviceManager_areNotNullAfterServiceConnection() {
-    whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+  fun featureCoordinator_isNotNullAfterServiceConnection() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
     val connector = CompanionConnector(context, isForegroundProcess = false)
 
     connector.connect()
     context
       .serviceConnection
       .firstOrNull()
-      ?.onServiceConnected(ComponentName(PACKAGE_NAME, BG_NAME), mockFeatureCoordinator)
+      ?.onServiceConnected(ComponentName(PACKAGE_NAME, BG_NAME), mockFeatureCoordinator.asBinder())
 
     assertThat(connector.featureCoordinator).isNotNull()
-    assertThat(connector.connectedDeviceManager).isNotNull()
-  }
-
-  @Test
-  fun connectedDeviceManager_isNotNullAfterServiceConnectionWhenLegacy() {
-    whenever(mockPackageManager.queryIntentServices(any(), any()))
-      .thenAnswer(connectedDeviceManagerOnlyAnswer)
-    val connector = CompanionConnector(context, isForegroundProcess = false)
-
-    connector.connect()
-    context.serviceConnection[0].onServiceConnected(
-      ComponentName(PACKAGE_NAME, BG_NAME),
-      mockConnectedDeviceManager
-    )
-    context.serviceConnection[1].onServiceConnected(
-      ComponentName(PACKAGE_NAME, BG_NAME),
-      mockAssociatedDeviceManager
-    )
-    assertThat(connector.connectedDeviceManager).isNotNull()
-    assertThat(connector.associatedDeviceManager).isNotNull()
-  }
-
-  @Test
-  fun connectedDeviceManager_wrapsFeatureCoordinatorWhenNotLegacy() {
-    val connectedDeviceManager =
-      CompanionConnector.createConnectedDeviceManagerWrapper(mockFeatureCoordinator)
-    val connectedDevice =
-      ConnectedDevice(
-        /* deviceId= */ UUID.randomUUID().toString(),
-        /* deviceName= */ null,
-        /* belongsToDriver= */ true,
-        /* isConnectionEnabled= */ true
-      )
-
-    connectedDeviceManager.activeUserConnectedDevices
-    verify(mockFeatureCoordinator).connectedDevicesForDriver
-
-    val loggerId = 0
-    val logRecords = ByteArray(0)
-    connectedDeviceManager.processLogRecords(loggerId, logRecords)
-    verify(mockFeatureCoordinator).processLogRecords(loggerId, logRecords)
-
-    val connectionCallback = mock<IConnectionCallback>()
-    connectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback)
-    verify(mockFeatureCoordinator).registerDriverConnectionCallback(connectionCallback)
-    connectedDeviceManager.unregisterConnectionCallback(connectionCallback)
-    verify(mockFeatureCoordinator).unregisterConnectionCallback(connectionCallback)
-
-    val deviceAssociationCallback = mock<IDeviceAssociationCallback>()
-    connectedDeviceManager.registerDeviceAssociationCallback(deviceAssociationCallback)
-    verify(mockFeatureCoordinator).registerDeviceAssociationCallback(deviceAssociationCallback)
-    connectedDeviceManager.unregisterDeviceAssociationCallback(deviceAssociationCallback)
-    verify(mockFeatureCoordinator).unregisterDeviceAssociationCallback(deviceAssociationCallback)
-
-    val deviceCallback = mock<IDeviceCallback>()
-    val recipientId = ParcelUuid(UUID.randomUUID())
-    connectedDeviceManager.registerDeviceCallback(connectedDevice, recipientId, deviceCallback)
-    verify(mockFeatureCoordinator)
-      .registerDeviceCallback(connectedDevice, recipientId, deviceCallback)
-    connectedDeviceManager.unregisterDeviceCallback(connectedDevice, recipientId, deviceCallback)
-    verify(mockFeatureCoordinator)
-      .unregisterDeviceCallback(connectedDevice, recipientId, deviceCallback)
-
-    val logRequestedListener = mock<IOnLogRequestedListener>()
-    connectedDeviceManager.registerOnLogRequestedListener(loggerId, logRequestedListener)
-    verify(mockFeatureCoordinator).registerOnLogRequestedListener(loggerId, logRequestedListener)
-    connectedDeviceManager.unregisterOnLogRequestedListener(loggerId, logRequestedListener)
-    verify(mockFeatureCoordinator).unregisterOnLogRequestedListener(loggerId, logRequestedListener)
-
-    val message =
-      DeviceMessage.createOutgoingMessage(
-        recipientId.uuid,
-        /* isMessageEncrypted= */ false,
-        DeviceMessage.OperationType.CLIENT_MESSAGE,
-        ByteUtils.randomBytes(10)
-      )
-    connectedDeviceManager.sendMessage(connectedDevice, message)
-    verify(mockFeatureCoordinator).sendMessage(connectedDevice, message)
-  }
-
-  @Test
-  fun associatedDeviceManager_wrapsFeatureCoordinatorWhenNotLegacy() {
-    val associatedDeviceManager =
-      CompanionConnector.createAssociatedDeviceManagerWrapper(mockFeatureCoordinator)
-    val mockAssociationCallback = mock<IAssociationCallback>()
-    val mockDeviceAssociationCallback = mock<IDeviceAssociationCallback>()
-    val mockConnectionCallback = mock<IConnectionCallback>()
-    val mockListener = mock<IOnAssociatedDevicesRetrievedListener>()
-    val deviceId = UUID.randomUUID().toString()
-
-    associatedDeviceManager.startAssociation(mockAssociationCallback)
-    verify(mockFeatureCoordinator).startAssociation(mockAssociationCallback)
-
-    associatedDeviceManager.stopAssociation()
-    verify(mockFeatureCoordinator).stopAssociation()
-
-    associatedDeviceManager.retrievedActiveUserAssociatedDevices(mockListener)
-    verify(mockFeatureCoordinator).retrieveAssociatedDevicesForDriver(mockListener)
-
-    associatedDeviceManager.acceptVerification()
-    verify(mockFeatureCoordinator).acceptVerification()
-
-    associatedDeviceManager.removeAssociatedDevice(deviceId)
-    verify(mockFeatureCoordinator).removeAssociatedDevice(deviceId)
-
-    associatedDeviceManager.registerDeviceAssociationCallback(mockDeviceAssociationCallback)
-    verify(mockFeatureCoordinator).registerDeviceAssociationCallback(mockDeviceAssociationCallback)
-
-    associatedDeviceManager.unregisterDeviceAssociationCallback(mockDeviceAssociationCallback)
-    verify(mockFeatureCoordinator)
-      .unregisterDeviceAssociationCallback(mockDeviceAssociationCallback)
-
-    associatedDeviceManager.activeUserConnectedDevices
-    verify(mockFeatureCoordinator).connectedDevicesForDriver
-
-    associatedDeviceManager.registerConnectionCallback(mockConnectionCallback)
-    verify(mockFeatureCoordinator).registerDriverConnectionCallback(mockConnectionCallback)
-
-    associatedDeviceManager.unregisterConnectionCallback(mockConnectionCallback)
-    verify(mockFeatureCoordinator).unregisterConnectionCallback(mockConnectionCallback)
-
-    associatedDeviceManager.enableAssociatedDeviceConnection(deviceId)
-    verify(mockFeatureCoordinator).enableAssociatedDeviceConnection(deviceId)
-
-    associatedDeviceManager.disableAssociatedDeviceConnection(deviceId)
-    verify(mockFeatureCoordinator).disableAssociatedDeviceConnection(deviceId)
   }
 
   @Test
@@ -455,6 +243,29 @@
   }
 
   @Test
+  fun connect_multipleCallsOnlyRegistersCallbacksOnce() {
+    val featureId = ParcelUuid(UUID.randomUUID())
+    val connector =
+      CompanionConnector(context, userType = USER_TYPE_DRIVER).apply {
+        featureCoordinator = mockFeatureCoordinator
+        this.featureId = featureId
+      }
+    val device =
+      ConnectedDevice(
+        UUID.randomUUID().toString(),
+        /* deviceName= */ "",
+        /* belongsToDriver= */ true,
+        /* hasSecureChannel= */ false
+      )
+    whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
+
+    connector.connect()
+    connector.connect()
+
+    verify(mockFeatureCoordinator).registerDeviceCallback(eq(device), eq(featureId), any())
+  }
+
+  @Test
   fun connect_userTypeDriverRegistersDriverConnectionCallbacks() {
     val connector =
       CompanionConnector(context, userType = USER_TYPE_DRIVER).apply {
@@ -622,69 +433,6 @@
   }
 
   @Test
-  fun disconnect_unregistersConnectedDeviceManagerCallbacks() {
-    defaultConnector.apply {
-      featureCoordinator = null
-      connectedDeviceManager = mockConnectedDeviceManager
-    }
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
-    whenever(mockConnectedDeviceManager.activeUserConnectedDevices).thenReturn(listOf(device))
-
-    defaultConnector.disconnect()
-
-    verify(mockConnectedDeviceManager).unregisterConnectionCallback(any())
-    verify(mockConnectedDeviceManager).unregisterDeviceAssociationCallback(any())
-    verify(mockConnectedDeviceManager).unregisterOnLogRequestedListener(any(), any())
-    verify(mockConnectedDeviceManager)
-      .unregisterDeviceCallback(eq(device), eq(defaultConnector.featureId), any())
-  }
-
-  @Test
-  fun disconnect_connectedDeviceManagerWithoutFeatureIdDoesNotThrow() {
-    defaultConnector.apply {
-      featureId = null
-      featureCoordinator = null
-      connectedDeviceManager = mockConnectedDeviceManager
-    }
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
-    whenever(mockConnectedDeviceManager.activeUserConnectedDevices).thenReturn(listOf(device))
-
-    defaultConnector.disconnect()
-  }
-
-  @Test
-  fun disconnect_connectedDeviceManagerRemoteExceptionIsCaught() {
-    whenever(mockConnectedDeviceManager.unregisterConnectionCallback(any()))
-      .thenThrow(RemoteException())
-    defaultConnector.apply {
-      featureCoordinator = null
-      connectedDeviceManager = mockConnectedDeviceManager
-    }
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
-    whenever(mockConnectedDeviceManager.activeUserConnectedDevices).thenReturn(listOf(device))
-
-    defaultConnector.disconnect()
-  }
-
-  @Test
   fun onDeviceConnected_registersDeviceCallbackWithFeatureId() {
     val device =
       ConnectedDevice(
@@ -765,31 +513,6 @@
   }
 
   @Test
-  fun onDeviceDisconnected_unregistersDeviceCallbackConnectedDeviceManager() {
-    defaultConnector.apply {
-      featureCoordinator = null
-      connectedDeviceManager = mockConnectedDeviceManager
-      associatedDeviceManager = mockAssociatedDeviceManager
-    }
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
-    defaultConnector.connect()
-
-    argumentCaptor<IConnectionCallback> {
-      verify(mockConnectedDeviceManager).registerActiveUserConnectionCallback(capture())
-      firstValue.onDeviceDisconnected(device)
-    }
-
-    verify(mockConnectedDeviceManager)
-      .unregisterDeviceCallback(eq(device), eq(defaultConnector.featureId), any())
-  }
-
-  @Test
   fun onSecureChannelEstablished_invokedWhenChannelEstablished() {
     val device =
       ConnectedDevice(
@@ -1040,7 +763,6 @@
   @Test
   fun onMessageFailedToSend_invokedWhenSendMessageIdCalledBeforeServiceConnection() {
     defaultConnector.featureCoordinator = null
-    defaultConnector.connectedDeviceManager = null
     val deviceId = UUID.randomUUID().toString()
     val message = ByteUtils.randomBytes(10)
 
@@ -1052,7 +774,6 @@
   @Test
   fun onMessageFailedToSend_invokedWhenSendMessageCalledBeforeServiceConnection() {
     defaultConnector.featureCoordinator = null
-    defaultConnector.connectedDeviceManager = null
     val device =
       ConnectedDevice(
         UUID.randomUUID().toString(),
@@ -1137,7 +858,6 @@
   @Test
   fun getConnectedDeviceById_returnsNullBeforeServiceConnection() {
     defaultConnector.featureCoordinator = null
-    defaultConnector.connectedDeviceManager = null
 
     assertThat(defaultConnector.getConnectedDeviceById(UUID.randomUUID().toString())).isNull()
   }
@@ -1312,7 +1032,6 @@
   @Test
   fun sendQuery_queryCallbackOnQueryNotSentInvokedBeforeServiceConnectionWithId() {
     defaultConnector.featureCoordinator = null
-    defaultConnector.connectedDeviceManager = null
     val deviceId = UUID.randomUUID().toString()
     val request = ByteUtils.randomBytes(10)
     val parameters = ByteUtils.randomBytes(10)
@@ -1326,7 +1045,6 @@
   @Test
   fun sendQuery_queryCallbackOnQueryNotSentInvokedBeforeServiceConnectionWithDevice() {
     defaultConnector.featureCoordinator = null
-    defaultConnector.connectedDeviceManager = null
     val device =
       ConnectedDevice(
         UUID.randomUUID().toString(),
@@ -1561,7 +1279,6 @@
   @Test
   fun respondToQuery_doesNotThrowBeforeServiceConnectionWithDevice() {
     defaultConnector.featureCoordinator = null
-    defaultConnector.connectedDeviceManager = null
     val device =
       ConnectedDevice(
         UUID.randomUUID().toString(),
@@ -1781,36 +1498,8 @@
   }
 
   @Test
-  fun createLocalConnector_createsConnectorWithLegacyManagers() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
-    whenever(mockConnectedDeviceManager.activeUserConnectedDevices).thenReturn(listOf(device))
-
-    val connector =
-      CompanionConnector.createLocalConnector(
-        context,
-        USER_TYPE_ALL,
-        mockConnectedDeviceManager,
-        mockAssociatedDeviceManager
-      )
-
-    assertThat(connector.connectedDeviceManager).isEqualTo(mockConnectedDeviceManager)
-    assertThat(connector.associatedDeviceManager).isEqualTo(mockAssociatedDeviceManager)
-    assertThat(connector.connectedDevices).containsExactly(device)
-    assertThat(connector.isConnected).isTrue()
-  }
-
-  @Test
   fun connectedDevices_returnsEmptyListWhenPlatformIsDisconnected() {
-    defaultConnector.apply {
-      featureCoordinator = null
-      connectedDeviceManager = null
-    }
+    defaultConnector.apply { featureCoordinator = null }
 
     assertThat(defaultConnector.connectedDevices).isEmpty()
   }
@@ -1875,12 +1564,6 @@
       .isEqualTo(defaultConnector.featureCoordinator?.asBinder())
     assertThat(defaultConnector.binderForAction(ACTION_BIND_FEATURE_COORDINATOR_FG))
       .isEqualTo(defaultConnector.featureCoordinator?.asBinder())
-    assertThat(defaultConnector.binderForAction(ACTION_BIND_REMOTE_FEATURE))
-      .isEqualTo(defaultConnector.connectedDeviceManager?.asBinder())
-    assertThat(defaultConnector.binderForAction(ACTION_BIND_REMOTE_FEATURE_FG))
-      .isEqualTo(defaultConnector.connectedDeviceManager?.asBinder())
-    assertThat(defaultConnector.binderForAction(ACTION_BIND_ASSOCIATION))
-      .isEqualTo(defaultConnector.associatedDeviceManager?.asBinder())
     assertThat(defaultConnector.binderForAction("")).isNull()
   }
 
@@ -1894,22 +1577,6 @@
   }
 
   @Test
-  fun startAssociation_startsAssociationOnAssociatedDeviceManager() {
-    val legacyConnector =
-      CompanionConnector.createLocalConnector(
-        context,
-        USER_TYPE_DRIVER,
-        mockConnectedDeviceManager,
-        mockAssociatedDeviceManager
-      )
-    val mockAssociationCallback = mock<IAssociationCallback>()
-
-    legacyConnector.startAssociation(mockAssociationCallback)
-
-    verify(mockAssociatedDeviceManager).startAssociation(mockAssociationCallback)
-  }
-
-  @Test
   fun startAssociationWithId_startsAssociationWithIdentifierOnFeatureCoordinator() {
     val mockAssociationCallback = mock<IAssociationCallback>()
     val id = ParcelUuid(UUID.randomUUID())
@@ -1920,21 +1587,6 @@
   }
 
   @Test
-  fun startAssociationWithId_doesNotThrowWhenLegacy() {
-    val mockAssociationCallback = mock<IAssociationCallback>()
-    val id = ParcelUuid(UUID.randomUUID())
-    val legacyConnector =
-      CompanionConnector.createLocalConnector(
-        context,
-        USER_TYPE_DRIVER,
-        mockConnectedDeviceManager,
-        mockAssociatedDeviceManager
-      )
-
-    legacyConnector.startAssociation(id, mockAssociationCallback)
-  }
-
-  @Test
   fun stopAssociation_stopsAssociationOnFeatureCoordinator() {
     defaultConnector.stopAssociation()
 
@@ -1942,21 +1594,6 @@
   }
 
   @Test
-  fun stopAssociation_stopsAssociationOnAssociatedDeviceManager() {
-    val legacyConnector =
-      CompanionConnector.createLocalConnector(
-        context,
-        USER_TYPE_DRIVER,
-        mockConnectedDeviceManager,
-        mockAssociatedDeviceManager
-      )
-
-    legacyConnector.stopAssociation()
-
-    verify(mockAssociatedDeviceManager).stopAssociation()
-  }
-
-  @Test
   fun acceptVerification_acceptsVerificationOnFeatureCoordinator() {
     defaultConnector.acceptVerification()
 
@@ -1964,21 +1601,6 @@
   }
 
   @Test
-  fun acceptVerification_acceptsVerificationOnAssociatedDeviceManager() {
-    val legacyConnector =
-      CompanionConnector.createLocalConnector(
-        context,
-        USER_TYPE_DRIVER,
-        mockConnectedDeviceManager,
-        mockAssociatedDeviceManager
-      )
-
-    legacyConnector.acceptVerification()
-
-    verify(mockAssociatedDeviceManager).acceptVerification()
-  }
-
-  @Test
   fun removeAssociatedDevice_removesAssociatedDeviceOnFeatureCoordinator() {
     val deviceId = UUID.randomUUID().toString()
 
@@ -1988,22 +1610,6 @@
   }
 
   @Test
-  fun removeAssociatedDevice_removesAssociatedDeviceOnAssociatedDeviceManager() {
-    val deviceId = UUID.randomUUID().toString()
-    val legacyConnector =
-      CompanionConnector.createLocalConnector(
-        context,
-        USER_TYPE_DRIVER,
-        mockConnectedDeviceManager,
-        mockAssociatedDeviceManager
-      )
-
-    legacyConnector.removeAssociatedDevice(deviceId)
-
-    verify(mockAssociatedDeviceManager).removeAssociatedDevice(deviceId)
-  }
-
-  @Test
   fun enableAssociatedDeviceConnection_enablesAssociatedDeviceConnectionOnFeatureCoordinator() {
     val deviceId = UUID.randomUUID().toString()
 
@@ -2013,22 +1619,6 @@
   }
 
   @Test
-  fun enableAssociatedDeviceConnection_enablesDeviceConnectionOnAssociatedDeviceManager() {
-    val deviceId = UUID.randomUUID().toString()
-    val legacyConnector =
-      CompanionConnector.createLocalConnector(
-        context,
-        USER_TYPE_DRIVER,
-        mockConnectedDeviceManager,
-        mockAssociatedDeviceManager
-      )
-
-    legacyConnector.enableAssociatedDeviceConnection(deviceId)
-
-    verify(mockAssociatedDeviceManager).enableAssociatedDeviceConnection(deviceId)
-  }
-
-  @Test
   fun disableAssociatedDeviceConnection_disablesAssociatedDeviceConnectionOnFeatureCoordinator() {
     val deviceId = UUID.randomUUID().toString()
 
@@ -2038,22 +1628,6 @@
   }
 
   @Test
-  fun disableAssociatedDeviceConnection_disablesOnAssociatedDeviceManager() {
-    val deviceId = UUID.randomUUID().toString()
-    val legacyConnector =
-      CompanionConnector.createLocalConnector(
-        context,
-        USER_TYPE_DRIVER,
-        mockConnectedDeviceManager,
-        mockAssociatedDeviceManager
-      )
-
-    legacyConnector.disableAssociatedDeviceConnection(deviceId)
-
-    verify(mockAssociatedDeviceManager).disableAssociatedDeviceConnection(deviceId)
-  }
-
-  @Test
   fun retrieveAssociatedDevices_retrievesAssociatedDevicesFromFeatureCoordinator() {
     val mockListener = mock<IOnAssociatedDevicesRetrievedListener>()
 
@@ -2081,80 +1655,32 @@
   }
 
   @Test
-  fun retrieveAssociatedDevices_retrievesActiveUserDevicesOnAssociatedDeviceManager() {
-    val mockListener = mock<IOnAssociatedDevicesRetrievedListener>()
-    val legacyConnector =
-      CompanionConnector.createLocalConnector(
-        context,
-        USER_TYPE_DRIVER,
-        mockConnectedDeviceManager,
-        mockAssociatedDeviceManager
-      )
+  fun claimAssociatedDevice_claimsDevice() {
+    val deviceId = UUID.randomUUID().toString()
 
-    legacyConnector.retrieveAssociatedDevices(mockListener)
+    defaultConnector.claimAssociatedDevice(deviceId)
 
-    verify(mockAssociatedDeviceManager).retrievedActiveUserAssociatedDevices(mockListener)
+    verify(mockFeatureCoordinator).claimAssociatedDevice(deviceId)
   }
 
   @Test
-  fun retrieveAssociatedDevicesForDriver_retrievesActiveUserDevicesOnAssociatedDeviceManager() {
-    val mockListener = mock<IOnAssociatedDevicesRetrievedListener>()
-    val legacyConnector =
-      CompanionConnector.createLocalConnector(
-        context,
-        USER_TYPE_DRIVER,
-        mockConnectedDeviceManager,
-        mockAssociatedDeviceManager
-      )
+  fun removeAssociatedDeviceClaim_removesDeviceClaim() {
+    val deviceId = UUID.randomUUID().toString()
 
-    legacyConnector.retrieveAssociatedDevicesForDriver(mockListener)
+    defaultConnector.removeAssociatedDeviceClaim(deviceId)
 
-    verify(mockAssociatedDeviceManager).retrievedActiveUserAssociatedDevices(mockListener)
+    verify(mockFeatureCoordinator).removeAssociatedDeviceClaim(deviceId)
   }
 
-  @Test
-  fun retrieveAssociatedDevicesForPassengers_issuesCallbackWithEmptyListWhenLegacy() {
-    val mockListener = mock<IOnAssociatedDevicesRetrievedListener>()
-    val legacyConnector =
-      CompanionConnector.createLocalConnector(
-        context,
-        USER_TYPE_DRIVER,
-        mockConnectedDeviceManager,
-        mockAssociatedDeviceManager
-      )
-
-    legacyConnector.retrieveAssociatedDevicesForPassengers(mockListener)
-
-    argumentCaptor<List<AssociatedDevice>> {
-      verify(mockListener).onAssociatedDevicesRetrieved(capture())
-      assertThat(firstValue).isEmpty()
-    }
+  private fun setQueryIntentServicesAnswer(answer: Answer<List<ResolveInfo>>) {
+    whenever(mockPackageManager.queryIntentServices(any(), any<Int>())).thenAnswer(answer)
   }
 
-  /** All companion actions are resolved. */
   private val defaultServiceAnswer = Answer {
     val intent = it.arguments.first() as Intent
     val resolveInfo =
       ResolveInfo().apply { serviceInfo = ServiceInfo().apply { packageName = PACKAGE_NAME } }
     when (intent.action) {
-      ACTION_BIND_FEATURE_COORDINATOR_FG, ACTION_BIND_REMOTE_FEATURE_FG -> {
-        resolveInfo.serviceInfo.name = FG_NAME
-        listOf(resolveInfo)
-      }
-      ACTION_BIND_FEATURE_COORDINATOR, ACTION_BIND_REMOTE_FEATURE -> {
-        resolveInfo.serviceInfo.name = BG_NAME
-        listOf(resolveInfo)
-      }
-      else -> listOf()
-    }
-  }
-
-  /** Only the feature coordinator actions are resolved. */
-  private val featureCoordinatorOnlyAnswer = Answer {
-    val intent = it.arguments.first() as Intent
-    val resolveInfo =
-      ResolveInfo().apply { serviceInfo = ServiceInfo().apply { packageName = PACKAGE_NAME } }
-    when (intent.action) {
       ACTION_BIND_FEATURE_COORDINATOR_FG -> {
         resolveInfo.serviceInfo.name = FG_NAME
         listOf(resolveInfo)
@@ -2167,24 +1693,6 @@
     }
   }
 
-  /** Only the connected device manager actions are resolved. */
-  private val connectedDeviceManagerOnlyAnswer = Answer {
-    val intent = it.arguments.first() as Intent
-    val resolveInfo =
-      ResolveInfo().apply { serviceInfo = ServiceInfo().apply { packageName = PACKAGE_NAME } }
-    when (intent.action) {
-      ACTION_BIND_REMOTE_FEATURE_FG -> {
-        resolveInfo.serviceInfo.name = FG_NAME
-        listOf(resolveInfo)
-      }
-      ACTION_BIND_REMOTE_FEATURE -> {
-        resolveInfo.serviceInfo.name = BG_NAME
-        listOf(resolveInfo)
-      }
-      else -> listOf()
-    }
-  }
-
   /** No companion actions are resolved. */
   private val notFoundAnswer = Answer<List<ResolveInfo>> { listOf() }
 
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/FakeConnector.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/FakeConnector.kt
index 6c4dfcb..18b13b1 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/FakeConnector.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/FakeConnector.kt
@@ -95,6 +95,18 @@
     listener.onAssociatedDevicesRetrieved(associatedDevices.filter { it.userId != currentUserId })
   }
 
+  override fun claimAssociatedDevice(deviceId: String) {
+    val device = associatedDevices.firstOrNull { it.deviceId == deviceId } ?: return
+    associatedDevices.remove(device)
+    associatedDevices.add(device.cloneWith(currentUserId))
+  }
+
+  override fun removeAssociatedDeviceClaim(deviceId: String) {
+    val device = associatedDevices.firstOrNull { it.deviceId == deviceId } ?: return
+    associatedDevices.remove(device)
+    associatedDevices.add(device.cloneWith(AssociatedDevice.UNCLAIMED_USER_ID))
+  }
+
   fun addAssociatedDevice(device: AssociatedDevice) {
     associatedDevices.add(device)
     callback?.onAssociatedDeviceAdded(device)
@@ -105,4 +117,7 @@
       callback?.onAssociatedDeviceRemoved(device)
     }
   }
+
+  private fun AssociatedDevice.cloneWith(userId: Int): AssociatedDevice =
+    AssociatedDevice(deviceId, deviceAddress, deviceName, isConnectionEnabled, userId)
 }
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/AttendeeContentDelegateTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/AttendeeContentDelegateTest.java
index 4222d25..77b34d2 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/AttendeeContentDelegateTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/AttendeeContentDelegateTest.java
@@ -40,7 +40,7 @@
           .put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED)
           .put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED)
           .put(Attendees.ATTENDEE_NAME, NAME)
-          .build();
+          .buildOrThrow();
 
   @Before
   public void setUp() {
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/CalendarContentDelegateTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/CalendarContentDelegateTest.java
index 9077520..290aac9 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/CalendarContentDelegateTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/CalendarContentDelegateTest.java
@@ -48,7 +48,7 @@
           .put(Calendars.CALENDAR_DISPLAY_NAME, NAME)
           .put(Calendars.OWNER_ACCOUNT, ACCOUNT)
           .put(Calendars.CALENDAR_TIME_ZONE, ZONE_ID.getId())
-          .build();
+          .buildOrThrow();
 
   private TestCalendarProvider testCalendarProvider;
 
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/EventContentDelegateTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/EventContentDelegateTest.java
index 0ccba8e..d512e4c 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/EventContentDelegateTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/EventContentDelegateTest.java
@@ -78,7 +78,7 @@
           .put(Instances.RDATE, NULL_VALUE)
           .put(Instances.ORIGINAL_ID, NULL_VALUE)
           .put(Instances.ORIGINAL_INSTANCE_TIME, NULL_VALUE)
-          .build();
+          .buildOrThrow();
 
   private TestCalendarProvider testCalendarProvider;
 
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/AssociationSecureChannelTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/AssociationSecureChannelTest.java
deleted file mode 100644
index 8bb32e4..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/AssociationSecureChannelTest.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyString;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.android.encryptionrunner.EncryptionRunner;
-import com.google.android.encryptionrunner.EncryptionRunnerFactory;
-import com.google.android.encryptionrunner.FakeEncryptionRunner;
-import java.util.UUID;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-public final class AssociationSecureChannelTest {
-  private static final UUID CLIENT_DEVICE_ID =
-      UUID.fromString("a5645523-3280-410a-90c1-582a6c6f4969");
-  private static final UUID SERVER_DEVICE_ID =
-      UUID.fromString("a29f0c74-2014-4b14-ac02-be6ed15b545a");
-  private static final byte[] CLIENT_SECRET = ByteUtils.randomBytes(32);
-
-  @Mock private ConnectedDeviceStorage storageMock;
-
-  @Mock
-  private AssociationSecureChannel.ShowVerificationCodeListener showVerificationCodeListenerMock;
-
-  @Mock private SecureChannel.Callback channelCallback;
-
-  @Mock private DeviceMessageStream streamMock;
-  private AssociationSecureChannel associationSecureChannel;
-
-  @Before
-  public void setUp() {
-    MockitoAnnotations.initMocks(this);
-    when(storageMock.getUniqueId()).thenReturn(SERVER_DEVICE_ID);
-  }
-
-  @Test
-  public void testEncryptionHandshake_association() {
-    setupAssociationSecureChannel(channelCallback, EncryptionRunnerFactory.newFakeRunner());
-    ArgumentCaptor<String> deviceIdCaptor = ArgumentCaptor.forClass(String.class);
-    ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
-
-    initHandshakeMessage();
-    verify(streamMock).writeMessage(messageCaptor.capture());
-    byte[] response = messageCaptor.getValue().getMessage();
-    assertThat(response).isEqualTo(FakeEncryptionRunner.INIT_RESPONSE);
-
-    respondToContinueMessage();
-    verify(showVerificationCodeListenerMock).showVerificationCode(anyString());
-
-    associationSecureChannel.notifyOutOfBandAccepted();
-    sendDeviceId();
-
-    verify(channelCallback).onDeviceIdReceived(deviceIdCaptor.capture());
-    verify(streamMock, times(2)).writeMessage(messageCaptor.capture());
-    DeviceMessage deviceIdMessage = messageCaptor.getValue();
-    associationSecureChannel.processMessage(deviceIdMessage);
-    assertThat(deviceIdMessage.getMessage()).isEqualTo(ByteUtils.uuidToBytes(SERVER_DEVICE_ID));
-    assertThat(deviceIdCaptor.getValue()).isEqualTo(CLIENT_DEVICE_ID.toString());
-    verify(storageMock).saveEncryptionKey(eq(CLIENT_DEVICE_ID.toString()), any());
-    verify(storageMock).saveChallengeSecret(CLIENT_DEVICE_ID.toString(), CLIENT_SECRET);
-    verify(channelCallback).onSecureChannelEstablished();
-  }
-
-  @Test
-  public void testEncryptionHandshake_association_wrongInitHandshakeMessage() {
-    setupAssociationSecureChannel(channelCallback, EncryptionRunnerFactory.newFakeRunner());
-
-    // Wrong init handshake message
-    respondToContinueMessage();
-
-    verify(channelCallback)
-        .onEstablishSecureChannelFailure(eq(SecureChannel.CHANNEL_ERROR_INVALID_HANDSHAKE));
-  }
-
-  @Test
-  public void testEncryptionHandshake_association_wrongRespondToContinueMessage()
-      throws InterruptedException {
-    setupAssociationSecureChannel(channelCallback, EncryptionRunnerFactory.newFakeRunner());
-
-    initHandshakeMessage();
-
-    // Wrong respond to continue message
-    initHandshakeMessage();
-
-    verify(channelCallback)
-        .onEstablishSecureChannelFailure(eq(SecureChannel.CHANNEL_ERROR_INVALID_HANDSHAKE));
-  }
-
-  private void setupAssociationSecureChannel(
-      SecureChannel.Callback callback, EncryptionRunner encryptionRunner) {
-    associationSecureChannel =
-        new AssociationSecureChannel(streamMock, storageMock, encryptionRunner);
-    associationSecureChannel.registerCallback(callback);
-    associationSecureChannel.setShowVerificationCodeListener(showVerificationCodeListenerMock);
-  }
-
-  private void sendDeviceId() {
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            /* recipient= */ null,
-            /* isMessageEncrypted= */ true,
-            DeviceMessage.OperationType.ENCRYPTION_HANDSHAKE,
-            FakeEncryptionRunner.encryptDataWithFakeKey(
-                ByteUtils.concatByteArrays(
-                    ByteUtils.uuidToBytes(CLIENT_DEVICE_ID), CLIENT_SECRET)));
-    associationSecureChannel.onMessageReceived(message);
-  }
-
-  private void initHandshakeMessage() {
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            /* recipient= */ null,
-            /* isMessageEncrypted= */ false,
-            DeviceMessage.OperationType.ENCRYPTION_HANDSHAKE,
-            FakeEncryptionRunner.INIT_MESSAGE);
-    associationSecureChannel.onMessageReceived(message);
-  }
-
-  private void respondToContinueMessage() {
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            /* recipient= */ null,
-            /* isMessageEncrypted= */ false,
-            DeviceMessage.OperationType.ENCRYPTION_HANDSHAKE,
-            FakeEncryptionRunner.CLIENT_RESPONSE);
-    associationSecureChannel.onMessageReceived(message);
-  }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ChannelResolverTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ChannelResolverTest.kt
index 9228f86..11a20e3 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ChannelResolverTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ChannelResolverTest.kt
@@ -16,11 +16,11 @@
 package com.google.android.connecteddevice.connection
 
 import android.content.Context
+import android.os.ParcelUuid
 import android.util.Base64
 import androidx.room.Room
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange
 import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange.OobChannelType
 import com.google.android.companionprotos.VersionExchangeProto
 import com.google.android.connecteddevice.model.DeviceMessage
@@ -29,30 +29,26 @@
 import com.google.android.connecteddevice.storage.ConnectedDeviceDatabase
 import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
 import com.google.android.connecteddevice.storage.CryptoHelper
+import com.google.android.connecteddevice.transport.ConnectChallenge
 import com.google.android.connecteddevice.transport.ConnectionProtocol
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DataReceivedListener
+import com.google.android.connecteddevice.transport.IDataReceivedListener
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
 import com.google.android.connecteddevice.transport.ProtocolDevice
-import com.google.android.connecteddevice.util.ThreadSafeCallbacks
 import com.google.android.encryptionrunner.FakeEncryptionRunner
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.MoreExecutors.directExecutor
-import com.google.protobuf.ExtensionRegistryLite
-import com.google.protobuf.InvalidProtocolBufferException
 import com.nhaarman.mockitokotlin2.any
 import com.nhaarman.mockitokotlin2.anyOrNull
 import com.nhaarman.mockitokotlin2.argumentCaptor
 import com.nhaarman.mockitokotlin2.doReturn
 import com.nhaarman.mockitokotlin2.eq
 import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.never
 import com.nhaarman.mockitokotlin2.spy
-import com.nhaarman.mockitokotlin2.times
 import com.nhaarman.mockitokotlin2.validateMockitoUsage
 import com.nhaarman.mockitokotlin2.verify
-import com.nhaarman.mockitokotlin2.verifyZeroInteractions
 import com.nhaarman.mockitokotlin2.whenever
 import java.util.UUID
-import kotlin.test.fail
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
@@ -60,7 +56,6 @@
 
 private const val TEST_PROTOCOL_ID_1 = "testDevice1"
 private const val TEST_PROTOCOL_ID_2 = "testDevice2"
-private const val OOB_CALLBACK_VALID_VERSION = 3
 private val TEST_DEVICE_ID = UUID.randomUUID()
 private val TEST_CHALLENGE = "test Challenge".toByteArray()
 private val TEST_CHALLENGE_RESPONSE = "test Challenge response".toByteArray()
@@ -72,13 +67,12 @@
   private val context = ApplicationProvider.getApplicationContext<Context>()
   private val testProtocol1: TestProtocol = spy(TestProtocol(needDeviceVerification = false))
   private val testProtocol2: TestProtocol = spy(TestProtocol(needDeviceVerification = true))
-  private val mockCallback: ChannelResolver.Callback = mock()
-  private val mockStreamFactory: ProtocolStreamFactory = mock()
-  private val mockStream: ProtocolStream = mock()
-  private val mockOobRunner: OobRunner = mock {
-    on { supportedTypes } doReturn SUPPORTED_OOB_CAPABILITIES
-  }
-  private var mockEncryptionRunner: FakeEncryptionRunner = mock()
+  private val mockCallback = mock<ChannelResolver.Callback>()
+  private val mockStreamFactory = mock<ProtocolStreamFactory>()
+  private val mockStream = mock<ProtocolStream>()
+  private val mockOobRunner =
+    mock<OobRunner> { on { supportedTypes } doReturn SUPPORTED_OOB_CAPABILITIES }
+  private var mockEncryptionRunner = mock<FakeEncryptionRunner>()
   private val testDevice1 = ProtocolDevice(testProtocol1, TEST_PROTOCOL_ID_1)
   private val testDevice2 = ProtocolDevice(testProtocol2, TEST_PROTOCOL_ID_2)
   private lateinit var spyStorage: ConnectedDeviceStorage
@@ -95,7 +89,7 @@
     val database = connectedDeviceDatabase.associatedDeviceDao()
     spyStorage =
       spy(ConnectedDeviceStorage(context, Base64CryptoHelper(), database, directExecutor()))
-    whenever(mockStreamFactory.createProtocolStream(any(), any())).thenReturn(mockStream)
+    whenever(mockStreamFactory.createProtocolStream(any())).thenReturn(mockStream)
     whenever(spyStorage.hashWithChallengeSecret(any(), any())).thenReturn(TEST_CHALLENGE_RESPONSE)
     channelResolver =
       ChannelResolver(
@@ -144,18 +138,18 @@
         .build()
         .toByteArray()
     channelResolver.resolveAssociation(mockOobRunner)
-    argumentCaptor<DataReceivedListener>().apply {
-      verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
+    argumentCaptor<IDataReceivedListener>().apply {
+      verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture())
       firstValue.onDataReceived(TEST_PROTOCOL_ID_1, unsupportedVersion)
     }
     verify(mockCallback).onChannelResolutionError()
   }
 
   @Test
-  fun receivedSupportedVersion_sendVersionMessage() {
+  fun receivedSupportedVersion_sendVersionMessageAndSendOobData() {
     channelResolver.resolveAssociation(mockOobRunner)
-    argumentCaptor<DataReceivedListener>().apply {
-      verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
+    argumentCaptor<IDataReceivedListener>().apply {
+      verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture())
       firstValue.onDataReceived(TEST_PROTOCOL_ID_1, createVersionMessage())
     }
     val expectedVersion = createVersionMessage()
@@ -163,72 +157,22 @@
       verify(testProtocol1).sendData(eq(TEST_PROTOCOL_ID_1), capture(), anyOrNull())
       assertThat(firstValue).isEqualTo(expectedVersion)
     }
+
+    verify(mockOobRunner).sendOobData(any())
   }
-
-  @Test
-  fun receivedCapabilityExchangeNotSupportedVersion_invokeOnChannelResolved() {
-    val version =
-      VersionExchangeProto.VersionExchange.newBuilder()
-        .setMaxSupportedMessagingVersion(ChannelResolver.MAX_MESSAGING_VERSION)
-        .setMinSupportedMessagingVersion(ChannelResolver.MIN_MESSAGING_VERSION)
-        .setMaxSupportedSecurityVersion(
-          ChannelResolver.SECURITY_VERSION_FOR_CAPABILITIES_EXCHANGE - 1
-        )
-        .setMinSupportedSecurityVersion(ChannelResolver.MIN_SECURITY_VERSION)
-        .build()
-        .toByteArray()
-    channelResolver.resolveAssociation(mockOobRunner)
-
-    argumentCaptor<DataReceivedListener>().apply {
-      verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
-      firstValue.onDataReceived(TEST_PROTOCOL_ID_1, version)
-    }
-    verify(mockCallback).onChannelResolved(any())
-  }
-
-  @Test
-  fun receivedCapabilityExchangeSupportedVersion_sendCapabilityMessage() {
-    val supportedOobChannelTypes = listOf(BT_RFCOMM_CHANNEL)
-    channelResolver.resolveAssociation(mockOobRunner)
-    val deviceCallback =
-      argumentCaptor<DataReceivedListener>().apply {
-        verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
-        firstValue.onDataReceived(TEST_PROTOCOL_ID_1, createVersionMessage())
-      }
-    verify(testProtocol1).sendData(eq(TEST_PROTOCOL_ID_1), any(), anyOrNull())
-
-    val capabilities =
-      CapabilitiesExchange.newBuilder()
-        .addAllSupportedOobChannels(supportedOobChannelTypes)
-        .build()
-        .toByteArray()
-    deviceCallback.firstValue.onDataReceived(TEST_PROTOCOL_ID_1, capabilities)
-    argumentCaptor<ByteArray> {
-      verify(testProtocol1, times(2)).sendData(eq(TEST_PROTOCOL_ID_1), capture(), anyOrNull())
-      val capabilityResponse =
-        try {
-          CapabilitiesExchange.parseFrom(lastValue, ExtensionRegistryLite.getEmptyRegistry())
-            .supportedOobChannelsList
-        } catch (e: InvalidProtocolBufferException) {
-          fail("Failed to parse capabilities exchange message.")
-        }
-      assertThat(capabilityResponse).isEqualTo(supportedOobChannelTypes)
-    }
-  }
-
   @Test
   fun receivedInvalidChallenge_invokeOnError() {
     channelResolver.resolveReconnect(TEST_DEVICE_ID, TEST_CHALLENGE)
     channelResolver.addProtocolDevice(testDevice2)
-    argumentCaptor<DataReceivedListener>().apply {
-      verify(testProtocol2).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_2), capture(), any())
+    argumentCaptor<IDataReceivedListener>().apply {
+      verify(testProtocol2).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_2), capture())
       firstValue.onDataReceived(TEST_PROTOCOL_ID_2, createVersionMessage())
     }
     val invalidChallengeMessage =
       DeviceMessage.createOutgoingMessage(
         /* recipient= */ null,
         /* isMessageEncrypted= */ false,
-        DeviceMessage.OperationType.ENCRYPTION_HANDSHAKE,
+        OperationType.ENCRYPTION_HANDSHAKE,
         "Invalid test Challenge".toByteArray()
       )
     mockStream.messageReceivedListener?.onMessageReceived(invalidChallengeMessage)
@@ -239,15 +183,15 @@
   fun receivedValidChallenge_sendChallengeResponse() {
     channelResolver.resolveReconnect(TEST_DEVICE_ID, TEST_CHALLENGE)
     channelResolver.addProtocolDevice(testDevice2)
-    argumentCaptor<DataReceivedListener>().apply {
-      verify(testProtocol2).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_2), capture(), any())
+    argumentCaptor<IDataReceivedListener>().apply {
+      verify(testProtocol2).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_2), capture())
       firstValue.onDataReceived(TEST_PROTOCOL_ID_2, createVersionMessage())
     }
     val validChallengeMessage =
       DeviceMessage.createOutgoingMessage(
         /* recipient= */ null,
         /* isMessageEncrypted= */ false,
-        DeviceMessage.OperationType.ENCRYPTION_HANDSHAKE,
+        OperationType.ENCRYPTION_HANDSHAKE,
         TEST_CHALLENGE
       )
     mockStream.messageReceivedListener?.onMessageReceived(validChallengeMessage)
@@ -263,8 +207,8 @@
   fun receivedValidChallenge_invokeOnChannelResolved() {
     channelResolver.resolveReconnect(TEST_DEVICE_ID, TEST_CHALLENGE)
     channelResolver.addProtocolDevice(testDevice2)
-    argumentCaptor<DataReceivedListener>().apply {
-      verify(testProtocol2).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_2), capture(), any())
+    argumentCaptor<IDataReceivedListener>().apply {
+      verify(testProtocol2).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_2), capture())
       firstValue.onDataReceived(TEST_PROTOCOL_ID_2, createVersionMessage())
     }
     val validChallengeMessage =
@@ -281,126 +225,13 @@
   @Test
   fun challengeNotRequired_invokeOnChannelResolved() {
     channelResolver.resolveReconnect(TEST_DEVICE_ID, TEST_CHALLENGE)
-    argumentCaptor<DataReceivedListener>().apply {
-      verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
+    argumentCaptor<IDataReceivedListener>().apply {
+      verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture())
       firstValue.onDataReceived(TEST_PROTOCOL_ID_1, createVersionMessage())
     }
     verify(mockCallback).onChannelResolved(any())
   }
 
-  @Test
-  fun onOobExchangeSuccess_V3_updateEncryptionRunnerAndResolveStream() {
-    val testProtocolDevice = ProtocolDevice(testProtocol1, TEST_PROTOCOL_ID_1)
-    val testOobChannels = listOf(BT_RFCOMM_CHANNEL)
-    whenever(mockOobRunner.startOobDataExchange(eq(testProtocolDevice), any(), any(), any()))
-      .thenReturn(true)
-    channelResolver.resolveAssociation(mockOobRunner)
-    val deviceCallback =
-      argumentCaptor<DataReceivedListener>().apply {
-        verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
-        firstValue.onDataReceived(
-          TEST_PROTOCOL_ID_1,
-          createVersionMessage(OOB_CALLBACK_VALID_VERSION)
-        )
-      }
-    val capabilities =
-      CapabilitiesExchange.newBuilder()
-        .addAllSupportedOobChannels(testOobChannels)
-        .build()
-        .toByteArray()
-    deviceCallback.firstValue.onDataReceived(TEST_PROTOCOL_ID_1, capabilities)
-    argumentCaptor<OobRunner.Callback>().apply {
-      verify(mockOobRunner).startOobDataExchange(eq(testProtocolDevice), any(), any(), capture())
-      firstValue.onOobDataExchangeSuccess()
-    }
-
-    verifyZeroInteractions(mockEncryptionRunner)
-    verify(mockStreamFactory).createProtocolStream(eq(testProtocolDevice), any())
-  }
-
-  @Test
-  fun onOobExchangeFailure_directlyResolveStream() {
-    val testProtocolDevice = ProtocolDevice(testProtocol1, TEST_PROTOCOL_ID_1)
-    val testOobChannels = listOf(BT_RFCOMM_CHANNEL)
-    whenever(mockOobRunner.startOobDataExchange(eq(testProtocolDevice), any(), any(), any()))
-      .thenReturn(true)
-    channelResolver.resolveAssociation(mockOobRunner)
-    val deviceCallback =
-      argumentCaptor<DataReceivedListener>().apply {
-        verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
-        firstValue.onDataReceived(
-          TEST_PROTOCOL_ID_1,
-          createVersionMessage(OOB_CALLBACK_VALID_VERSION)
-        )
-      }
-    val capabilities =
-      CapabilitiesExchange.newBuilder()
-        .addAllSupportedOobChannels(testOobChannels)
-        .build()
-        .toByteArray()
-    deviceCallback.firstValue.onDataReceived(TEST_PROTOCOL_ID_1, capabilities)
-    argumentCaptor<OobRunner.Callback>().apply {
-      verify(mockOobRunner).startOobDataExchange(eq(testProtocolDevice), any(), any(), capture())
-      firstValue.onOobDataExchangeFailure()
-    }
-
-    verify(mockEncryptionRunner).setIsReconnect(false)
-    verify(mockStreamFactory).createProtocolStream(eq(testProtocolDevice), any())
-  }
-
-  @Test
-  fun receivedCapabilityExchangeSupportedVersion_createOobChannelSuccessfully_waitForOobCallback() {
-    val supportedOobChannelTypes = listOf(BT_RFCOMM_CHANNEL)
-    val testProtocolDevice = ProtocolDevice(testProtocol1, TEST_PROTOCOL_ID_1)
-    whenever(mockOobRunner.startOobDataExchange(eq(testProtocolDevice), any(), any(), any()))
-      .thenReturn(true)
-    channelResolver.resolveAssociation(mockOobRunner)
-    val deviceCallback =
-      argumentCaptor<DataReceivedListener>().apply {
-        verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
-        firstValue.onDataReceived(
-          TEST_PROTOCOL_ID_1,
-          createVersionMessage(OOB_CALLBACK_VALID_VERSION)
-        )
-      }
-    verify(testProtocol1).sendData(eq(TEST_PROTOCOL_ID_1), any(), anyOrNull())
-
-    val capabilities =
-      CapabilitiesExchange.newBuilder()
-        .addAllSupportedOobChannels(supportedOobChannelTypes)
-        .build()
-        .toByteArray()
-    deviceCallback.firstValue.onDataReceived(TEST_PROTOCOL_ID_1, capabilities)
-
-    verify(mockStreamFactory, never()).createProtocolStream(any(), any())
-  }
-
-  @Test
-  fun receivedCapabilityExchangeSupportedVersion_createOobChannelFailed_directlyResolveStream() {
-    val supportedOobChannelTypes = listOf(BT_RFCOMM_CHANNEL)
-    val testProtocolDevice = ProtocolDevice(testProtocol1, TEST_PROTOCOL_ID_1)
-    channelResolver.resolveAssociation(mockOobRunner)
-    val deviceCallback =
-      argumentCaptor<DataReceivedListener>().apply {
-        verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
-        firstValue.onDataReceived(
-          TEST_PROTOCOL_ID_1,
-          createVersionMessage(OOB_CALLBACK_VALID_VERSION)
-        )
-      }
-    verify(testProtocol1).sendData(eq(TEST_PROTOCOL_ID_1), any(), anyOrNull())
-
-    val capabilities =
-      CapabilitiesExchange.newBuilder()
-        .addAllSupportedOobChannels(supportedOobChannelTypes)
-        .build()
-        .toByteArray()
-    deviceCallback.firstValue.onDataReceived(TEST_PROTOCOL_ID_1, capabilities)
-
-    verify(mockStreamFactory).createProtocolStream(eq(testProtocolDevice), any())
-    verify(mockEncryptionRunner).setIsReconnect(false)
-  }
-
   private fun createVersionMessage(
     maxSecurityVersion: Int = ChannelResolver.MAX_SECURITY_VERSION
   ): ByteArray {
@@ -420,28 +251,28 @@
   }
 
   open class TestProtocol(private val needDeviceVerification: Boolean) : ConnectionProtocol() {
-    val dataReceivedListenerList: MutableMap<String, ThreadSafeCallbacks<DataReceivedListener>> =
-      dataReceivedListeners
 
-    override val isDeviceVerificationRequired: Boolean = needDeviceVerification
+    val dataReceivedListenerList = dataReceivedListeners
+
+    override fun isDeviceVerificationRequired() = needDeviceVerification
 
     override fun startAssociationDiscovery(
       name: String,
-      callback: DiscoveryCallback,
-      identifier: UUID
+      identifier: ParcelUuid,
+      callback: IDiscoveryCallback
     ) {}
 
     override fun startConnectionDiscovery(
-      id: UUID,
+      id: ParcelUuid,
       challenge: ConnectChallenge,
-      callback: DiscoveryCallback
+      callback: IDiscoveryCallback
     ) {}
 
     override fun stopAssociationDiscovery() {}
 
-    override fun stopConnectionDiscovery(id: UUID) {}
+    override fun stopConnectionDiscovery(id: ParcelUuid) {}
 
-    override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {}
+    override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {}
 
     override fun disconnectDevice(protocolId: String) {}
 
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ConnectionResolverTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ConnectionResolverTest.java
deleted file mode 100644
index 58c6121..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ConnectionResolverTest.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright (C) 2021 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 com.google.android.connecteddevice.connection;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange;
-import com.google.android.companionprotos.VersionExchangeProto.VersionExchange;
-import com.google.android.connecteddevice.connection.DeviceMessageStream.DataReceivedListener;
-import com.google.android.connecteddevice.connection.DeviceMessageStream.MessageReceivedErrorListener;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.protobuf.InvalidProtocolBufferException;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-public final class ConnectionResolverTest {
-
-  private static final int WRITE_SIZE = 500;
-
-  private DeviceMessageStream stream;
-  private ConnectionResolver connectionResolver;
-  @Mock private MessageReceivedErrorListener mockErrorListener;
-  @Mock private DataReceivedListener mockDataReceivedListener;
-  @Mock private ConnectionResolver.ConnectionResolvedListener mockConnectionResolvedListener;
-
-  @Before
-  public void setUp() {
-    MockitoAnnotations.initMocks(this);
-    stream =
-        spy(
-            new DeviceMessageStream(WRITE_SIZE) {
-              @Override
-              protected void send(byte[] data) {}
-            });
-    stream.setMessageReceivedErrorListener(mockErrorListener);
-    stream.setDataReceivedListener(mockDataReceivedListener);
-    connectionResolver = new ConnectionResolver(stream, /* isCapabilitiesEligible= */ true);
-  }
-
-  @Test
-  public void resolveConnection_invalidVersionExchangeMessage() {
-    connectionResolver.resolveConnection(mockConnectionResolvedListener);
-    connectionResolver.onMessageReceived(ByteUtils.randomBytes(WRITE_SIZE));
-    verify(mockErrorListener).onMessageReceivedError(any(InvalidProtocolBufferException.class));
-    verify(stream, times(0)).send(any());
-    verify(mockConnectionResolvedListener, times(0)).onConnectionResolved(any());
-  }
-
-  @Test
-  public void resolveConnection_invalidVersionNumbers() {
-    connectionResolver.resolveConnection(mockConnectionResolvedListener);
-    int invalidVersion = 0;
-    byte[] phoneVersionBytes =
-        VersionExchange.newBuilder()
-            .setMinSupportedMessagingVersion(invalidVersion)
-            .setMaxSupportedMessagingVersion(invalidVersion)
-            .setMinSupportedSecurityVersion(invalidVersion)
-            .setMaxSupportedSecurityVersion(invalidVersion)
-            .build()
-            .toByteArray();
-    stream.onDataReceived(phoneVersionBytes);
-    verify(mockErrorListener).onMessageReceivedError(any(IllegalStateException.class));
-    verify(stream, times(0)).send(any());
-    verify(mockConnectionResolvedListener, times(0)).onConnectionResolved(any());
-  }
-
-  @Test
-  public void resolveConnection_securityVersion2_success() {
-    connectionResolver.resolveConnection(mockConnectionResolvedListener);
-
-    byte[] phoneVersionBytes = VersionExchange.newBuilder()
-        .setMinSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
-        .setMaxSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
-        .setMinSupportedSecurityVersion(2)
-        .setMaxSupportedSecurityVersion(2)
-        .build()
-        .toByteArray();
-    byte[] expectedVersionBytes = VersionExchange.newBuilder()
-        .setMinSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
-        .setMaxSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
-        .setMinSupportedSecurityVersion(ConnectionResolver.MIN_SECURITY_VERSION)
-        .setMaxSupportedSecurityVersion(ConnectionResolver.MAX_SECURITY_VERSION)
-        .build()
-        .toByteArray();
-    connectionResolver.onMessageReceived(phoneVersionBytes);
-    verify(stream).writeRawBytes(expectedVersionBytes);
-
-    ArgumentCaptor<ConnectionResolver.ResolvedConnection> resolvedConnectionCaptor =
-        ArgumentCaptor.forClass(ConnectionResolver.ResolvedConnection.class);
-    verify(mockConnectionResolvedListener).onConnectionResolved(resolvedConnectionCaptor.capture());
-
-    ConnectionResolver.ResolvedConnection resolvedConnection = resolvedConnectionCaptor.getValue();
-    assertThat(resolvedConnection.messagingVersion())
-        .isEqualTo(ConnectionResolver.MESSAGING_VERSION);
-    assertThat(resolvedConnection.securityVersion())
-        .isEqualTo(2);
-    assertThat(resolvedConnection.oobChannelTypes()).isNull();
-  }
-
-  @Test
-  @Ignore("b/175066810")
-  public void resolveConnection_isNotCapabilitiesEligible_capabilitesAreNotExchanged() {
-    connectionResolver = new ConnectionResolver(stream, /* isCapabilitiesEligible= */ false);
-    connectionResolver.resolveConnection(mockConnectionResolvedListener);
-
-    byte[] phoneVersionBytes = createVersionExchangeMessage().toByteArray();
-    connectionResolver.onMessageReceived(phoneVersionBytes);
-    verify(stream).send(phoneVersionBytes);
-    ArgumentCaptor<ConnectionResolver.ResolvedConnection> resolvedConnectionCaptor =
-        ArgumentCaptor.forClass(ConnectionResolver.ResolvedConnection.class);
-    verify(mockConnectionResolvedListener).onConnectionResolved(resolvedConnectionCaptor.capture());
-
-    ConnectionResolver.ResolvedConnection resolvedConnection = resolvedConnectionCaptor.getValue();
-    assertThat(resolvedConnection.messagingVersion())
-        .isEqualTo(ConnectionResolver.MESSAGING_VERSION);
-    assertThat(resolvedConnection.securityVersion())
-        .isEqualTo(ConnectionResolver.MAX_SECURITY_VERSION);
-    assertThat(resolvedConnection.oobChannelTypes()).isNull();
-  }
-
-  @Test
-  @Ignore("b/175066810")
-  public void resolveConnection_securityVersion3_success() {
-    connectionResolver.resolveConnection(mockConnectionResolvedListener);
-
-    byte[] phoneVersionBytes = createVersionExchangeMessage().toByteArray();
-    connectionResolver.onMessageReceived(phoneVersionBytes);
-    verify(stream).send(phoneVersionBytes);
-
-    byte[] phoneCapabilitiesBytes = CapabilitiesExchange.getDefaultInstance().toByteArray();
-    connectionResolver.onMessageReceived(phoneCapabilitiesBytes);
-    verify(stream).send(phoneCapabilitiesBytes);
-
-    ArgumentCaptor<ConnectionResolver.ResolvedConnection> resolvedConnectionCaptor =
-        ArgumentCaptor.forClass(ConnectionResolver.ResolvedConnection.class);
-    verify(mockConnectionResolvedListener).onConnectionResolved(resolvedConnectionCaptor.capture());
-
-    ConnectionResolver.ResolvedConnection resolvedConnection = resolvedConnectionCaptor.getValue();
-    assertThat(resolvedConnection.messagingVersion())
-        .isEqualTo(ConnectionResolver.MESSAGING_VERSION);
-    assertThat(resolvedConnection.securityVersion())
-        .isEqualTo(ConnectionResolver.MAX_SECURITY_VERSION);
-    assertThat(resolvedConnection.oobChannelTypes()).isNotNull();
-  }
-
-  @Test
-  @Ignore("b/175066810")
-  public void processCapabilitiesExchange_invalidCapabilitiesExchangeMessage() {
-    connectionResolver.resolveConnection(mockConnectionResolvedListener);
-
-    byte[] phoneVersionBytes = createVersionExchangeMessage().toByteArray();
-    connectionResolver.onMessageReceived(phoneVersionBytes);
-    verify(stream).send(phoneVersionBytes);
-
-    stream.onDataReceived(ByteUtils.randomBytes(WRITE_SIZE));
-    verify(mockErrorListener).onMessageReceivedError(any(InvalidProtocolBufferException.class));
-    verifyNoMoreInteractions(stream);
-    verify(mockConnectionResolvedListener, times(0)).onConnectionResolved(any());
-  }
-
-  private static VersionExchange createVersionExchangeMessage() {
-    return VersionExchange.newBuilder()
-        .setMinSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
-        .setMaxSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
-        .setMinSupportedSecurityVersion(ConnectionResolver.MIN_SECURITY_VERSION)
-        .setMaxSupportedSecurityVersion(ConnectionResolver.MAX_SECURITY_VERSION)
-        .build();
-  }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/DeviceMessageStreamTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/DeviceMessageStreamTest.java
deleted file mode 100644
index 08d7f28..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/DeviceMessageStreamTest.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.companionprotos.DeviceMessageProto.Message;
-import com.google.android.companionprotos.OperationProto.OperationType;
-import com.google.android.companionprotos.PacketProto.Packet;
-import com.google.android.connecteddevice.connection.DeviceMessageStream.MessageReceivedErrorListener;
-import com.google.android.connecteddevice.connection.DeviceMessageStream.MessageReceivedListener;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.common.collect.Iterables;
-import com.google.protobuf.ByteString;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.ThreadLocalRandom;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-public class DeviceMessageStreamTest {
-
-  private static final int WRITE_SIZE = 500;
-
-  private DeviceMessageStream stream;
-
-  @Mock private MessageReceivedListener mockMessageReceivedListener;
-  @Mock private MessageReceivedErrorListener mockErrorListener;
-
-  @Before
-  public void setup() {
-    MockitoAnnotations.initMocks(this);
-    stream =
-        spy(
-            new DeviceMessageStream(WRITE_SIZE) {
-              @Override
-              protected void send(byte[] data) {}
-            });
-  }
-
-  @Test
-  public void processPacket_notifiesWithEntireMessageForSinglePacketMessage() {
-    stream.setMessageReceivedListener(mockMessageReceivedListener);
-    byte[] data = ByteUtils.randomBytes(5);
-
-    processMessage(data);
-    ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
-    verify(mockMessageReceivedListener).onMessageReceived(messageCaptor.capture());
-    DeviceMessage message = messageCaptor.getValue();
-
-    assertThat(message.getMessage()).isEqualTo(data);
-    assertThat(message.getOriginalMessageSize()).isEqualTo(data.length);
-  }
-
-  @Test
-  public void processPacket_notifiesWithEntireMessageForMultiPacketMessage() {
-    stream.setMessageReceivedListener(mockMessageReceivedListener);
-    byte[] data = ByteUtils.randomBytes(750);
-    processMessage(data);
-    ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
-    verify(mockMessageReceivedListener).onMessageReceived(messageCaptor.capture());
-    assertThat(Arrays.equals(data, messageCaptor.getValue().getMessage())).isTrue();
-  }
-
-  @Test
-  public void processPacket_receivingMultipleMessagesInParallelParsesSuccessfully() {
-    stream.setMessageReceivedListener(mockMessageReceivedListener);
-    byte[] data1 = ByteUtils.randomBytes((int) (WRITE_SIZE * 1.5));
-    byte[] data2 = ByteUtils.randomBytes((int) (WRITE_SIZE * 1.5));
-    List<Packet> packets1 = createPackets(data1);
-    List<Packet> packets2 = createPackets(data2);
-
-    for (int i = 0; i < packets1.size(); i++) {
-      stream.processPacket(packets1.get(i));
-      if (i == packets1.size() - 1) {
-        break;
-      }
-      stream.processPacket(packets2.get(i));
-    }
-    ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
-    verify(mockMessageReceivedListener).onMessageReceived(messageCaptor.capture());
-    DeviceMessage message1 = messageCaptor.getValue();
-
-    assertThat(message1.getMessage()).isEqualTo(data1);
-    assertThat(message1.getOriginalMessageSize()).isEqualTo(data1.length);
-
-    stream.setMessageReceivedListener(mockMessageReceivedListener);
-    stream.processPacket(Iterables.getLast(packets2));
-    verify(mockMessageReceivedListener, times(2)).onMessageReceived(messageCaptor.capture());
-    DeviceMessage message2 = messageCaptor.getValue();
-
-    assertThat(message2.getMessage()).isEqualTo(data2);
-    assertThat(message2.getOriginalMessageSize()).isEqualTo(data2.length);
-  }
-
-  @Test
-  public void processPacket_doesNotNotifyOfNewMessageIfNotAllPacketsReceived()
-      throws InterruptedException {
-    stream.setMessageReceivedListener(mockMessageReceivedListener);
-    stream.setMessageReceivedErrorListener(mockErrorListener);
-    byte[] data = ByteUtils.randomBytes((int) (WRITE_SIZE * 1.5));
-    List<Packet> packets = createPackets(data);
-    for (int i = 0; i < packets.size() - 1; i++) {
-      stream.processPacket(packets.get(i));
-    }
-    verify(mockMessageReceivedListener, never()).onMessageReceived(any());
-    verify(mockErrorListener, never()).onMessageReceivedError(any());
-  }
-
-  @Test
-  public void processPacket_ignoresDuplicatePacket() {
-    byte[] data = ByteUtils.randomBytes((int) (WRITE_SIZE * 2.5));
-    stream.setMessageReceivedListener(mockMessageReceivedListener);
-    ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(
-        DeviceMessage.class);
-    List<Packet> packets = createPackets(data);
-    for (int i = 0; i < packets.size(); i++) {
-      stream.processPacket(packets.get(i));
-      stream.processPacket(packets.get(i)); // Process each packet twice.
-    }
-    verify(mockMessageReceivedListener).onMessageReceived(messageCaptor.capture());
-    assertThat(Arrays.equals(data, messageCaptor.getValue().getMessage())).isTrue();
-  }
-
-  @Test
-  public void processPacket_packetBeforeExpectedRangeNotifiesMessageError()
-      throws InterruptedException {
-    stream.setMessageReceivedErrorListener(mockErrorListener);
-    List<Packet> packets = createPackets(ByteUtils.randomBytes((int) (WRITE_SIZE * 2.5)));
-    stream.processPacket(packets.get(0));
-    stream.processPacket(packets.get(1));
-    stream.processPacket(packets.get(0));
-    verify(mockErrorListener).onMessageReceivedError(any(IllegalStateException.class));
-  }
-
-  @Test
-  public void processPacket_packetAfterExpectedNotifiesMessageError() throws InterruptedException {
-    stream.setMessageReceivedErrorListener(mockErrorListener);
-    List<Packet> packets = createPackets(ByteUtils.randomBytes((int) (WRITE_SIZE * 1.5)));
-    stream.processPacket(packets.get(1));
-    verify(mockErrorListener).onMessageReceivedError(any(IllegalStateException.class));
-  }
-
-
-  @NonNull
-  private static List<Packet> createPackets(byte[] data) {
-    try {
-      Message message =
-          Message.newBuilder()
-              .setPayload(ByteString.copyFrom(data))
-              .setOperation(OperationType.CLIENT_MESSAGE)
-              .setOriginalSize(data.length)
-              .build();
-      return PacketFactory.makePackets(
-          message.toByteArray(), ThreadLocalRandom.current().nextInt(), WRITE_SIZE);
-    } catch (Exception e) {
-      assertWithMessage("Uncaught exception while making packets.").fail();
-      return new ArrayList<>();
-    }
-  }
-
-  private void processMessage(byte[] data) {
-    List<Packet> packets = createPackets(data);
-    for (Packet packet : packets) {
-      stream.processPacket(packet);
-    }
-  }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelTest.kt
index 3dc968a..4f1df72 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelTest.kt
@@ -16,10 +16,13 @@
 package com.google.android.connecteddevice.connection
 
 import android.content.Context
+import android.os.ParcelUuid
 import android.util.Base64
 import androidx.room.Room
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.companionprotos.VerificationCode
+import com.google.android.companionprotos.VerificationCodeState
 import com.google.android.connecteddevice.connection.MultiProtocolSecureChannel.ChannelError
 import com.google.android.connecteddevice.connection.MultiProtocolSecureChannel.MessageError
 import com.google.android.connecteddevice.connection.MultiProtocolSecureChannel.ShowVerificationCodeListener
@@ -29,7 +32,10 @@
 import com.google.android.connecteddevice.storage.ConnectedDeviceDatabase
 import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
 import com.google.android.connecteddevice.storage.CryptoHelper
+import com.google.android.connecteddevice.transport.ConnectChallenge
 import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
 import com.google.android.connecteddevice.transport.ProtocolDevice
 import com.google.android.connecteddevice.util.ByteUtils
 import com.google.android.encryptionrunner.EncryptionRunnerFactory
@@ -37,6 +43,7 @@
 import com.google.android.encryptionrunner.HandshakeException
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import com.google.protobuf.ByteString
 import com.nhaarman.mockitokotlin2.any
 import com.nhaarman.mockitokotlin2.argumentCaptor
 import com.nhaarman.mockitokotlin2.eq
@@ -62,10 +69,8 @@
 @RunWith(AndroidJUnit4::class)
 class MultiProtocolSecureChannelTest {
   private val context = ApplicationProvider.getApplicationContext<Context>()
-  private val stream1 =
-    spy(ProtocolStream(ProtocolDevice(TestProtocol(), PROTOCOL_ID_1), directExecutor()))
-  private val stream2 =
-    spy(ProtocolStream(ProtocolDevice(TestProtocol(), PROTOCOL_ID_2), directExecutor()))
+  private val stream1 = spy(ProtocolStream(ProtocolDevice(TestProtocol(), PROTOCOL_ID_1)))
+  private val stream2 = spy(ProtocolStream(ProtocolDevice(TestProtocol(), PROTOCOL_ID_2)))
   private val mockInflater: Inflater = mock()
   private val mockCallback: MultiProtocolSecureChannel.Callback = mock()
   private val mockOobRunner: OobRunner = mock()
@@ -199,7 +204,7 @@
   }
 
   @Test
-  fun onDeviceMessageReceived_noExceptionWhenReceivedUnknowMessageType() {
+  fun onDeviceMessageReceived_noExceptionWhenReceivedUnknownMessageType() {
     completeHandshakeAndSaveTheKey()
     val message =
       DeviceMessage.createOutgoingMessage(
@@ -247,7 +252,7 @@
   }
 
   @Test
-  fun decompressMessage_returnsFlaseWhenThereAreDataFormatException() {
+  fun decompressMessage_returnsFalseWhenThereAreDataFormatException() {
     completeHandshakeAndSaveTheKey()
     whenever(mockInflater.inflate(any())).then { throw DataFormatException() }
     val deviceMessage =
@@ -407,6 +412,17 @@
       assertThat(response).isEqualTo(FakeEncryptionRunner.INIT_RESPONSE)
     }
     respondToContinueMessage()
+    val testVerificationCodeMessage =
+      VerificationCode.newBuilder().setState(VerificationCodeState.VISUAL_VERIFICATION).build()
+    val deviceMessage =
+      DeviceMessage.createOutgoingMessage(
+        /* recipient= */ null,
+        /* isMessageEncrypted= */ false,
+        OperationType.ENCRYPTION_HANDSHAKE,
+        testVerificationCodeMessage.toByteArray()
+      )
+    secureChannel.onDeviceMessageReceived(deviceMessage)
+
     verify(mockShowVerificationCodeListener).showVerificationCode(any())
 
     secureChannel.notifyVerificationCodeAccepted()
@@ -546,6 +562,18 @@
     setupSecureChannel(false)
     initHandshakeMessage()
     respondToContinueMessage()
+
+    val testVerificationCodeMessage =
+      VerificationCode.newBuilder().setState(VerificationCodeState.VISUAL_VERIFICATION).build()
+    val deviceMessage =
+      DeviceMessage.createOutgoingMessage(
+        /* recipient= */ null,
+        /* isMessageEncrypted= */ false,
+        OperationType.ENCRYPTION_HANDSHAKE,
+        testVerificationCodeMessage.toByteArray()
+      )
+    secureChannel.onDeviceMessageReceived(deviceMessage)
+
     verify(mockCallback).onEstablishSecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_STATE)
   }
 
@@ -569,76 +597,86 @@
   }
 
   @Test
-  fun receivedOobVerificationCode_verifiedSuccessfully_notifyChannelEstablishedCallback() {
-    val encryptedCode = "oobCode".toByteArray()
-    val oobCode = FakeEncryptionRunner.VERIFICATION_CODE
-    val carEncryptedCode = "carOobCode".toByteArray()
-    whenever(mockOobRunner.decryptData(encryptedCode)).thenReturn(oobCode)
-    whenever(mockOobRunner.encryptData(oobCode)).thenReturn(carEncryptedCode)
-    setupOobSecureChannel(mockOobRunner)
+  fun processVerificationCodeMessage_oobVerification_verifyOobCode() {
+    setupSecureChannel(false)
     initHandshakeMessage()
     respondToContinueMessage()
-    respondToOobCode(encryptedCode)
-    verify(mockOobRunner).encryptData(oobCode)
-    argumentCaptor<DeviceMessage>().apply {
-      verify(stream1, times(2)).sendMessage(capture())
-      val codeMessage = secondValue.message
-      assertThat(codeMessage).isEqualTo(carEncryptedCode)
-    }
-    verify(mockCallback).onSecureChannelEstablished()
+    val testPayload = "testPayload".toByteArray()
+    val testVerificationCodeMessage =
+      VerificationCode.newBuilder().run {
+        setState(VerificationCodeState.OOB_VERIFICATION)
+        setPayload(ByteString.copyFrom(testPayload))
+        build()
+      }
+    val deviceMessage =
+      DeviceMessage.createOutgoingMessage(
+        /* recipient= */ null,
+        /* isMessageEncrypted= */ false,
+        OperationType.ENCRYPTION_HANDSHAKE,
+        testVerificationCodeMessage.toByteArray()
+      )
+    secureChannel.onDeviceMessageReceived(deviceMessage)
+
+    verify(mockOobRunner).decryptData(testPayload)
   }
 
   @Test
-  fun receivedOobVerificationCode_decryptFailed_issueInvalidEncryptionKeyError() {
-    val encryptedCode = "oobCode".toByteArray()
-    whenever(mockOobRunner.decryptData(encryptedCode)).then { throw Exception() }
-    setupOobSecureChannel(mockOobRunner)
+  fun createOobResponse_oobCodeMatch_sendCorrectMessage() {
+    val testPayload = "testPayload".toByteArray()
+    whenever(mockOobRunner.decryptData(testPayload))
+      .thenReturn(FakeEncryptionRunner.VERIFICATION_CODE)
+    whenever(mockOobRunner.encryptData(FakeEncryptionRunner.VERIFICATION_CODE))
+      .thenReturn(testPayload)
+    setupSecureChannel(false)
     initHandshakeMessage()
     respondToContinueMessage()
-    respondToOobCode(encryptedCode)
+    val testVerificationCodeMessage =
+      VerificationCode.newBuilder().run {
+        setState(VerificationCodeState.OOB_VERIFICATION)
+        setPayload(ByteString.copyFrom(testPayload))
+        build()
+      }
+    val deviceMessage =
+      DeviceMessage.createOutgoingMessage(
+        /* recipient= */ null,
+        /* isMessageEncrypted= */ false,
+        OperationType.ENCRYPTION_HANDSHAKE,
+        testVerificationCodeMessage.toByteArray()
+      )
+    secureChannel.onDeviceMessageReceived(deviceMessage)
 
-    verify(mockCallback)
-      .onEstablishSecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_ENCRYPTION_KEY)
+    val confirmationMessage =
+      argumentCaptor<DeviceMessage>().run {
+        verify(stream1, times(2)).sendMessage(capture())
+        secondValue.message
+      }
+    val verificationCodeMessage = VerificationCode.parseFrom(confirmationMessage)
+    assertThat(verificationCodeMessage.state).isEqualTo(VerificationCodeState.OOB_VERIFICATION)
   }
 
   @Test
-  fun receivedOobVerificationCode_encryptFailed_issueInvalidEncryptionKeyError() {
-    val encryptedCode = "oobCode".toByteArray()
-    val incorrectOobCode = "incorrectCode".toByteArray()
-    whenever(mockOobRunner.decryptData(encryptedCode)).thenReturn(incorrectOobCode)
-    setupOobSecureChannel(mockOobRunner)
+  fun onVisualVerificationCodeConfirmed_sendConfirmationMessage() {
+    setupSecureChannel(false)
     initHandshakeMessage()
     respondToContinueMessage()
-    respondToOobCode(encryptedCode)
-
-    verify(mockCallback)
-      .onEstablishSecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_ENCRYPTION_KEY)
-  }
-
-  @Test
-  fun receivedOobVerificationCode_codeDoesNotMatch_issueInvalidEncryptionKeyError() {
-    val encryptedCode = "oobCode".toByteArray()
-    val oobCode = FakeEncryptionRunner.VERIFICATION_CODE
-    whenever(mockOobRunner.decryptData(encryptedCode)).thenReturn(oobCode)
-    whenever(mockOobRunner.encryptData(oobCode)).then { throw Exception() }
-    setupOobSecureChannel(mockOobRunner)
-    initHandshakeMessage()
-    respondToContinueMessage()
-    respondToOobCode(encryptedCode)
-
-    verify(mockCallback)
-      .onEstablishSecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_ENCRYPTION_KEY)
-  }
-
-  @Test
-  fun receivedOobVerificationCode_oobRunnerIsNull_issueInvalidEncryptionKeyError() {
-    setupOobSecureChannel()
-    initHandshakeMessage()
-    respondToContinueMessage()
-    respondToOobCode(ByteArray(10))
-
-    verify(mockCallback)
-      .onEstablishSecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_ENCRYPTION_KEY)
+    val testVerificationCodeMessage =
+      VerificationCode.newBuilder().setState(VerificationCodeState.VISUAL_VERIFICATION).build()
+    val deviceMessage =
+      DeviceMessage.createOutgoingMessage(
+        /* recipient= */ null,
+        /* isMessageEncrypted= */ false,
+        OperationType.ENCRYPTION_HANDSHAKE,
+        testVerificationCodeMessage.toByteArray()
+      )
+    secureChannel.onDeviceMessageReceived(deviceMessage)
+    secureChannel.notifyVerificationCodeAccepted()
+    val confirmationMessage =
+      argumentCaptor<DeviceMessage>().run {
+        verify(stream1, times(2)).sendMessage(capture())
+        secondValue.message
+      }
+    val verificationCodeMessage = VerificationCode.parseFrom(confirmationMessage)
+    assertThat(verificationCodeMessage.state).isEqualTo(VerificationCodeState.VISUAL_CONFIRMATION)
   }
 
   private fun setupSecureChannel(isReconnect: Boolean, deviceId: String? = null) {
@@ -646,10 +684,11 @@
     encryptionRunner.setIsReconnect(isReconnect)
     secureChannel =
       spy(
-        MultiProtocolSecureChannelPreV4(
+        MultiProtocolSecureChannel(
             stream1,
             spyStorage,
             encryptionRunner,
+            mockOobRunner,
             deviceId = deviceId,
             inflater = mockInflater
           )
@@ -657,22 +696,6 @@
       )
   }
 
-  private fun setupOobSecureChannel(oobRunner: OobRunner? = null) {
-    val oobEncryptionRunner = EncryptionRunnerFactory.newOobFakeRunner()
-    secureChannel =
-      spy(
-        MultiProtocolSecureChannelPreV4(
-            stream1,
-            spyStorage,
-            oobEncryptionRunner,
-            oobRunner,
-            deviceId = null,
-            inflater = mockInflater
-          )
-          .apply { callback = mockCallback }
-      )
-  }
-
   private fun initHandshakeMessage(message: ByteArray = FakeEncryptionRunner.INIT_MESSAGE) {
     val deviceMessage =
       DeviceMessage.createOutgoingMessage(
@@ -695,17 +718,6 @@
     secureChannel.onDeviceMessageReceived(deviceMessage)
   }
 
-  private fun respondToOobCode(encryptedOobCode: ByteArray = "testCode".toByteArray()) {
-    val message =
-      DeviceMessage.createOutgoingMessage(
-        /* recipient= */ null,
-        /* isMessageEncrypted= */ false,
-        OperationType.ENCRYPTION_HANDSHAKE,
-        encryptedOobCode
-      )
-    secureChannel.onDeviceMessageReceived(message)
-  }
-
   private fun respondToResumeMessage(message: ByteArray = "Placeholder Message".toByteArray()) {
     val deviceMessage =
       DeviceMessage.createOutgoingMessage(
@@ -727,25 +739,25 @@
 }
 
 private open class TestProtocol : ConnectionProtocol() {
-  override val isDeviceVerificationRequired = false
+  override fun isDeviceVerificationRequired() = false
 
   override fun startAssociationDiscovery(
     name: String,
-    callback: DiscoveryCallback,
-    identifier: UUID
+    identifier: ParcelUuid,
+    callback: IDiscoveryCallback
   ) {}
 
   override fun startConnectionDiscovery(
-    id: UUID,
+    id: ParcelUuid,
     challenge: ConnectChallenge,
-    callback: DiscoveryCallback
+    callback: IDiscoveryCallback
   ) {}
 
   override fun stopAssociationDiscovery() {}
 
-  override fun stopConnectionDiscovery(id: UUID) {}
+  override fun stopConnectionDiscovery(id: ParcelUuid) {}
 
-  override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {}
+  override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {}
 
   override fun disconnectDevice(protocolId: String) {}
 
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelV4Test.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelV4Test.kt
index 17927c5..f0a275a 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelV4Test.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelV4Test.kt
@@ -16,6 +16,7 @@
 package com.google.android.connecteddevice.connection
 
 import android.content.Context
+import android.os.ParcelUuid
 import android.util.Base64
 import androidx.room.Room
 import androidx.test.core.app.ApplicationProvider
@@ -29,7 +30,10 @@
 import com.google.android.connecteddevice.storage.ConnectedDeviceDatabase
 import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
 import com.google.android.connecteddevice.storage.CryptoHelper
+import com.google.android.connecteddevice.transport.ConnectChallenge
 import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
 import com.google.android.connecteddevice.transport.ProtocolDevice
 import com.google.android.encryptionrunner.EncryptionRunnerFactory
 import com.google.android.encryptionrunner.FakeEncryptionRunner
@@ -43,7 +47,6 @@
 import com.nhaarman.mockitokotlin2.times
 import com.nhaarman.mockitokotlin2.verify
 import com.nhaarman.mockitokotlin2.whenever
-import java.util.UUID
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -53,8 +56,7 @@
 @RunWith(AndroidJUnit4::class)
 class MultiProtocolSecureChannelV4Test {
   private val context = ApplicationProvider.getApplicationContext<Context>()
-  private val stream1 =
-    spy(ProtocolStream(ProtocolDevice(TestProtocol(), PROTOCOL_ID), directExecutor()))
+  private val stream1 = spy(ProtocolStream(ProtocolDevice(TestProtocolV4(), PROTOCOL_ID)))
   private lateinit var storage: ConnectedDeviceStorage
   private val mockOobRunner: OobRunner = mock()
   private val mockShowVerificationCodeListener: ShowVerificationCodeListener = mock()
@@ -68,7 +70,7 @@
         .setQueryExecutor(directExecutor())
         .build()
         .associatedDeviceDao()
-    storage = ConnectedDeviceStorage(context, Base64CryptoHelper(), database, directExecutor())
+    storage = ConnectedDeviceStorage(context, Base64CryptoHelperV4(), database, directExecutor())
   }
   @Test
   fun processVerificationCodeMessage_oobVerification_verifyOobCode() {
@@ -78,8 +80,8 @@
     val testPayload = "testPayload".toByteArray()
     val testVerificationCodeMessage =
       VerificationCode.newBuilder().run {
-        setState(VerificationCodeState.OOB_VERIFICATION)
-        setPayload(ByteString.copyFrom(testPayload))
+        state = VerificationCodeState.OOB_VERIFICATION
+        payload = ByteString.copyFrom(testPayload)
         build()
       }
     val deviceMessage =
@@ -106,8 +108,8 @@
     respondToContinueMessage(secureChannel)
     val testVerificationCodeMessage =
       VerificationCode.newBuilder().run {
-        setState(VerificationCodeState.OOB_VERIFICATION)
-        setPayload(ByteString.copyFrom(testPayload))
+        state = VerificationCodeState.OOB_VERIFICATION
+        payload = ByteString.copyFrom(testPayload)
         build()
       }
     val deviceMessage =
@@ -220,26 +222,26 @@
   }
 }
 
-private class TestProtocol : ConnectionProtocol() {
-  override val isDeviceVerificationRequired = false
+private class TestProtocolV4 : ConnectionProtocol() {
+  override fun isDeviceVerificationRequired() = false
 
   override fun startAssociationDiscovery(
     name: String,
-    callback: DiscoveryCallback,
-    identifier: UUID
+    identifier: ParcelUuid,
+    callback: IDiscoveryCallback
   ) {}
 
   override fun startConnectionDiscovery(
-    id: UUID,
+    id: ParcelUuid,
     challenge: ConnectChallenge,
-    callback: DiscoveryCallback
+    callback: IDiscoveryCallback
   ) {}
 
   override fun stopAssociationDiscovery() {}
 
-  override fun stopConnectionDiscovery(id: UUID) {}
+  override fun stopConnectionDiscovery(id: ParcelUuid) {}
 
-  override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {}
+  override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {}
 
   override fun disconnectDevice(protocolId: String) {}
 
@@ -250,7 +252,7 @@
   }
 }
 
-private class Base64CryptoHelper : CryptoHelper {
+private class Base64CryptoHelperV4 : CryptoHelper {
   override fun encrypt(value: ByteArray?): String? = Base64.encodeToString(value, Base64.DEFAULT)
 
   override fun decrypt(value: String?): ByteArray? = Base64.decode(value, Base64.DEFAULT)
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/OobAssociationSecureChannelTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/OobAssociationSecureChannelTest.java
deleted file mode 100644
index 33e385a..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/OobAssociationSecureChannelTest.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.model.DeviceMessage.OperationType;
-import com.google.android.connecteddevice.oob.OobConnectionManager;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.android.encryptionrunner.EncryptionRunnerFactory;
-import com.google.android.encryptionrunner.FakeEncryptionRunner;
-import java.util.UUID;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-public class OobAssociationSecureChannelTest {
-  private static final UUID CLIENT_DEVICE_ID =
-      UUID.fromString("a5645523-3280-410a-90c1-582a6c6f4969");
-
-  private static final UUID SERVER_DEVICE_ID =
-      UUID.fromString("a29f0c74-2014-4b14-ac02-be6ed15b545a");
-
-  private static final byte[] CLIENT_SECRET = ByteUtils.randomBytes(32);
-
-  @Mock private DeviceMessageStream streamMock;
-
-  @Mock private ConnectedDeviceStorage storageMock;
-
-  @Mock private OobConnectionManager oobConnectionManagerMock;
-
-  @Mock private SecureChannel.Callback channelCallback;
-
-  private OobAssociationSecureChannel channel;
-
-  @Before
-  public void setUp() {
-    MockitoAnnotations.initMocks(this);
-    when(storageMock.getUniqueId()).thenReturn(SERVER_DEVICE_ID);
-  }
-
-  @Test
-  public void testEncryptionHandshake_oobAssociation() throws Exception {
-    setupOobAssociationSecureChannel(channelCallback);
-    ArgumentCaptor<String> deviceIdCaptor = ArgumentCaptor.forClass(String.class);
-    ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
-
-    initHandshakeMessage();
-    verify(streamMock).writeMessage(messageCaptor.capture());
-    byte[] response = messageCaptor.getValue().getMessage();
-    assertThat(response).isEqualTo(FakeEncryptionRunner.INIT_RESPONSE);
-    reset(streamMock);
-    respondToContinueMessage();
-    respondToOobCode();
-    verify(streamMock, times(2)).writeMessage(messageCaptor.capture());
-    byte[] oobCodeResponse = messageCaptor.getAllValues().get(1).getMessage();
-    assertThat(oobCodeResponse).isEqualTo(FakeEncryptionRunner.VERIFICATION_CODE);
-    DeviceMessage severDeviceId = messageCaptor.getAllValues().get(2);
-    channel.processMessage(severDeviceId);
-    assertThat(severDeviceId.getMessage()).isEqualTo(ByteUtils.uuidToBytes(SERVER_DEVICE_ID));
-    sendDeviceId();
-    verify(channelCallback).onDeviceIdReceived(deviceIdCaptor.capture());
-    assertThat(deviceIdCaptor.getValue()).isEqualTo(CLIENT_DEVICE_ID.toString());
-    verify(storageMock).saveEncryptionKey(eq(CLIENT_DEVICE_ID.toString()), any());
-    verify(storageMock).saveChallengeSecret(CLIENT_DEVICE_ID.toString(), CLIENT_SECRET);
-
-    verify(channelCallback).onSecureChannelEstablished();
-  }
-
-  private void setupOobAssociationSecureChannel(SecureChannel.Callback callback) throws Exception {
-    channel =
-        new OobAssociationSecureChannel(
-            streamMock,
-            storageMock,
-            oobConnectionManagerMock,
-            EncryptionRunnerFactory.newOobFakeRunner());
-    channel.registerCallback(callback);
-
-    when(oobConnectionManagerMock.encryptVerificationCode(any()))
-        .thenReturn(FakeEncryptionRunner.VERIFICATION_CODE);
-    when(oobConnectionManagerMock.decryptVerificationCode(any()))
-        .thenReturn(FakeEncryptionRunner.VERIFICATION_CODE);
-  }
-
-  private void sendDeviceId() {
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            /* recipient= */ null,
-            /* isMessageEncrypted= */ true,
-            OperationType.ENCRYPTION_HANDSHAKE,
-            FakeEncryptionRunner.encryptDataWithFakeKey(
-                ByteUtils.concatByteArrays(
-                    ByteUtils.uuidToBytes(CLIENT_DEVICE_ID), CLIENT_SECRET)));
-    channel.onMessageReceived(message);
-  }
-
-  private void initHandshakeMessage() {
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            /* recipient= */ null,
-            /* isMessageEncrypted= */ false,
-            OperationType.ENCRYPTION_HANDSHAKE,
-            FakeEncryptionRunner.INIT_MESSAGE);
-    channel.onMessageReceived(message);
-  }
-
-  private void respondToContinueMessage() {
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            /* recipient= */ null,
-            /* isMessageEncrypted= */ false,
-            OperationType.ENCRYPTION_HANDSHAKE,
-            FakeEncryptionRunner.CLIENT_RESPONSE);
-    channel.onMessageReceived(message);
-  }
-
-  private void respondToOobCode() {
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            /* recipient= */ null,
-            /* isMessageEncrypted= */ false,
-            OperationType.ENCRYPTION_HANDSHAKE,
-            FakeEncryptionRunner.VERIFICATION_CODE);
-    channel.onMessageReceived(message);
-  }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ProtocolStreamTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ProtocolStreamTest.kt
index 24305c3..0312c91 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ProtocolStreamTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ProtocolStreamTest.kt
@@ -15,6 +15,7 @@
  */
 package com.google.android.connecteddevice.connection
 
+import android.os.ParcelUuid
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.android.companionprotos.DeviceMessageProto.Message
 import com.google.android.companionprotos.OperationProto.OperationType
@@ -22,7 +23,10 @@
 import com.google.android.connecteddevice.connection.ProtocolStream.MessageReceivedListener
 import com.google.android.connecteddevice.connection.ProtocolStream.ProtocolDisconnectListener
 import com.google.android.connecteddevice.model.DeviceMessage
+import com.google.android.connecteddevice.transport.ConnectChallenge
 import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
 import com.google.android.connecteddevice.transport.ProtocolDevice
 import com.google.android.connecteddevice.util.ByteUtils
 import com.google.common.truth.Truth
@@ -51,7 +55,7 @@
 
   private val protocol = spy(TestProtocol())
 
-  private val stream = ProtocolStream(ProtocolDevice(protocol, PROTOCOL_ID), directExecutor())
+  private val stream = ProtocolStream(ProtocolDevice(protocol, PROTOCOL_ID))
 
   @Test
   fun sendMessage_smallMessageSendsSinglePacket() {
@@ -133,8 +137,7 @@
   @Test
   fun protocolOnDataFailedToSend_disconnectsProtocol() {
     val failingProtocol = spy(FailingSendProtocol())
-    val failingStream =
-      ProtocolStream(ProtocolDevice(failingProtocol, PROTOCOL_ID), directExecutor())
+    val failingStream = ProtocolStream(ProtocolDevice(failingProtocol, PROTOCOL_ID))
     val recipient = UUID.randomUUID()
     val message = ByteUtils.randomBytes(MAX_WRITE_SIZE / 2)
     failingStream.sendMessage(
@@ -246,8 +249,8 @@
     }
   }
 
-  open class TestProtocol : ConnectionProtocol() {
-    override val isDeviceVerificationRequired = false
+  open class TestProtocol : ConnectionProtocol(directExecutor()) {
+    override fun isDeviceVerificationRequired() = false
 
     fun receiveData(data: ByteArray) {
       dataReceivedListeners[PROTOCOL_ID]?.invoke { it.onDataReceived(PROTOCOL_ID, data) }
@@ -255,21 +258,21 @@
 
     override fun startAssociationDiscovery(
       name: String,
-      callback: DiscoveryCallback,
-      identifier: UUID
+      identifier: ParcelUuid,
+      callback: IDiscoveryCallback
     ) {}
 
     override fun startConnectionDiscovery(
-      id: UUID,
+      id: ParcelUuid,
       challenge: ConnectChallenge,
-      callback: DiscoveryCallback
+      callback: IDiscoveryCallback
     ) {}
 
     override fun stopAssociationDiscovery() {}
 
-    override fun stopConnectionDiscovery(id: UUID) {}
+    override fun stopConnectionDiscovery(id: ParcelUuid) {}
 
-    override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {
+    override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {
       callback?.onDataSentSuccessfully()
     }
 
@@ -284,26 +287,26 @@
     }
   }
 
-  open class FailingSendProtocol : ConnectionProtocol() {
-    override val isDeviceVerificationRequired = false
+  open class FailingSendProtocol : ConnectionProtocol(directExecutor()) {
+    override fun isDeviceVerificationRequired() = false
 
     override fun startAssociationDiscovery(
       name: String,
-      callback: DiscoveryCallback,
-      identifier: UUID
+      identifier: ParcelUuid,
+      callback: IDiscoveryCallback
     ) {}
 
     override fun startConnectionDiscovery(
-      id: UUID,
+      id: ParcelUuid,
       challenge: ConnectChallenge,
-      callback: DiscoveryCallback
+      callback: IDiscoveryCallback
     ) {}
 
     override fun stopAssociationDiscovery() {}
 
-    override fun stopConnectionDiscovery(id: UUID) {}
+    override fun stopConnectionDiscovery(id: ParcelUuid) {}
 
-    override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {
+    override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {
       callback?.onDataFailedToSend()
     }
 
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/SecureChannelTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/SecureChannelTest.java
deleted file mode 100644
index 358de56..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/SecureChannelTest.java
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection;
-
-import static com.google.android.connecteddevice.connection.SecureChannel.CHANNEL_ERROR_INVALID_HANDSHAKE;
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.model.DeviceMessage.OperationType;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.android.encryptionrunner.EncryptionRunnerFactory;
-import com.google.android.encryptionrunner.HandshakeException;
-import com.google.android.encryptionrunner.Key;
-import java.security.SignatureException;
-import java.util.UUID;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-public class SecureChannelTest {
-  @Mock private DeviceMessageStream mockStream;
-  @Mock private SecureChannel.Callback mockCallback;
-
-  private Key fakeKey;
-
-  private SecureChannel secureChannel;
-
-  @Before
-  public void setUp() throws SignatureException {
-    MockitoAnnotations.initMocks(this);
-
-    fakeKey =
-        spy(
-            new Key() {
-              @Override
-              public byte[] asBytes() {
-                return new byte[0];
-              }
-
-              @Override
-              public byte[] encryptData(byte[] data) {
-                return data;
-              }
-
-              @Override
-              public byte[] decryptData(byte[] encryptedData) {
-                return encryptedData;
-              }
-
-              @Override
-              public byte[] getUniqueSession() {
-                return new byte[0];
-              }
-            });
-
-    secureChannel =
-        new SecureChannel(mockStream, EncryptionRunnerFactory.newFakeRunner()) {
-          @Override
-          void processHandshake(byte[] message) {}
-        };
-    secureChannel.setEncryptionKey(fakeKey);
-  }
-
-  @Test
-  public void processMessage_doesNothingForUnencryptedMessage() throws SignatureException {
-    byte[] payload = ByteUtils.randomBytes(10);
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            UUID.randomUUID(),
-            /* isMessageEncrypted= */ false,
-            OperationType.CLIENT_MESSAGE,
-            payload);
-    secureChannel.processMessage(message);
-    assertThat(message.getMessage()).isEqualTo(payload);
-    verify(fakeKey, never()).decryptData(any());
-  }
-
-  @Test
-  public void processMessage_decryptsEncryptedMessage() throws SignatureException {
-    byte[] payload = ByteUtils.randomBytes(10);
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            UUID.randomUUID(),
-            /* isMessageEncrypted= */ true,
-            OperationType.CLIENT_MESSAGE,
-            payload);
-    secureChannel.processMessage(message);
-    verify(fakeKey).decryptData(any());
-  }
-
-  @Test
-  public void processMessage_onMessageReceivedErrorForEncryptedMessageWithNoKey()
-      throws InterruptedException {
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            UUID.randomUUID(),
-            /* isMessageEncrypted= */ true,
-            OperationType.CLIENT_MESSAGE,
-            ByteUtils.randomBytes(10));
-
-    secureChannel.setEncryptionKey(null);
-    secureChannel.registerCallback(mockCallback);
-    secureChannel.processMessage(message);
-
-    verify(mockCallback).onMessageReceivedError(isNull());
-    assertThat(message.getMessage()).isNull();
-  }
-
-  @Test
-  public void onMessageReceived_onEstablishSecureChannelFailureBadHandshakeMessage() {
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            UUID.randomUUID(),
-            /* isMessageEncrypted= */ true,
-            OperationType.ENCRYPTION_HANDSHAKE,
-            ByteUtils.randomBytes(10));
-
-    secureChannel.setEncryptionKey(null);
-    secureChannel.registerCallback(mockCallback);
-    secureChannel.onMessageReceived(message);
-
-    verify(mockCallback).onEstablishSecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE);
-  }
-
-  @Test
-  public void onMessageReceived_onMessageReceivedNotIssuedForNullMessage() {
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            UUID.randomUUID(),
-            /* isMessageEncrypted= */ false,
-            OperationType.CLIENT_MESSAGE,
-            /* message= */ null);
-
-    secureChannel.registerCallback(mockCallback);
-    secureChannel.onMessageReceived(message);
-
-    verify(mockCallback, never()).onMessageReceived(any());
-  }
-
-  @Test
-  public void onMessageReceived_processHandshakeExceptionIssuesSecureChannelFailureCallback() {
-    SecureChannel secureChannel =
-        new SecureChannel(mockStream, EncryptionRunnerFactory.newFakeRunner()) {
-          @Override
-          void processHandshake(byte[] message) throws HandshakeException {
-            throw new HandshakeException("test");
-          }
-        };
-    secureChannel.registerCallback(mockCallback);
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            UUID.randomUUID(),
-            /* isMessageEncrypted= */ true,
-            OperationType.ENCRYPTION_HANDSHAKE,
-            ByteUtils.randomBytes(10));
-
-    secureChannel.onMessageReceived(message);
-
-    verify(mockCallback).onEstablishSecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE);
-  }
-
-  @Test
-  public void decompressMessage_returnsOriginalMessageIfOriginalSizeIsZero() {
-    byte[] message = ByteUtils.randomBytes(10);
-    DeviceMessage deviceMessage =
-        DeviceMessage.createOutgoingMessage(
-            UUID.randomUUID(),
-            /* isMessageEncrypted= */ false,
-            OperationType.CLIENT_MESSAGE,
-            message);
-    deviceMessage.setOriginalMessageSize(0);
-    assertThat(secureChannel.decompressMessage(deviceMessage)).isTrue();
-    assertThat(deviceMessage.getMessage()).isEqualTo(message);
-  }
-
-  @Test
-  public void compressMessage_returnsCompressedMessageWithOriginalSize() {
-    byte[] message = new byte[100];
-    DeviceMessage deviceMessage =
-        DeviceMessage.createOutgoingMessage(
-            UUID.randomUUID(),
-            /* isMessageEncrypted= */ false,
-            OperationType.CLIENT_MESSAGE,
-            message);
-    secureChannel.compressMessage(deviceMessage);
-    assertThat(deviceMessage.getMessage()).isNotEqualTo(message);
-    assertThat(deviceMessage.getOriginalMessageSize()).isEqualTo(message.length);
-  }
-
-  @Test
-  public void compressedMessageCanBeDecompressed() {
-    byte[] message = new byte[100];
-    DeviceMessage deviceMessage =
-        DeviceMessage.createOutgoingMessage(
-            UUID.randomUUID(),
-            /* isMessageEncrypted= */ false,
-            OperationType.CLIENT_MESSAGE,
-            message);
-    secureChannel.compressMessage(deviceMessage);
-    assertThat(secureChannel.decompressMessage(deviceMessage)).isTrue();
-    assertThat(deviceMessage.getMessage()).isEqualTo(message);
-  }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ble/CarBlePeripheralManagerTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ble/CarBlePeripheralManagerTest.java
deleted file mode 100644
index 00b9ba5..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ble/CarBlePeripheralManagerTest.java
+++ /dev/null
@@ -1,299 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection.ble;
-
-import static com.google.common.truth.Truth.assertThat;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.le.AdvertiseCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.ParcelUuid;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.companionprotos.VersionExchangeProto.VersionExchange;
-import com.google.android.connecteddevice.connection.AssociationCallback;
-import com.google.android.connecteddevice.connection.AssociationSecureChannel;
-import com.google.android.connecteddevice.connection.ConnectionResolver;
-import com.google.android.connecteddevice.connection.SecureChannel;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.OobData;
-import com.google.android.connecteddevice.model.StartAssociationResponse;
-import com.google.android.connecteddevice.oob.OobConnectionManager;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.transport.ble.BlePeripheralManager;
-import com.google.android.connecteddevice.util.ByteUtils;
-import java.time.Duration;
-import java.util.Arrays;
-import java.util.UUID;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-import org.robolectric.shadows.ShadowLooper;
-
-@RunWith(AndroidJUnit4.class)
-public class CarBlePeripheralManagerTest {
-  private static final UUID ASSOCIATION_SERVICE_UUID = UUID.randomUUID();
-  private static final UUID RECONNECT_SERVICE_UUID = UUID.randomUUID();
-  private static final UUID RECONNECT_DATA_UUID = UUID.randomUUID();
-  private static final UUID ADVERTISE_DATA_CHARACTERISTIC_UUID = UUID.randomUUID();
-  private static final UUID WRITE_UUID = UUID.randomUUID();
-  private static final UUID READ_UUID = UUID.randomUUID();
-  private static final int DEVICE_NAME_LENGTH_LIMIT = 2;
-  private static final String TEST_REMOTE_DEVICE_ADDRESS = "00:11:22:33:AA:BB";
-  private static final UUID TEST_REMOTE_DEVICE_ID = UUID.randomUUID();
-  private static final String TEST_VERIFICATION_CODE = "000000";
-  private static final String TEST_ENCRYPTED_VERIFICATION_CODE = "12345";
-  private static final Duration RECONNECT_ADVERTISEMENT_DURATION = Duration.ofMillis(2);
-  private static final int DEFAULT_MTU_SIZE = 23;
-  private static final boolean COMPRESSION_ENABLED = true;
-  private static final boolean EXCHANGE_CAPABILITIES = false;
-
-  @Rule public final MockitoRule mockito = MockitoJUnit.rule();
-
-  @Mock private BlePeripheralManager mockBlePeripheralManager;
-  @Mock private ConnectedDeviceStorage mockConnectedDeviceStorage;
-  @Mock private OobConnectionManager mockOobConnectionManager;
-  @Mock private AssociationCallback mockAssociationCallback;
-  private CarBlePeripheralManager carBlePeripheralManager;
-
-  @Before
-  public void setUp() throws Exception {
-    carBlePeripheralManager =
-        new CarBlePeripheralManager(
-            mockBlePeripheralManager,
-            mockConnectedDeviceStorage,
-            ASSOCIATION_SERVICE_UUID,
-            RECONNECT_SERVICE_UUID,
-            RECONNECT_DATA_UUID,
-            ADVERTISE_DATA_CHARACTERISTIC_UUID,
-            WRITE_UUID,
-            READ_UUID,
-            RECONNECT_ADVERTISEMENT_DURATION,
-            DEFAULT_MTU_SIZE,
-            COMPRESSION_ENABLED,
-            EXCHANGE_CAPABILITIES);
-
-    when(mockOobConnectionManager.encryptVerificationCode(TEST_VERIFICATION_CODE.getBytes(UTF_8)))
-        .thenReturn(TEST_ENCRYPTED_VERIFICATION_CODE.getBytes(UTF_8));
-    when(mockOobConnectionManager.decryptVerificationCode(
-            TEST_ENCRYPTED_VERIFICATION_CODE.getBytes(UTF_8)))
-        .thenReturn(TEST_VERIFICATION_CODE.getBytes(UTF_8));
-
-    carBlePeripheralManager.start();
-  }
-
-  @After
-  public void tearDown() {
-    if (carBlePeripheralManager != null) {
-      carBlePeripheralManager.stop();
-    }
-  }
-
-  @Test
-  public void testStartAssociationAdvertisingSuccess() {
-    byte[] testDeviceName = getNameForAssociation();
-    startAssociation(mockAssociationCallback, testDeviceName);
-    ArgumentCaptor<AdvertiseData> advertisementDataCaptor =
-        ArgumentCaptor.forClass(AdvertiseData.class);
-    ArgumentCaptor<AdvertiseData> scanResponseDataCaptor =
-        ArgumentCaptor.forClass(AdvertiseData.class);
-    verify(mockBlePeripheralManager)
-        .startAdvertising(
-            any(), advertisementDataCaptor.capture(), scanResponseDataCaptor.capture(), any());
-    AdvertiseData advertisementData = advertisementDataCaptor.getValue();
-    ParcelUuid serviceUuid = new ParcelUuid(ASSOCIATION_SERVICE_UUID);
-    assertThat(advertisementData.getServiceUuids()).contains(serviceUuid);
-    AdvertiseData scanResponseData = scanResponseDataCaptor.getValue();
-    assertThat(scanResponseData.getIncludeDeviceName()).isFalse();
-    ParcelUuid dataUuid = new ParcelUuid(RECONNECT_DATA_UUID);
-    assertThat(scanResponseData.getServiceData().get(dataUuid)).isEqualTo(testDeviceName);
-  }
-
-  @Test
-  public void testStartAssociationAdvertisingFailure() {
-    startAssociation(mockAssociationCallback, getNameForAssociation());
-    ArgumentCaptor<AdvertiseCallback> callbackCaptor =
-        ArgumentCaptor.forClass(AdvertiseCallback.class);
-    verify(mockBlePeripheralManager)
-        .startAdvertising(any(), any(), any(), callbackCaptor.capture());
-    AdvertiseCallback advertiseCallback = callbackCaptor.getValue();
-    int testErrorCode = 2;
-    advertiseCallback.onStartFailure(testErrorCode);
-    verify(mockAssociationCallback).onAssociationStartFailure();
-  }
-
-  @Test
-  public void testNotifyAssociationSuccess() {
-    byte[] testDeviceName = getNameForAssociation();
-    startAssociation(mockAssociationCallback, testDeviceName);
-    ArgumentCaptor<AdvertiseCallback> callbackCaptor =
-        ArgumentCaptor.forClass(AdvertiseCallback.class);
-    verify(mockBlePeripheralManager)
-        .startAdvertising(any(), any(), any(), callbackCaptor.capture());
-    AdvertiseCallback advertiseCallback = callbackCaptor.getValue();
-    AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
-    advertiseCallback.onStartSuccess(settings);
-    OobData emptyOobData = new OobData(new byte[0], new byte[0], new byte[0]);
-    verify(mockAssociationCallback)
-        .onAssociationStartSuccess(
-            new StartAssociationResponse(
-                emptyOobData, testDeviceName, ByteUtils.byteArrayToHexString(testDeviceName)));
-  }
-
-  @Test
-  public void testShowVerificationCode() {
-    AssociationSecureChannel channel = getChannelForAssociation(mockAssociationCallback);
-    channel.getShowVerificationCodeListener().showVerificationCode(TEST_VERIFICATION_CODE);
-    verify(mockAssociationCallback).onVerificationCodeAvailable(eq(TEST_VERIFICATION_CODE));
-  }
-
-  @Test
-  public void testAssociationSuccess() {
-    SecureChannel channel = getChannelForAssociation(mockAssociationCallback);
-    SecureChannel.Callback channelCallback = channel.getCallback();
-    assertThat(channelCallback).isNotNull();
-    channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
-    channelCallback.onSecureChannelEstablished();
-    ArgumentCaptor<AssociatedDevice> deviceCaptor = ArgumentCaptor.forClass(AssociatedDevice.class);
-    verify(mockConnectedDeviceStorage).addAssociatedDeviceForDriver(deviceCaptor.capture());
-    AssociatedDevice device = deviceCaptor.getValue();
-    assertThat(device.getDeviceId()).isEqualTo(TEST_REMOTE_DEVICE_ID.toString());
-    verify(mockAssociationCallback).onAssociationCompleted(eq(TEST_REMOTE_DEVICE_ID.toString()));
-  }
-
-  @Test
-  public void testAssociationFailure_channelError() {
-    SecureChannel channel = getChannelForAssociation(mockAssociationCallback);
-    SecureChannel.Callback channelCallback = channel.getCallback();
-    int testErrorCode = 1;
-    assertThat(channelCallback).isNotNull();
-    channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
-    channelCallback.onEstablishSecureChannelFailure(testErrorCode);
-    verify(mockAssociationCallback).onAssociationError(eq(testErrorCode));
-  }
-
-  @Test
-  public void connectToDevice_embedAdvertiseDataInCharacteristic() {
-    carBlePeripheralManager.setTimeoutHandler(new Handler(Looper.getMainLooper()));
-    when(mockConnectedDeviceStorage.hashWithChallengeSecret(any(), any()))
-        .thenReturn(ByteUtils.randomBytes(32));
-    carBlePeripheralManager.connectToDevice(UUID.randomUUID());
-
-    ArgumentCaptor<AdvertiseData> advertiseDataCaptor =
-        ArgumentCaptor.forClass(AdvertiseData.class);
-    ArgumentCaptor<BluetoothGattService> gattServiceCaptor =
-        ArgumentCaptor.forClass(BluetoothGattService.class);
-    verify(mockBlePeripheralManager)
-        .startAdvertising(gattServiceCaptor.capture(), advertiseDataCaptor.capture(), any(), any());
-    assertThat(
-            Arrays.equals(
-                gattServiceCaptor
-                    .getValue()
-                    .getCharacteristic(ADVERTISE_DATA_CHARACTERISTIC_UUID)
-                    .getValue(),
-                advertiseDataCaptor.getValue().getServiceData().values().iterator().next()))
-        .isTrue();
-  }
-
-  @Test
-  public void connectToDevice_stopsAdvertisingAfterTimeout() {
-    carBlePeripheralManager.setTimeoutHandler(new Handler(Looper.getMainLooper()));
-    when(mockConnectedDeviceStorage.hashWithChallengeSecret(any(), any()))
-        .thenReturn(ByteUtils.randomBytes(32));
-    carBlePeripheralManager.connectToDevice(UUID.randomUUID());
-    ArgumentCaptor<AdvertiseCallback> callbackCaptor =
-        ArgumentCaptor.forClass(AdvertiseCallback.class);
-    verify(mockBlePeripheralManager)
-        .startAdvertising(any(), any(), any(), callbackCaptor.capture());
-    callbackCaptor.getValue().onStartSuccess(null);
-    // Simulate the timeout.
-    ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
-    verify(mockBlePeripheralManager).stopAdvertising(any(AdvertiseCallback.class));
-  }
-
-  @Test
-  public void disconnectDevice_stopsAdvertisingForPendingReconnect() {
-    when(mockConnectedDeviceStorage.hashWithChallengeSecret(any(), any()))
-        .thenReturn(ByteUtils.randomBytes(32));
-    UUID deviceId = UUID.randomUUID();
-    carBlePeripheralManager.connectToDevice(deviceId);
-    reset(mockBlePeripheralManager);
-    carBlePeripheralManager.disconnectDevice(deviceId.toString());
-    verify(mockBlePeripheralManager).cleanup();
-  }
-
-  private AssociationSecureChannel getChannelForAssociation(AssociationCallback callback) {
-    BlePeripheralManager.Callback bleManagerCallback =
-        startAssociation(callback, getNameForAssociation());
-    BluetoothDevice bluetoothDevice =
-        ApplicationProvider.getApplicationContext()
-            .getSystemService(BluetoothManager.class)
-            .getAdapter()
-            .getRemoteDevice(TEST_REMOTE_DEVICE_ADDRESS);
-    bleManagerCallback.onRemoteDeviceConnected(bluetoothDevice);
-    ArgumentCaptor<BlePeripheralManager.OnCharacteristicWriteListener> listenerCaptor =
-        ArgumentCaptor.forClass(BlePeripheralManager.OnCharacteristicWriteListener.class);
-    verify(mockBlePeripheralManager).addOnCharacteristicWriteListener(listenerCaptor.capture());
-    // Use the MIN_SECURITY_VERSION for both to avoid further complexity introduced with
-    // the capabilities exchange that has nothing to do with the tests here. In the future this
-    // class will be deprecated in favor of a protocol agnostic version.
-    VersionExchange versionExchangeMessage =
-        VersionExchange.newBuilder()
-            .setMinSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
-            .setMaxSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
-            .setMinSupportedSecurityVersion(ConnectionResolver.MIN_SECURITY_VERSION)
-            .setMaxSupportedSecurityVersion(ConnectionResolver.MIN_SECURITY_VERSION)
-            .build();
-    listenerCaptor
-        .getValue()
-        .onCharacteristicWrite(
-            bluetoothDevice,
-            carBlePeripheralManager.readCharacteristic,
-            versionExchangeMessage.toByteArray());
-    return (AssociationSecureChannel) carBlePeripheralManager.getConnectedDeviceChannel();
-  }
-
-  private BlePeripheralManager.Callback startAssociation(
-      AssociationCallback callback, byte[] deviceName) {
-    ArgumentCaptor<BlePeripheralManager.Callback> callbackCaptor =
-        ArgumentCaptor.forClass(BlePeripheralManager.Callback.class);
-    carBlePeripheralManager.startAssociation(deviceName, callback);
-    verify(mockBlePeripheralManager).registerCallback(callbackCaptor.capture());
-    return callbackCaptor.getValue();
-  }
-
-  private static byte[] getNameForAssociation() {
-    return ByteUtils.randomBytes(DEVICE_NAME_LENGTH_LIMIT);
-  }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/spp/CarSppManagerTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/spp/CarSppManagerTest.java
deleted file mode 100644
index ed1dedf..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/spp/CarSppManagerTest.java
+++ /dev/null
@@ -1,331 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection.spp;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothManager;
-import android.os.RemoteException;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.companionprotos.VersionExchangeProto.VersionExchange;
-import com.google.android.connecteddevice.connection.AssociationCallback;
-import com.google.android.connecteddevice.connection.AssociationSecureChannel;
-import com.google.android.connecteddevice.connection.CarBluetoothManager;
-import com.google.android.connecteddevice.connection.ConnectionResolver;
-import com.google.android.connecteddevice.connection.ReconnectSecureChannel;
-import com.google.android.connecteddevice.connection.SecureChannel;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.OobData;
-import com.google.android.connecteddevice.model.StartAssociationResponse;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder;
-import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder.OnMessageReceivedListener;
-import com.google.android.connecteddevice.transport.spp.PendingConnection;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-import org.mockito.stubbing.Answer;
-
-@RunWith(AndroidJUnit4.class)
-public class CarSppManagerTest {
-  private static final String TEST_REMOTE_DEVICE_ADDRESS = "00:11:22:33:AA:BB";
-  private static final UUID TEST_REMOTE_DEVICE_ID = UUID.randomUUID();
-  private static final UUID TEST_SERVICE_UUID_1 = UUID.randomUUID();
-  private static final boolean IS_SECURE = true;
-  private static final String TEST_VERIFICATION_CODE = "000000";
-  private static final int MAX_PACKET_SIZE = 700;
-  private static final boolean COMPRESSION_ENABLED = true;
-  private static final boolean EXCHANGE_CAPABILITIES = false;
-  private static final BluetoothDevice TEST_BLUETOOTH_DEVICE =
-      ApplicationProvider.getApplicationContext()
-          .getSystemService(BluetoothManager.class)
-          .getAdapter()
-          .getRemoteDevice(TEST_REMOTE_DEVICE_ADDRESS);
-
-  @Rule public final MockitoRule mockito = MockitoJUnit.rule();
-  private final Executor callbackExecutor = directExecutor();
-
-  @Mock private CarBluetoothManager.Callback mockCallback;
-  @Mock private AssociationCallback mockAssociationCallback;
-  @Mock private ConnectedDeviceSppDelegateBinder mockSppBinder;
-  @Mock private ConnectedDeviceStorage mockStorage;
-  private CarSppManager carSppManager;
-  private ConnectionResultCaptor connectionResultCaptor;
-
-  @Before
-  public void setUp() throws Exception {
-    connectionResultCaptor = new ConnectionResultCaptor();
-    doAnswer(connectionResultCaptor).when(mockSppBinder).connectAsServer(any(), eq(true));
-    carSppManager =
-        new CarSppManager(
-            mockSppBinder,
-            mockStorage,
-            TEST_SERVICE_UUID_1,
-            MAX_PACKET_SIZE,
-            COMPRESSION_ENABLED,
-            EXCHANGE_CAPABILITIES);
-    carSppManager.registerCallback(mockCallback, callbackExecutor);
-  }
-
-  @After
-  public void tearDown() {
-    if (carSppManager != null) {
-      carSppManager.stop();
-    }
-  }
-
-  @Test
-  public void testStartAssociationSuccess() throws RemoteException {
-    carSppManager.initiateConnectionToDevice(TEST_REMOTE_DEVICE_ID);
-
-    carSppManager.startAssociation(/* nameForAssociation= */ null, mockAssociationCallback);
-
-    verify(mockSppBinder).unregisterConnectionCallback(TEST_REMOTE_DEVICE_ID);
-    verify(mockSppBinder).connectAsServer(TEST_SERVICE_UUID_1, IS_SECURE);
-    OobData emptyOobData = new OobData(new byte[0], new byte[0], new byte[0]);
-    verify(mockAssociationCallback)
-        .onAssociationStartSuccess(new StartAssociationResponse(emptyOobData, new byte[0], ""));
-  }
-
-  @Test
-  public void testStartAssociationFailure() throws RemoteException {
-    when(mockSppBinder.connectAsServer(TEST_SERVICE_UUID_1, IS_SECURE)).thenReturn(null);
-
-    carSppManager.startAssociation(/* nameForAssociation= */ null, mockAssociationCallback);
-
-    verify(mockAssociationCallback).onAssociationStartFailure();
-  }
-
-  @Test
-  public void testShowVerificationCode() {
-    AssociationSecureChannel channel = getChannelForAssociation(mockAssociationCallback);
-
-    channel.getShowVerificationCodeListener().showVerificationCode(TEST_VERIFICATION_CODE);
-
-    verify(mockAssociationCallback).onVerificationCodeAvailable(TEST_VERIFICATION_CODE);
-  }
-
-  @Test
-  public void testAssociationSuccess() {
-    SecureChannel channel = getChannelForAssociation(mockAssociationCallback);
-    SecureChannel.Callback channelCallback = channel.getCallback();
-
-    assertThat(channelCallback).isNotNull();
-
-    channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
-    channelCallback.onSecureChannelEstablished();
-
-    ArgumentCaptor<AssociatedDevice> deviceCaptor = ArgumentCaptor.forClass(AssociatedDevice.class);
-    verify(mockStorage).addAssociatedDeviceForDriver(deviceCaptor.capture());
-    AssociatedDevice device = deviceCaptor.getValue();
-
-    assertThat(device.getDeviceId()).isEqualTo(TEST_REMOTE_DEVICE_ID.toString());
-
-    verify(mockAssociationCallback).onAssociationCompleted(TEST_REMOTE_DEVICE_ID.toString());
-  }
-
-  @Test
-  public void testInitiateConnectionToDevice() throws RemoteException {
-    carSppManager.initiateConnectionToDevice(TEST_SERVICE_UUID_1);
-
-    verify(mockSppBinder).registerConnectionCallback(eq(TEST_SERVICE_UUID_1), any());
-    verify(mockSppBinder).connectAsServer(TEST_SERVICE_UUID_1, IS_SECURE);
-  }
-
-  @Test
-  public void testReset() throws RemoteException {
-    carSppManager.startAssociation(/* nameForAssociation= */ null, mockAssociationCallback);
-    PendingConnection connection = connectionResultCaptor.getResult();
-    connection.notifyConnected(TEST_BLUETOOTH_DEVICE, TEST_BLUETOOTH_DEVICE.getName());
-
-    carSppManager.reset();
-
-    verify(mockSppBinder).disconnect(connection.toConnection(TEST_BLUETOOTH_DEVICE));
-  }
-
-  @Test
-  public void testResetBeforeConnection() throws RemoteException {
-    carSppManager.startAssociation(/* nameForAssociation= */ null, mockAssociationCallback);
-
-    carSppManager.reset();
-
-    verify(mockSppBinder).cancelConnectionAttempt(any());
-  }
-
-  @Test
-  public void testAssociationCallbackOnErrorSucceed() {
-    SecureChannel channel = getChannelForAssociation(mockAssociationCallback);
-    SecureChannel.Callback channelCallback = channel.getCallback();
-    channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
-    channelCallback.onSecureChannelEstablished();
-
-    carSppManager.reconnectOnErrorListener.onError(
-        connectionResultCaptor.getResult().toConnection(TEST_BLUETOOTH_DEVICE));
-
-    verify(mockCallback).onDeviceDisconnected(TEST_REMOTE_DEVICE_ID.toString());
-    assertThat(carSppManager.currentConnection).isNull();
-  }
-
-  @Test
-  public void testAssociationCallbackOnErrorFailed() {
-    SecureChannel channel = getChannelForAssociation(mockAssociationCallback);
-    SecureChannel.Callback channelCallback = channel.getCallback();
-    channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
-    channelCallback.onSecureChannelEstablished();
-
-    connectionResultCaptor.getResult().notifyConnectionError();
-
-    verify(mockCallback, never()).onDeviceDisconnected(TEST_REMOTE_DEVICE_ID.toString());
-    assertThat(carSppManager.currentConnection).isNotNull();
-  }
-
-  @Test
-  public void testAssociationCallbackOnConnected() {
-    carSppManager.startAssociation(/* nameForAssociation= */ null, mockAssociationCallback);
-    PendingConnection connection = connectionResultCaptor.getResult();
-    connection.notifyConnected(TEST_BLUETOOTH_DEVICE, TEST_BLUETOOTH_DEVICE.getName());
-
-    assertThat(carSppManager.currentConnection)
-        .isEqualTo(connection.toConnection(TEST_BLUETOOTH_DEVICE));
-  }
-
-  @Test
-  public void testAssociationCallbackOnConnectAttemptFailed() {
-    carSppManager.startAssociation(/* nameForAssociation= */ null, mockAssociationCallback);
-    connectionResultCaptor.getResult().notifyConnectionError();
-
-    verify(mockCallback, never()).onDeviceDisconnected(any());
-  }
-
-  @Test
-  public void testReconnectCallbackOnConnected() {
-    carSppManager.startAssociation(/* nameForAssociation= */ null, mockAssociationCallback);
-    PendingConnection connection = connectionResultCaptor.getResult();
-    connection.notifyConnected(TEST_BLUETOOTH_DEVICE, TEST_BLUETOOTH_DEVICE.getName());
-
-    assertThat(carSppManager.currentConnection)
-        .isEqualTo(connection.toConnection(TEST_BLUETOOTH_DEVICE));
-  }
-
-  @Test
-  public void testReconnectCallbackOnErrorSucceed() {
-    SecureChannel channel = getChannelForReconnect();
-    SecureChannel.Callback channelCallback = channel.getCallback();
-    channelCallback.onSecureChannelEstablished();
-
-    carSppManager.reconnectOnErrorListener.onError(
-        connectionResultCaptor.getResult().toConnection(TEST_BLUETOOTH_DEVICE));
-
-    verify(mockCallback).onDeviceDisconnected(TEST_REMOTE_DEVICE_ID.toString());
-    assertThat(carSppManager.currentConnection).isNull();
-  }
-
-  @Test
-  public void testReconnectCallbackOnErrorFailed() {
-    SecureChannel channel = getChannelForReconnect();
-    SecureChannel.Callback channelCallback = channel.getCallback();
-    channelCallback.onSecureChannelEstablished();
-
-    connectionResultCaptor.getResult().notifyConnectionError();
-
-    verify(mockCallback, never()).onDeviceDisconnected(TEST_REMOTE_DEVICE_ID.toString());
-    assertThat(carSppManager.currentConnection).isNotNull();
-  }
-
-  @Test
-  public void testReconnectCallbackOnConnectAttemptFailed() {
-    carSppManager.startAssociation(/* nameForAssociation= */ null, mockAssociationCallback);
-    connectionResultCaptor.getResult().notifyConnectionError();
-
-    verify(mockCallback, never()).onDeviceDisconnected(any());
-  }
-
-  private AssociationSecureChannel getChannelForAssociation(AssociationCallback callback) {
-    carSppManager.startAssociation(/* nameForAssociation= */ null, callback);
-
-    connectionResultCaptor
-        .getResult()
-        .notifyConnected(TEST_BLUETOOTH_DEVICE, TEST_BLUETOOTH_DEVICE.getName());
-    completeVersionExchange();
-
-    return (AssociationSecureChannel) carSppManager.getConnectedDeviceChannel();
-  }
-
-  private ReconnectSecureChannel getChannelForReconnect() {
-    carSppManager.initiateConnectionToDevice(TEST_REMOTE_DEVICE_ID);
-    
-    connectionResultCaptor
-        .getResult()
-        .notifyConnected(TEST_BLUETOOTH_DEVICE, TEST_BLUETOOTH_DEVICE.getName());
-    completeVersionExchange();
-
-    return (ReconnectSecureChannel) carSppManager.getConnectedDeviceChannel();
-  }
-
-  private void completeVersionExchange() {
-    ArgumentCaptor<OnMessageReceivedListener> listenerCaptor =
-        ArgumentCaptor.forClass(OnMessageReceivedListener.class);
-    verify(mockSppBinder)
-        .setOnMessageReceivedListener(
-            eq(carSppManager.currentConnection), listenerCaptor.capture());
-    // Use the MIN_SECURITY_VERSION for both to avoid further complexity introduced with
-    // the capabilities exchange that has nothing to do with the tests here. In the future this
-    // class will be deprecated in favor of a protocol agnostic version.
-    VersionExchange versionExchangeMessage =
-        VersionExchange.newBuilder()
-            .setMinSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
-            .setMaxSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
-            .setMinSupportedSecurityVersion(ConnectionResolver.MIN_SECURITY_VERSION)
-            .setMaxSupportedSecurityVersion(ConnectionResolver.MIN_SECURITY_VERSION)
-            .build();
-    listenerCaptor.getValue().onMessageReceived(versionExchangeMessage.toByteArray());
-  }
-
-  private static class ConnectionResultCaptor implements Answer<PendingConnection> {
-    private PendingConnection result;
-
-    public PendingConnection getResult() {
-      return result;
-    }
-
-    @Override
-    public PendingConnection answer(InvocationOnMock invocationOnMock) {
-      UUID uuid = invocationOnMock.getArgument(0);
-      boolean isSecure = invocationOnMock.getArgument(1);
-      result = new PendingConnection(uuid, isSecure);
-      return result;
-    }
-  }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/spp/SppDeviceMessageStreamTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/spp/SppDeviceMessageStreamTest.java
deleted file mode 100644
index 48951bd..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/spp/SppDeviceMessageStreamTest.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.connection.spp;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import android.bluetooth.BluetoothManager;
-import android.os.ParcelUuid;
-import android.os.RemoteException;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.connecteddevice.connection.DeviceMessageStream.DataReceivedListener;
-import com.google.android.connecteddevice.connection.DeviceMessageStream.MessageReceivedErrorListener;
-import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder;
-import com.google.android.connecteddevice.transport.spp.Connection;
-import java.util.UUID;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-@RunWith(AndroidJUnit4.class)
-public class SppDeviceMessageStreamTest {
-  private static final int MAX_WRITE_SIZE = 700;
-  private static final byte[] TEST_DATA = "testData".getBytes(UTF_8);
-  private static final UUID TEST_UUID = UUID.randomUUID();
-  private static final boolean IS_SECURE = true;
-
-  private static final Connection TEST_CONNECTION =
-      new Connection(
-          new ParcelUuid(TEST_UUID),
-          ApplicationProvider.getApplicationContext()
-              .getSystemService(BluetoothManager.class)
-              .getAdapter()
-              .getRemoteDevice("00:11:22:33:44:55"),
-          IS_SECURE,
-          "testName");
-  @Rule public final MockitoRule mockito = MockitoJUnit.rule();
-
-  @Mock private ConnectedDeviceSppDelegateBinder mockSppBinder;
-  @Mock private MessageReceivedErrorListener mockErrorListener;
-  @Mock private DataReceivedListener mockListener;
-
-  private SppDeviceMessageStream sppDeviceMessageStream;
-
-  @Before
-  public void setUp() {
-    sppDeviceMessageStream =
-        spy(new SppDeviceMessageStream(mockSppBinder, TEST_CONNECTION, MAX_WRITE_SIZE));
-    sppDeviceMessageStream.setMessageReceivedErrorListener(mockErrorListener);
-    sppDeviceMessageStream.setDataReceivedListener(mockListener);
-  }
-
-  @Test
-  public void testSend() throws RemoteException {
-    sppDeviceMessageStream.send(TEST_DATA);
-    verify(mockSppBinder).sendMessage(TEST_CONNECTION, TEST_DATA);
-    verify(sppDeviceMessageStream).sendCompleted();
-  }
-
-  @Test
-  public void testOnMessageReceived_connectionIsNotResolved() {
-    verify(mockSppBinder).setOnMessageReceivedListener(eq(TEST_CONNECTION), any());
-
-    sppDeviceMessageStream.onMessageReceived(TEST_DATA);
-
-    verify(mockListener).onDataReceived(TEST_DATA);
-  }
-
-  @Test
-  public void testOnMessageReceived_connectionIsResolved() {
-    sppDeviceMessageStream.setConnectionResolved(true);
-    verify(mockSppBinder).setOnMessageReceivedListener(eq(TEST_CONNECTION), any());
-
-    sppDeviceMessageStream.onMessageReceived(TEST_DATA);
-
-    verify(mockErrorListener).onMessageReceivedError(any());
-  }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/MultiProtocolDeviceControllerTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/MultiProtocolDeviceControllerTest.kt
index 2fae8f9..75316fc 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/MultiProtocolDeviceControllerTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/MultiProtocolDeviceControllerTest.kt
@@ -17,13 +17,13 @@
 
 import android.content.Context
 import android.database.sqlite.SQLiteCantOpenDatabaseException
+import android.os.ParcelUuid
 import android.util.Base64
 import androidx.room.Room
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.android.connecteddevice.api.IAssociationCallback
 import com.google.android.connecteddevice.connection.MultiProtocolSecureChannel
-import com.google.android.connecteddevice.connection.MultiProtocolSecureChannelPreV4
 import com.google.android.connecteddevice.connection.ProtocolStream
 import com.google.android.connecteddevice.core.DeviceController.Callback
 import com.google.android.connecteddevice.core.util.mockToBeAlive
@@ -40,9 +40,11 @@
 import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
 import com.google.android.connecteddevice.storage.ConnectedDeviceStorage.CHALLENGE_SECRET_BYTES
 import com.google.android.connecteddevice.storage.CryptoHelper
+import com.google.android.connecteddevice.transport.ConnectChallenge
 import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
 import com.google.android.connecteddevice.util.ByteUtils
-import com.google.android.connecteddevice.util.ThreadSafeCallbacks
 import com.google.android.encryptionrunner.EncryptionRunnerFactory
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.MoreExecutors.directExecutor
@@ -72,13 +74,13 @@
 class MultiProtocolDeviceControllerTest {
   private val context = ApplicationProvider.getApplicationContext<Context>()
   private val testConnectionProtocol: TestConnectionProtocol = spy(TestConnectionProtocol())
-  private val mockCallback: Callback = mock()
-  private val mockStream: ProtocolStream = mock()
-  private val mockOobRunner: OobRunner = mock { on { generateOobData() } doReturn TEST_OOB_DATA }
-  private val mockAssociationCallback: IAssociationCallback = mockToBeAlive()
-  private val mockDeadAssociationCallback: IAssociationCallback = mockToBeDead()
+  private val mockCallback = mock<Callback>()
+  private val mockStream = mock<ProtocolStream>()
+  private val mockOobRunner = mock<OobRunner> { on { generateOobData() } doReturn TEST_OOB_DATA }
+  private val mockAssociationCallback = mockToBeAlive<IAssociationCallback>()
+  private val mockDeadAssociationCallback = mockToBeDead<IAssociationCallback>()
   private val protocols = setOf(testConnectionProtocol)
-  private val testAssociationServiceUuid = UUID.randomUUID()
+  private val testAssociationServiceUuid = ParcelUuid(UUID.randomUUID())
   private lateinit var deviceController: MultiProtocolDeviceController
   private lateinit var secureChannel: MultiProtocolSecureChannel
   private lateinit var spyStorage: ConnectedDeviceStorage
@@ -100,18 +102,14 @@
         protocols,
         spyStorage,
         mockOobRunner,
-        testAssociationServiceUuid,
+        testAssociationServiceUuid.uuid,
         enablePassenger = false,
         callbackExecutor = directExecutor()
       )
     deviceController.registerCallback(mockCallback, directExecutor())
     secureChannel =
       spy(
-        MultiProtocolSecureChannelPreV4(
-          mockStream,
-          spyStorage,
-          EncryptionRunnerFactory.newFakeRunner()
-        )
+        MultiProtocolSecureChannel(mockStream, spyStorage, EncryptionRunnerFactory.newFakeRunner())
       )
   }
 
@@ -146,7 +144,7 @@
         protocols,
         transientErrorStorage,
         mockOobRunner,
-        testAssociationServiceUuid,
+        testAssociationServiceUuid.uuid,
         enablePassenger = false,
         callbackExecutor = directExecutor()
       )
@@ -157,7 +155,7 @@
 
   @Test
   fun start_connectsToPassengerDevicesWhenPassengerEnabled() {
-    val driverId = UUID.randomUUID()
+    val driverId = ParcelUuid(UUID.randomUUID())
     val driverDevice =
       AssociatedDevice(
         driverId.toString(),
@@ -165,7 +163,7 @@
         /* deviceName= */ null,
         /* isConnectionEnabled= */ true
       )
-    val passengerId = UUID.randomUUID()
+    val passengerId = ParcelUuid(UUID.randomUUID())
     val passengerDevice =
       AssociatedDevice(
         passengerId.toString(),
@@ -181,7 +179,7 @@
         protocols,
         spyStorage,
         mockOobRunner,
-        testAssociationServiceUuid,
+        testAssociationServiceUuid.uuid,
         enablePassenger = true,
         callbackExecutor = directExecutor()
       )
@@ -194,15 +192,15 @@
 
   @Test
   fun start_doesNotConnectToPassengerDevicesWhenPassengerDisabled() {
-    val driverId = UUID.randomUUID()
+    val driverId = ParcelUuid(UUID.randomUUID())
     val driverDevice =
       AssociatedDevice(
-        driverId.toString(),
+        driverId.uuid.toString(),
         /* deviceAddress= */ "",
         /* deviceName= */ null,
         /* isConnectionEnabled= */ true
       )
-    val passengerId = UUID.randomUUID()
+    val passengerId = ParcelUuid(UUID.randomUUID())
     val passengerDevice =
       AssociatedDevice(
         passengerId.toString(),
@@ -218,7 +216,7 @@
         protocols,
         spyStorage,
         mockOobRunner,
-        testAssociationServiceUuid,
+        testAssociationServiceUuid.uuid,
         enablePassenger = false,
         callbackExecutor = directExecutor()
       )
@@ -236,18 +234,18 @@
     deviceController.startAssociation(deviceName, mockAssociationCallback)
 
     verify(testConnectionProtocol)
-      .startAssociationDiscovery(eq(deviceName), any(), eq(testAssociationServiceUuid))
+      .startAssociationDiscovery(eq(deviceName), eq(testAssociationServiceUuid), any())
   }
 
   @Test
   fun startAssociation_startedSuccessfully() {
     val deviceName = "TestDeviceName"
-    val testIdentifier = UUID.randomUUID()
+    val testIdentifier = ParcelUuid(UUID.randomUUID())
 
-    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol)
-        .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+        .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
       firstValue.onDiscoveryStartedSuccessfully()
     }
 
@@ -270,12 +268,12 @@
   @Test
   fun startAssociation_callbackBinderIsDead_DoNotThrow() {
     val deviceName = "TestDeviceName"
-    val testIdentifier = UUID.randomUUID()
+    val testIdentifier = ParcelUuid(UUID.randomUUID())
 
-    deviceController.startAssociation(deviceName, mockDeadAssociationCallback, testIdentifier)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.startAssociation(deviceName, mockDeadAssociationCallback, testIdentifier.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol)
-        .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+        .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
       firstValue.onDiscoveryStartedSuccessfully()
     }
     verify(mockAssociationCallback, never())
@@ -291,12 +289,12 @@
   @Test
   fun startAssociation_onDiscoveryFailedToStartInvokesOnAssociationStartFailure() {
     val deviceName = "TestDeviceName"
-    val testIdentifier = UUID.randomUUID()
+    val testIdentifier = ParcelUuid(UUID.randomUUID())
 
-    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol)
-        .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+        .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
       firstValue.onDiscoveryFailedToStart()
     }
 
@@ -306,12 +304,12 @@
   @Test
   fun startAssociation_onDiscoveryFailedToStart_callbackBinderDeadDoNotThrow() {
     val deviceName = "TestDeviceName"
-    val testIdentifier = UUID.randomUUID()
+    val testIdentifier = ParcelUuid(UUID.randomUUID())
 
-    deviceController.startAssociation(deviceName, mockDeadAssociationCallback, testIdentifier)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.startAssociation(deviceName, mockDeadAssociationCallback, testIdentifier.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol)
-        .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+        .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
       firstValue.onDiscoveryFailedToStart()
     }
 
@@ -320,9 +318,9 @@
 
   @Test
   fun initiateConnectionToDevice_invokesStartConnectionDiscovery() {
-    val testUuid = UUID.randomUUID()
+    val testUuid = ParcelUuid(UUID.randomUUID())
 
-    deviceController.initiateConnectionToDevice(testUuid)
+    deviceController.initiateConnectionToDevice(testUuid.uuid)
 
     verify(testConnectionProtocol).startConnectionDiscovery(eq(testUuid), any(), any())
   }
@@ -353,14 +351,14 @@
         protocols,
         spyStorage,
         mockOobRunner,
-        testAssociationServiceUuid,
+        testAssociationServiceUuid.uuid,
         enablePassenger = false,
         callbackExecutor = directExecutor()
       )
     deviceController.registerCallback(mockCallback, directExecutor())
     deviceController.start()
     deviceController.initiateConnectionToDevice(deviceId)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol).startConnectionDiscovery(any(), any(), capture())
       firstValue.onDeviceConnected(UUID.randomUUID().toString())
     }
@@ -377,7 +375,7 @@
   fun onDeviceConnected_registerDeviceDisconnectedListener() {
     val testUuid = UUID.randomUUID()
     deviceController.initiateConnectionToDevice(testUuid)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol).startConnectionDiscovery(any(), any(), capture())
       firstValue.onDeviceConnected(UUID.randomUUID().toString())
     }
@@ -401,7 +399,7 @@
         )
       )
     deviceController.initiateConnectionToDevice(deviceId)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol).startConnectionDiscovery(any(), any(), capture())
       firstValue.onDeviceConnected(testProtocolId.toString())
     }
@@ -417,10 +415,10 @@
   @Test
   fun onDeviceDisconnected_duringAssociation_invokesAssociationErrorCallback() {
     val testProtocolId = UUID.randomUUID()
-    val testIdentifier = UUID.randomUUID()
-    deviceController.startAssociation("deviceName", mockAssociationCallback, testIdentifier)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
-      verify(testConnectionProtocol).startAssociationDiscovery(any(), capture(), eq(testIdentifier))
+    val testIdentifier = ParcelUuid(UUID.randomUUID())
+    deviceController.startAssociation("deviceName", mockAssociationCallback, testIdentifier.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
+      verify(testConnectionProtocol).startAssociationDiscovery(any(), eq(testIdentifier), capture())
       firstValue.onDeviceConnected(testProtocolId.toString())
     }
 
@@ -435,7 +433,7 @@
   @Test
   fun onDeviceDisconnected_afterAssociationCompleted_doNotInvokesAssociationErrorCallback() {
     val deviceId = UUID.randomUUID()
-    val testIdentifier = UUID.randomUUID()
+    val testIdentifier = ParcelUuid(UUID.randomUUID())
     val testProtocolId = UUID.randomUUID()
     val secret = ByteUtils.randomBytes(CHALLENGE_SECRET_BYTES)
     val testDeviceMessage =
@@ -446,9 +444,9 @@
         ByteUtils.uuidToBytes(deviceId) + secret
       )
 
-    deviceController.startAssociation("deviceName", mockAssociationCallback, testIdentifier)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
-      verify(testConnectionProtocol).startAssociationDiscovery(any(), capture(), eq(testIdentifier))
+    deviceController.startAssociation("deviceName", mockAssociationCallback, testIdentifier.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
+      verify(testConnectionProtocol).startAssociationDiscovery(any(), eq(testIdentifier), capture())
       firstValue.onDeviceConnected(testProtocolId.toString())
     }
 
@@ -471,7 +469,7 @@
 
   @Test
   fun onDeviceDisconnected_attemptsReconnectIfDeviceIsEnabled() {
-    val deviceId = UUID.randomUUID()
+    val deviceId = ParcelUuid(UUID.randomUUID())
     val testProtocolId = UUID.randomUUID()
     val associatedDevice =
       AssociatedDevice(
@@ -481,8 +479,8 @@
         /* isConnectionEnabled= */ true
       )
     spyStorage.addAssociatedDeviceForDriver(associatedDevice)
-    deviceController.initiateConnectionToDevice(deviceId)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.initiateConnectionToDevice(deviceId.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol).startConnectionDiscovery(eq(deviceId), any(), capture())
       firstValue.onDeviceConnected(testProtocolId.toString())
     }
@@ -497,7 +495,7 @@
 
   @Test
   fun onDeviceDisconnected_doesNotAttemptReconnectForDisabledDevice() {
-    val deviceId = UUID.randomUUID()
+    val deviceId = ParcelUuid(UUID.randomUUID())
     val testProtocolId = UUID.randomUUID()
     val associatedDevice =
       AssociatedDevice(
@@ -507,8 +505,8 @@
         /* isConnectionEnabled= */ true
       )
     spyStorage.addAssociatedDeviceForDriver(associatedDevice)
-    deviceController.initiateConnectionToDevice(deviceId)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.initiateConnectionToDevice(deviceId.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol).startConnectionDiscovery(eq(deviceId), any(), capture())
       firstValue.onDeviceConnected(testProtocolId.toString())
     }
@@ -551,7 +549,7 @@
         "test message".toByteArray()
       )
     deviceController.initiateConnectionToDevice(deviceId)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol).startConnectionDiscovery(any(), any(), capture())
       firstValue.onDeviceConnected(testProtocolId.toString())
     }
@@ -575,7 +573,7 @@
       )
 
     deviceController.initiateConnectionToDevice(testUuid)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol).startConnectionDiscovery(any(), any(), capture())
       firstValue.onDeviceConnected(UUID.randomUUID().toString())
     }
@@ -589,7 +587,7 @@
     val testProtocolId = UUID.randomUUID()
 
     deviceController.initiateConnectionToDevice(testUuid)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol).startConnectionDiscovery(any(), any(), capture())
       firstValue.onDeviceConnected(testProtocolId.toString())
     }
@@ -602,12 +600,12 @@
   @Test
   fun notifyVerificationCodeAccepted_notifiesChannel() {
     val deviceName = "TestDeviceName"
-    val testIdentifier = UUID.randomUUID()
+    val testIdentifier = ParcelUuid(UUID.randomUUID())
 
-    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol)
-        .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+        .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
       firstValue.onDeviceConnected(UUID.randomUUID().toString())
     }
     val device =
@@ -629,12 +627,12 @@
   @Test
   fun notifyVerificationCodeAccepted_deviceWithoutChannelDoesNotThrow() {
     val deviceName = "TestDeviceName"
-    val testIdentifier = UUID.randomUUID()
+    val testIdentifier = ParcelUuid(UUID.randomUUID())
 
-    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol)
-        .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+        .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
       firstValue.onDeviceConnected(UUID.randomUUID().toString())
     }
 
@@ -645,7 +643,7 @@
   fun handleSecureChannelMessage_firstAssociationMessageSavesIdAndSecretAndIssuesDeviceConnected() {
     val deviceName = "TestDeviceName"
     val deviceId = UUID.randomUUID()
-    val testIdentifier = UUID.randomUUID()
+    val testIdentifier = ParcelUuid(UUID.randomUUID())
     val secret = ByteUtils.randomBytes(CHALLENGE_SECRET_BYTES)
     val testDeviceMessage =
       DeviceMessage.createOutgoingMessage(
@@ -655,10 +653,10 @@
         ByteUtils.uuidToBytes(deviceId) + secret
       )
 
-    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol)
-        .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+        .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
       firstValue.onDeviceConnected(UUID.randomUUID().toString())
     }
 
@@ -680,7 +678,7 @@
   fun handleSecureChannelMessage_associationStorageErrorInvokesOnAssociationErrorCallback() {
     val deviceName = "TestDeviceName"
     val deviceId = UUID.randomUUID()
-    val testIdentifier = UUID.randomUUID()
+    val testIdentifier = ParcelUuid(UUID.randomUUID())
     val secret = ByteUtils.randomBytes(CHALLENGE_SECRET_BYTES - 1)
     val testDeviceMessage =
       DeviceMessage.createOutgoingMessage(
@@ -690,10 +688,10 @@
         ByteUtils.uuidToBytes(deviceId) + secret
       )
 
-    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol)
-        .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+        .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
       firstValue.onDeviceConnected(UUID.randomUUID().toString())
     }
 
@@ -731,7 +729,7 @@
         ByteUtils.randomBytes(10)
       )
     deviceController.initiateConnectionToDevice(deviceId)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol).startConnectionDiscovery(any(), any(), capture())
       firstValue.onDeviceConnected(testProtocolId.toString())
     }
@@ -749,7 +747,7 @@
   fun handleSecureChannelMessage_firstMessagePersistsDeviceAsDriverWhenPassengerDisabled() {
     val deviceName = "TestDeviceName"
     val deviceId = UUID.randomUUID()
-    val testIdentifier = UUID.randomUUID()
+    val testIdentifier = ParcelUuid(UUID.randomUUID())
     val secret = ByteUtils.randomBytes(CHALLENGE_SECRET_BYTES)
     val testDeviceMessage =
       DeviceMessage.createOutgoingMessage(
@@ -763,15 +761,15 @@
         protocols,
         spyStorage,
         mockOobRunner,
-        testAssociationServiceUuid,
+        testAssociationServiceUuid.uuid,
         enablePassenger = false,
         callbackExecutor = directExecutor()
       )
 
-    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol)
-        .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+        .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
       firstValue.onDeviceConnected(UUID.randomUUID().toString())
     }
 
@@ -792,30 +790,30 @@
   @Test
   fun handleSecureChannelMessage_firstMessagePersistsDeviceAsUnclaimedWhenPassengerEnabled() {
     val deviceName = "TestDeviceName"
-    val deviceId = UUID.randomUUID()
-    val testIdentifier = UUID.randomUUID()
+    val deviceId = ParcelUuid(UUID.randomUUID())
+    val testIdentifier = ParcelUuid(UUID.randomUUID())
     val secret = ByteUtils.randomBytes(CHALLENGE_SECRET_BYTES)
     val testDeviceMessage =
       DeviceMessage.createOutgoingMessage(
         null,
         true,
         OperationType.CLIENT_MESSAGE,
-        ByteUtils.uuidToBytes(deviceId) + secret
+        ByteUtils.uuidToBytes(deviceId.uuid) + secret
       )
     deviceController =
       MultiProtocolDeviceController(
         protocols,
         spyStorage,
         mockOobRunner,
-        testAssociationServiceUuid,
+        testAssociationServiceUuid.uuid,
         enablePassenger = true,
         callbackExecutor = directExecutor()
       )
 
-    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol)
-        .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+        .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
       firstValue.onDeviceConnected(UUID.randomUUID().toString())
     }
 
@@ -836,7 +834,7 @@
 
   @Test
   fun connectedDevices_returnsAllConnectedDevices() {
-    val activeUserDeviceId = UUID.randomUUID()
+    val activeUserDeviceId = ParcelUuid(UUID.randomUUID())
     val activeUserDevice =
       AssociatedDevice(
         activeUserDeviceId.toString(),
@@ -844,7 +842,7 @@
         "userDeviceName",
         /* isConnectionEnabled= */ true
       )
-    val otherUserDeviceId = UUID.randomUUID()
+    val otherUserDeviceId = ParcelUuid(UUID.randomUUID())
     val otherUserDevice =
       AssociatedDevice(
         otherUserDeviceId.toString(),
@@ -869,20 +867,20 @@
         protocols,
         spyStorage,
         mockOobRunner,
-        testAssociationServiceUuid,
+        testAssociationServiceUuid.uuid,
         enablePassenger = false,
         callbackExecutor = directExecutor()
       )
     deviceController.registerCallback(mockCallback, directExecutor())
     deviceController.start()
 
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol)
         .startConnectionDiscovery(eq(activeUserDeviceId), any(), capture())
       firstValue.onDeviceConnected(activeUserDeviceId.toString())
     }
-    deviceController.initiateConnectionToDevice(otherUserDeviceId)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.initiateConnectionToDevice(otherUserDeviceId.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol)
         .startConnectionDiscovery(eq(otherUserDeviceId), any(), capture())
       firstValue.onDeviceConnected(otherUserDeviceId.toString())
@@ -930,18 +928,18 @@
 
   @Test
   fun connectedDevices_returnsEmptyListWithNoAssociatedDevices() {
-    val activeUserDeviceId = UUID.randomUUID()
-    val otherUserDeviceId = UUID.randomUUID()
+    val activeUserDeviceId = ParcelUuid(UUID.randomUUID())
+    val otherUserDeviceId = ParcelUuid(UUID.randomUUID())
     whenever(spyStorage.driverAssociatedDevices).thenReturn(listOf())
     whenever(spyStorage.allAssociatedDevices).thenReturn(listOf())
-    deviceController.initiateConnectionToDevice(activeUserDeviceId)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.initiateConnectionToDevice(activeUserDeviceId.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol)
         .startConnectionDiscovery(eq(activeUserDeviceId), any(), capture())
       firstValue.onDeviceConnected(activeUserDeviceId.toString())
     }
-    deviceController.initiateConnectionToDevice(otherUserDeviceId)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.initiateConnectionToDevice(otherUserDeviceId.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol)
         .startConnectionDiscovery(eq(otherUserDeviceId), any(), capture())
       firstValue.onDeviceConnected(otherUserDeviceId.toString())
@@ -954,7 +952,7 @@
 
   @Test
   fun disconnectDevice_stopsDiscoveryAndDisconnectsAllProtocolsForDevice() {
-    val deviceId = UUID.randomUUID()
+    val deviceId = ParcelUuid(UUID.randomUUID())
     val testProtocolId1 = UUID.randomUUID().toString()
     val testProtocolId2 = UUID.randomUUID().toString()
     val protocol1 = spy(TestConnectionProtocol())
@@ -964,22 +962,22 @@
           setOf(protocol1, protocol2),
           spyStorage,
           mockOobRunner,
-          testAssociationServiceUuid,
+          testAssociationServiceUuid.uuid,
           enablePassenger = false,
           callbackExecutor = directExecutor()
         )
         .apply { registerCallback(mockCallback, directExecutor()) }
-    deviceController.initiateConnectionToDevice(deviceId)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.initiateConnectionToDevice(deviceId.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(protocol1).startConnectionDiscovery(any(), any(), capture())
       firstValue.onDeviceConnected(testProtocolId1)
     }
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(protocol2).startConnectionDiscovery(any(), any(), capture())
       firstValue.onDeviceConnected(testProtocolId2)
     }
 
-    deviceController.disconnectDevice(deviceId)
+    deviceController.disconnectDevice(deviceId.uuid)
 
     verify(protocol1).disconnectDevice(testProtocolId1)
     verify(protocol1).stopConnectionDiscovery(deviceId)
@@ -1002,13 +1000,13 @@
           setOf(protocol),
           spyStorage,
           mockOobRunner,
-          testAssociationServiceUuid,
+          testAssociationServiceUuid.uuid,
           enablePassenger = false,
           callbackExecutor = directExecutor()
         )
         .apply { registerCallback(mockCallback, directExecutor()) }
     deviceController.initiateConnectionToDevice(deviceId)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(protocol).startConnectionDiscovery(any(), any(), capture())
       firstValue.onDeviceConnected(testProtocolId)
     }
@@ -1040,13 +1038,13 @@
           setOf(protocol),
           spyStorage,
           mockOobRunner,
-          testAssociationServiceUuid,
+          testAssociationServiceUuid.uuid,
           enablePassenger = false,
           callbackExecutor = directExecutor()
         )
         .apply { registerCallback(mockCallback, directExecutor()) }
     deviceController.start()
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(protocol).startConnectionDiscovery(any(), any(), capture())
       firstValue.onDeviceConnected(testProtocolId)
     }
@@ -1060,13 +1058,13 @@
   @Test
   fun stopAssociation_disconnectPendingDeviceAndClearOobRunner() {
     val deviceName = "TestDeviceName"
-    val testIdentifier = UUID.randomUUID()
+    val testIdentifier = ParcelUuid(UUID.randomUUID())
     val testProtocolId = UUID.randomUUID().toString()
 
-    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol)
-        .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+        .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
       firstValue.onDeviceConnected(testProtocolId)
     }
     deviceController.stopAssociation()
@@ -1078,8 +1076,8 @@
 
   @Test
   fun start_initiatesConnectionToAllEnabledActiveUserDevices() {
-    val enabledDeviceId = UUID.randomUUID()
-    val disabledDeviceId = UUID.randomUUID()
+    val enabledDeviceId = ParcelUuid(UUID.randomUUID())
+    val disabledDeviceId = ParcelUuid(UUID.randomUUID())
     val enabledDevice =
       AssociatedDevice(
         enabledDeviceId.toString(),
@@ -1158,12 +1156,12 @@
 
   private fun startAssociation() {
     val deviceName = "TestDeviceName"
-    val testIdentifier = UUID.randomUUID()
+    val testIdentifier = ParcelUuid(UUID.randomUUID())
 
-    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
-    argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+    deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+    argumentCaptor<IDiscoveryCallback>().apply {
       verify(testConnectionProtocol)
-        .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+        .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
       firstValue.onDeviceConnected(UUID.randomUUID().toString())
     }
   }
@@ -1174,30 +1172,28 @@
     override fun decrypt(value: String?): ByteArray? = Base64.decode(value, Base64.DEFAULT)
   }
 
-  open class TestConnectionProtocol : ConnectionProtocol() {
-    val deviceDisconnectedListenerList:
-      MutableMap<String, ThreadSafeCallbacks<DeviceDisconnectedListener>> =
-      deviceDisconnectedListeners
+  open class TestConnectionProtocol : ConnectionProtocol(directExecutor()) {
+    val deviceDisconnectedListenerList = deviceDisconnectedListeners
 
-    override val isDeviceVerificationRequired = false
+    override fun isDeviceVerificationRequired() = false
 
     override fun startAssociationDiscovery(
       name: String,
-      callback: DiscoveryCallback,
-      identifier: UUID
+      identifier: ParcelUuid,
+      callback: IDiscoveryCallback
     ) {}
 
     override fun startConnectionDiscovery(
-      id: UUID,
+      id: ParcelUuid,
       challenge: ConnectChallenge,
-      callback: DiscoveryCallback
+      callback: IDiscoveryCallback
     ) {}
 
     override fun stopAssociationDiscovery() {}
 
-    override fun stopConnectionDiscovery(id: UUID) {}
+    override fun stopConnectionDiscovery(id: ParcelUuid) {}
 
-    override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {}
+    override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {}
 
     override fun disconnectDevice(protocolId: String) {}
 
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannelTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannelTest.java
index 647a852..432b7d4 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannelTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannelTest.java
@@ -16,7 +16,6 @@
 
 package com.google.android.connecteddevice.oob;
 
-import static com.google.android.connecteddevice.model.OobEligibleDevice.OOB_TYPE_BLUETOOTH;
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.mockito.ArgumentMatchers.any;
@@ -29,24 +28,24 @@
 
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothManager;
+import android.os.ParcelUuid;
 import android.os.RemoteException;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.connecteddevice.model.OobEligibleDevice;
 import com.google.android.connecteddevice.transport.BluetoothDeviceProvider;
+import com.google.android.connecteddevice.transport.ConnectChallenge;
 import com.google.android.connecteddevice.transport.ConnectionProtocol;
+import com.google.android.connecteddevice.transport.IDataSendCallback;
+import com.google.android.connecteddevice.transport.IDiscoveryCallback;
 import com.google.android.connecteddevice.transport.ProtocolDevice;
 import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder;
 import com.google.android.connecteddevice.transport.spp.PendingConnection;
-import com.google.android.connecteddevice.transport.spp.PendingSentMessage;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.primitives.Bytes;
 import java.util.UUID;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.junit.MockitoJUnit;
@@ -57,8 +56,6 @@
 @RunWith(AndroidJUnit4.class)
 public class BluetoothRfcommChannelTest {
   private static final String DEVICE_ADDRESS = "00:11:22:33:AA:BB";
-  private static final OobEligibleDevice OOB_ELIGIBLE_DEVICE =
-      new OobEligibleDevice(DEVICE_ADDRESS, OOB_TYPE_BLUETOOTH);
   private static final byte[] TEST_MESSAGE = "someData".getBytes(UTF_8);
   private static final BluetoothDevice TEST_BLUETOOTH_DEVICE =
       ApplicationProvider.getApplicationContext()
@@ -80,54 +77,20 @@
   }
 
   @Test
-  public void completeOobExchange_success() throws Exception {
-    PendingSentMessage pendingSentMessage = new PendingSentMessage();
-    when(mockSppDelegateBinder.sendMessage(any(), any())).thenReturn(pendingSentMessage);
-    PendingConnection connection = establishConnection();
-
-    OobConnectionManager oobConnectionManager = new OobConnectionManager();
-    oobConnectionManager.startOobExchange(bluetoothRfcommChannel);
-
-    ArgumentCaptor<byte[]> messageArgumentCaptor = ArgumentCaptor.forClass(byte[].class);
-    verify(mockSppDelegateBinder).sendMessage(any(), messageArgumentCaptor.capture());
-    byte[] oobData = messageArgumentCaptor.getValue();
-
-    assertThat(oobData)
-        .isEqualTo(
-            Bytes.concat(
-                oobConnectionManager.decryptionIv,
-                oobConnectionManager.encryptionIv,
-                oobConnectionManager.encryptionKey.getEncoded()));
-
-    pendingSentMessage.notifyMessageSent();
-    verify(mockSppDelegateBinder).disconnect(connection.toConnection(TEST_BLUETOOTH_DEVICE));
-  }
-
-  @Test
-  public void completeOobExchange_pendingSentMessageIsNull_callOnFailed() throws Exception {
-    establishConnection();
-
-    new OobConnectionManager().startOobExchange(bluetoothRfcommChannel);
-
-    verify(mockSppDelegateBinder).sendMessage(any(), any());
-    verify(mockCallback).onOobExchangeFailure();
-  }
-
-  @Test
   public void completeOobExchange_createRfcommSocketFails_callOnFailed() throws Exception {
     doThrow(RemoteException.class)
         .when(mockSppDelegateBinder)
         .connectAsClient(any(), any(), anyBoolean());
 
     bluetoothRfcommChannel.completeOobDataExchange(
-        OOB_ELIGIBLE_DEVICE, mockCallback, () -> ImmutableSet.of(TEST_BLUETOOTH_DEVICE));
+        TEST_BLUETOOTH_DEVICE, mockCallback, () -> ImmutableSet.of(TEST_BLUETOOTH_DEVICE));
     verify(mockCallback).onOobExchangeFailure();
   }
 
   @Test
   public void completeOobExchange_noBondedDevices_callOnFailed() {
     bluetoothRfcommChannel.completeOobDataExchange(
-        OOB_ELIGIBLE_DEVICE, mockCallback, ImmutableSet::of);
+        TEST_BLUETOOTH_DEVICE, mockCallback, ImmutableSet::of);
 
     verify(mockCallback).onOobExchangeFailure();
   }
@@ -140,7 +103,7 @@
             .getAdapter()
             .getRemoteDevice("BB:AA:33:22:11:00");
     bluetoothRfcommChannel.completeOobDataExchange(
-        OOB_ELIGIBLE_DEVICE, mockCallback, () -> ImmutableSet.of(otherBtDevice));
+        TEST_BLUETOOTH_DEVICE, mockCallback, () -> ImmutableSet.of(otherBtDevice));
 
     verify(mockCallback).onOobExchangeFailure();
   }
@@ -191,7 +154,7 @@
         .connectAsClient(any(), any(), anyBoolean());
 
     bluetoothRfcommChannel.completeOobDataExchange(
-        OOB_ELIGIBLE_DEVICE, mockCallback, () -> ImmutableSet.of(TEST_BLUETOOTH_DEVICE));
+        TEST_BLUETOOTH_DEVICE, mockCallback, () -> ImmutableSet.of(TEST_BLUETOOTH_DEVICE));
 
     verify(mockCallback, never()).onOobExchangeSuccess();
     verify(mockCallback, never()).onOobExchangeFailure();
@@ -240,7 +203,7 @@
         .connectAsClient(any(), any(), anyBoolean());
 
     bluetoothRfcommChannel.completeOobDataExchange(
-        OOB_ELIGIBLE_DEVICE, mockCallback, () -> ImmutableSet.of(TEST_BLUETOOTH_DEVICE));
+        TEST_BLUETOOTH_DEVICE, mockCallback, () -> ImmutableSet.of(TEST_BLUETOOTH_DEVICE));
     verify(mockSppDelegateBinder).connectAsClient(any(), any(), anyBoolean());
 
     return connectionCaptor.getResult();
@@ -264,7 +227,6 @@
 
   private static class TestConnectionProtocol extends ConnectionProtocol
       implements BluetoothDeviceProvider {
-
     @Override
     public BluetoothDevice getBluetoothDeviceById(String protocolId) {
       return null;
@@ -277,20 +239,20 @@
 
     @Override
     public void startAssociationDiscovery(
-        String name, DiscoveryCallback callback, UUID identifier) {}
+        String name, ParcelUuid identifier, IDiscoveryCallback callback) {}
 
     @Override
     public void startConnectionDiscovery(
-        UUID id, ConnectChallenge challenge, DiscoveryCallback callback) {}
+        ParcelUuid id, ConnectChallenge challenge, IDiscoveryCallback callback) {}
 
     @Override
     public void stopAssociationDiscovery() {}
 
     @Override
-    public void stopConnectionDiscovery(UUID id) {}
+    public void stopConnectionDiscovery(ParcelUuid id) {}
 
     @Override
-    public void sendData(String protocolId, byte[] data, DataSendCallback callback) {}
+    public void sendData(String protocolId, byte[] data, IDataSendCallback callback) {}
 
     @Override
     public void disconnectDevice(String protocolId) {}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobConnectionManagerTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobConnectionManagerTest.java
deleted file mode 100644
index 4fff6bc..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobConnectionManagerTest.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.oob;
-
-import static com.google.common.truth.Truth.assertThat;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.Assert.assertThrows;
-
-import android.security.keystore.KeyProperties;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.connecteddevice.model.OobEligibleDevice;
-import com.google.android.connecteddevice.transport.ProtocolDevice;
-import com.google.common.primitives.Bytes;
-import java.security.InvalidKeyException;
-import java.security.SecureRandom;
-import javax.crypto.AEADBadTagException;
-import javax.crypto.KeyGenerator;
-import javax.crypto.SecretKey;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class OobConnectionManagerTest {
-  private static final byte[] TEST_MESSAGE = "testMessage".getBytes(UTF_8);
-  private static final byte[] TEST_ENCRYPTION_IV =
-      new byte[OobConnectionManager.NONCE_LENGTH_BYTES];
-  private static final byte[] TEST_DECRYPTION_IV =
-      new byte[OobConnectionManager.NONCE_LENGTH_BYTES];
-
-  private TestChannel testChannel;
-  private SecretKey testKey;
-  private byte[] testOobData;
-
-  @Before
-  public void setUp() throws Exception {
-    testChannel = new TestChannel();
-    testKey = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES).generateKey();
-
-    SecureRandom secureRandom = new SecureRandom();
-    secureRandom.nextBytes(TEST_ENCRYPTION_IV);
-    secureRandom.nextBytes(TEST_DECRYPTION_IV);
-
-    testOobData = Bytes.concat(TEST_DECRYPTION_IV, TEST_ENCRYPTION_IV, testKey.getEncoded());
-  }
-
-  @Test
-  public void testInitAsServer_keyIsNull() {
-    OobConnectionManager oobConnectionManager = new OobConnectionManager();
-    assertThat(oobConnectionManager.encryptionKey).isNull();
-  }
-
-  @Test
-  public void testServer_onSetOobData_setsKeyAndNonce() {
-    OobConnectionManager oobConnectionManager = new OobConnectionManager();
-    oobConnectionManager.setOobData(testOobData);
-    assertThat(oobConnectionManager.encryptionKey).isEqualTo(testKey);
-    // The decryption IV for the server is the encryption IV for the client and vice versa
-    assertThat(oobConnectionManager.decryptionIv).isEqualTo(TEST_ENCRYPTION_IV);
-    assertThat(oobConnectionManager.encryptionIv).isEqualTo(TEST_DECRYPTION_IV);
-  }
-
-  @Test
-  public void testInitAsClient_keyAndNoncesAreNonNullAndSent() {
-    OobConnectionManager oobConnectionManager = new OobConnectionManager();
-    oobConnectionManager.startOobExchange(testChannel);
-    assertThat(oobConnectionManager.encryptionKey).isNotNull();
-    assertThat(oobConnectionManager.encryptionIv).isNotNull();
-    assertThat(oobConnectionManager.decryptionIv).isNotNull();
-    assertThat(testChannel.sentOobData)
-        .isEqualTo(
-            Bytes.concat(
-                oobConnectionManager.decryptionIv,
-                oobConnectionManager.encryptionIv,
-                oobConnectionManager.encryptionKey.getEncoded()));
-  }
-
-  @Test
-  public void testServerEncryptAndClientDecrypt() throws Exception {
-    OobConnectionManager clientOobConnectionManager = new OobConnectionManager();
-    clientOobConnectionManager.startOobExchange(testChannel);
-    OobConnectionManager serverOobConnectionManager = new OobConnectionManager();
-    serverOobConnectionManager.setOobData(testChannel.sentOobData);
-
-    byte[] encryptedTestMessage = clientOobConnectionManager.encryptVerificationCode(TEST_MESSAGE);
-    byte[] decryptedTestMessage =
-        serverOobConnectionManager.decryptVerificationCode(encryptedTestMessage);
-
-    assertThat(decryptedTestMessage).isEqualTo(TEST_MESSAGE);
-  }
-
-  @Test
-  public void testClientEncryptAndServerDecrypt() throws Exception {
-    OobConnectionManager clientOobConnectionManager = new OobConnectionManager();
-    clientOobConnectionManager.startOobExchange(testChannel);
-    OobConnectionManager serverOobConnectionManager = new OobConnectionManager();
-    serverOobConnectionManager.setOobData(testChannel.sentOobData);
-
-    byte[] encryptedTestMessage = serverOobConnectionManager.encryptVerificationCode(TEST_MESSAGE);
-    byte[] decryptedTestMessage =
-        clientOobConnectionManager.decryptVerificationCode(encryptedTestMessage);
-
-    assertThat(decryptedTestMessage).isEqualTo(TEST_MESSAGE);
-  }
-
-  @Test
-  public void testEncryptAndDecryptWithDifferentNonces_throwsAEADBadTagException()
-      throws Exception {
-    // The OobConnectionManager stores a different nonce for encryption and decryption, so it
-    // can't decrypt messages that it encrypted itself. It can only send encrypted messages to
-    // an OobConnectionManager on another device that share its nonces and encryption key.
-    OobConnectionManager oobConnectionManager = new OobConnectionManager();
-    oobConnectionManager.startOobExchange(testChannel);
-    byte[] encryptedMessage = oobConnectionManager.encryptVerificationCode(TEST_MESSAGE);
-    assertThrows(
-        AEADBadTagException.class,
-        () -> oobConnectionManager.decryptVerificationCode(encryptedMessage));
-  }
-
-  @Test
-  public void testDecryptWithShortMessage_throwsAEADBadTagException() {
-    OobConnectionManager oobConnectionManager = new OobConnectionManager();
-    oobConnectionManager.startOobExchange(testChannel);
-    assertThrows(
-        AEADBadTagException.class,
-        () -> oobConnectionManager.decryptVerificationCode("short".getBytes(UTF_8)));
-  }
-
-  @Test
-  public void testEncryptWithNullKey_throwsInvalidKeyException() {
-    OobConnectionManager oobConnectionManager = new OobConnectionManager();
-    assertThrows(
-        InvalidKeyException.class,
-        () -> oobConnectionManager.encryptVerificationCode(TEST_MESSAGE));
-  }
-
-  @Test
-  public void testDecryptWithNullKey_throwsInvalidKeyException() {
-    OobConnectionManager oobConnectionManager = new OobConnectionManager();
-    assertThrows(
-        InvalidKeyException.class,
-        () -> oobConnectionManager.decryptVerificationCode(TEST_MESSAGE));
-  }
-
-  private static class TestChannel implements OobChannel {
-    byte[] sentOobData = null;
-
-    @Override
-    public boolean completeOobDataExchange(ProtocolDevice protocolDevice, Callback callback) {
-      return false;
-    }
-
-    @Override
-    public void completeOobDataExchange(OobEligibleDevice device, Callback callback) {}
-
-    @Override
-    public void sendOobData(byte[] oobData) {
-      sentOobData = oobData;
-    }
-
-    @Override
-    public void interrupt() {}
-  }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobRunnerTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobRunnerTest.kt
index 58eed81..4d6e895 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobRunnerTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobRunnerTest.kt
@@ -16,23 +16,22 @@
 
 package com.google.android.connecteddevice.oob
 
+import android.os.ParcelUuid
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange.OobChannelType
-import com.google.android.companionprotos.OutOfBandAssociationToken
 import com.google.android.connecteddevice.model.OobData
-import com.google.android.connecteddevice.model.OobEligibleDevice
+import com.google.android.connecteddevice.transport.ConnectChallenge
 import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
 import com.google.android.connecteddevice.transport.ProtocolDevice
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.any
-import com.nhaarman.mockitokotlin2.argumentCaptor
 import com.nhaarman.mockitokotlin2.mock
 import com.nhaarman.mockitokotlin2.spy
 import com.nhaarman.mockitokotlin2.times
 import com.nhaarman.mockitokotlin2.verify
 import com.nhaarman.mockitokotlin2.whenever
-import java.io.IOException
-import java.util.UUID
 import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Test
@@ -40,9 +39,7 @@
 
 @RunWith(AndroidJUnit4::class)
 class OobRunnerTest {
-  private val mockOobRunnerCallback: OobRunner.Callback = mock()
-  private val mockOobChannelFactory: OobChannelFactory = mock()
-  private val testSecurityVersion = OobRunner.MIN_SECURITY_VERSION_FOR_OOB_PROTO
+  private val mockOobChannelFactory = mock<OobChannelFactory>()
   private val supportedTypesSingle = listOf(OobChannelType.BT_RFCOMM.name)
   private val supportedTypesTwo =
     listOf(OobChannelType.BT_RFCOMM.name, OobChannelType.OOB_CHANNEL_UNKNOWN.name)
@@ -61,70 +58,14 @@
   fun startOobDataExchange_startOobDataExchangeSuccessfully_returnTrue() {
     testOobChannel.oobDataExchangeResult = true
 
-    assertThat(
-        oobRunner.startOobDataExchange(
-          testProtocolDevice,
-          listOf(OobChannelType.BT_RFCOMM),
-          testSecurityVersion,
-          mockOobRunnerCallback
-        )
-      )
-      .isTrue()
-  }
-
-  @Test
-  fun startOobDataExchange_PreOobDataProto_sendRawBytes() {
-    testOobChannel.oobDataExchangeResult = true
-    oobRunner.generateOobData()
-
-    assertThat(
-        oobRunner.startOobDataExchange(
-          testProtocolDevice,
-          listOf(OobChannelType.BT_RFCOMM),
-          testSecurityVersion - 1,
-          mockOobRunnerCallback
-        )
-      )
-      .isTrue()
-    testOobChannel.oobChannelCallback!!.onOobExchangeSuccess()
-
-    val data =
-      argumentCaptor<ByteArray>().run {
-        verify(testOobChannel).sendOobData(capture())
-        firstValue
-      }
-
-    assertThrows(IOException::class.java) { OutOfBandAssociationToken.parseFrom(data) }
+    assertThat(oobRunner.sendOobData(testProtocolDevice)).isTrue()
   }
 
   @Test
   fun startOobDataExchange_startOobDataExchangeFailed_returnFalse() {
     testOobChannel.oobDataExchangeResult = false
 
-    assertThat(
-        oobRunner.startOobDataExchange(
-          testProtocolDevice,
-          listOf(OobChannelType.BT_RFCOMM),
-          testSecurityVersion,
-          mockOobRunnerCallback
-        )
-      )
-      .isFalse()
-  }
-
-  @Test
-  fun startOobDataExchange_unsupportedChannelType_returnFalse() {
-    testOobChannel.oobDataExchangeResult = true
-
-    assertThat(
-        oobRunner.startOobDataExchange(
-          testProtocolDevice,
-          listOf(OobChannelType.OOB_CHANNEL_UNKNOWN),
-          testSecurityVersion,
-          mockOobRunnerCallback
-        )
-      )
-      .isFalse()
+    assertThat(oobRunner.sendOobData(testProtocolDevice)).isFalse()
   }
 
   @Test
@@ -132,74 +73,15 @@
     val oobRunner = OobRunner(mockOobChannelFactory, supportedTypesTwo)
     testOobChannel.oobDataExchangeResult = false
 
-    assertThat(
-        oobRunner.startOobDataExchange(
-          testProtocolDevice,
-          listOf(OobChannelType.BT_RFCOMM, OobChannelType.OOB_CHANNEL_UNKNOWN),
-          testSecurityVersion,
-          mockOobRunnerCallback
-        )
-      )
-      .isFalse()
+    assertThat(oobRunner.sendOobData(testProtocolDevice)).isFalse()
     verify(mockOobChannelFactory, times(2)).createOobChannel(any())
   }
 
   @Test
-  fun startOobDataExchange_onOobExchangeSuccess_invokeSuccessCallback() {
-    testOobChannel.oobDataExchangeResult = true
-    oobRunner.generateOobData()
-    oobRunner.startOobDataExchange(
-      testProtocolDevice,
-      listOf(OobChannelType.BT_RFCOMM),
-      testSecurityVersion,
-      mockOobRunnerCallback
-    )
-    assertThat(testOobChannel.oobChannelCallback).isNotNull()
-    testOobChannel.oobChannelCallback!!.onOobExchangeSuccess()
-
-    verify(mockOobRunnerCallback).onOobDataExchangeSuccess()
-  }
-
-  @Test
-  fun startOobDataExchange_onOobExchangeSuccessWithoutOobData_invokeFailureCallback() {
-    testOobChannel.oobDataExchangeResult = true
-    oobRunner.startOobDataExchange(
-      testProtocolDevice,
-      listOf(OobChannelType.BT_RFCOMM),
-      testSecurityVersion,
-      mockOobRunnerCallback
-    )
-    testOobChannel.oobChannelCallback!!.onOobExchangeSuccess()
-
-    verify(mockOobRunnerCallback).onOobDataExchangeFailure()
-  }
-
-  @Test
-  fun startOobDataExchange_onOobExchangeFailure_invokeFailureCallback() {
-    testOobChannel.oobDataExchangeResult = true
-    oobRunner.generateOobData()
-    oobRunner.startOobDataExchange(
-      testProtocolDevice,
-      listOf(OobChannelType.BT_RFCOMM),
-      testSecurityVersion,
-      mockOobRunnerCallback
-    )
-    assertThat(testOobChannel.oobChannelCallback).isNotNull()
-    testOobChannel.oobChannelCallback!!.onOobExchangeFailure()
-
-    verify(mockOobRunnerCallback).onOobDataExchangeFailure()
-  }
-
-  @Test
   fun reset_resetAllStatus() {
     testOobChannel.oobDataExchangeResult = true
     oobRunner.generateOobData()
-    oobRunner.startOobDataExchange(
-      testProtocolDevice,
-      listOf(OobChannelType.BT_RFCOMM),
-      testSecurityVersion,
-      mockOobRunnerCallback
-    )
+    oobRunner.sendOobData(testProtocolDevice)
     oobRunner.reset()
     assertThat(testOobChannel.isInterrupted).isTrue()
     assertThat(oobRunner.encryptionKey).isNull()
@@ -285,11 +167,6 @@
       return oobDataExchangeResult
     }
 
-    override fun completeOobDataExchange(
-      device: OobEligibleDevice,
-      callback: OobChannel.Callback
-    ) {}
-
     override fun sendOobData(oobData: ByteArray) {}
 
     override fun interrupt() {
@@ -298,23 +175,22 @@
   }
 
   private class TestConnectionProtocol : ConnectionProtocol() {
-    override val isDeviceVerificationRequired: Boolean
-      get() = false
+    override fun isDeviceVerificationRequired() = false
 
     override fun startAssociationDiscovery(
       name: String,
-      callback: DiscoveryCallback,
-      identifier: UUID
+      identifier: ParcelUuid,
+      callback: IDiscoveryCallback
     ) {}
     override fun startConnectionDiscovery(
-      id: UUID,
+      id: ParcelUuid,
       challenge: ConnectChallenge,
-      callback: DiscoveryCallback
+      callback: IDiscoveryCallback
     ) {}
 
     override fun stopAssociationDiscovery() {}
-    override fun stopConnectionDiscovery(id: UUID) {}
-    override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {}
+    override fun stopConnectionDiscovery(id: ParcelUuid) {}
+    override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {}
     override fun disconnectDevice(protocolId: String) {}
     override fun getMaxWriteSize(protocolId: String): Int {
       return 0
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/PassThroughChannelTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/PassThroughChannelTest.kt
index 1966648..3ec4eec 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/PassThroughChannelTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/PassThroughChannelTest.kt
@@ -16,13 +16,16 @@
 
 package com.google.android.connecteddevice.oob
 
+import android.os.ParcelUuid
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.connecteddevice.transport.ConnectChallenge
 import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
 import com.google.android.connecteddevice.transport.ProtocolDevice
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.mock
 import com.nhaarman.mockitokotlin2.verify
-import java.util.UUID
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -40,23 +43,22 @@
 }
 
 private class TestConnectionProtocol : ConnectionProtocol() {
-  override val isDeviceVerificationRequired: Boolean
-    get() = false
+  override fun isDeviceVerificationRequired() = false
 
   override fun startAssociationDiscovery(
     name: String,
-    callback: DiscoveryCallback,
-    identifier: UUID
+    identifier: ParcelUuid,
+    callback: IDiscoveryCallback
   ) {}
   override fun startConnectionDiscovery(
-    id: UUID,
+    id: ParcelUuid,
     challenge: ConnectChallenge,
-    callback: DiscoveryCallback
+    callback: IDiscoveryCallback
   ) {}
 
   override fun stopAssociationDiscovery() {}
-  override fun stopConnectionDiscovery(id: UUID) {}
-  override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {}
+  override fun stopConnectionDiscovery(id: ParcelUuid) {}
+  override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {}
   override fun disconnectDevice(protocolId: String) {}
   override fun getMaxWriteSize(protocolId: String): Int {
     return 0
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/service/ConnectedDeviceManagerBinderTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/service/ConnectedDeviceManagerBinderTest.java
deleted file mode 100644
index 6244c96..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/service/ConnectedDeviceManagerBinderTest.java
+++ /dev/null
@@ -1,237 +0,0 @@
-/*
- * Copyright (C) 2020 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 com.google.android.connecteddevice.service;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.refEq;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import android.os.ParcelUuid;
-import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.connecteddevice.ConnectedDeviceManager;
-import com.google.android.connecteddevice.ConnectedDeviceManager.ConnectionCallback;
-import com.google.android.connecteddevice.ConnectedDeviceManager.DeviceAssociationCallback;
-import com.google.android.connecteddevice.ConnectedDeviceManager.DeviceCallback;
-import com.google.android.connecteddevice.api.IConnectionCallback;
-import com.google.android.connecteddevice.api.IDeviceAssociationCallback;
-import com.google.android.connecteddevice.api.IDeviceCallback;
-import com.google.android.connecteddevice.api.IOnLogRequestedListener;
-import com.google.android.connecteddevice.logging.LoggingManager;
-import com.google.android.connecteddevice.logging.LoggingManager.OnLogRequestedListener;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.ConnectedDevice;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.model.DeviceMessage.OperationType;
-import com.google.android.connecteddevice.util.ByteUtils;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-@RunWith(AndroidJUnit4.class)
-public class ConnectedDeviceManagerBinderTest {
-
-  private final ParcelUuid recipientId = new ParcelUuid(UUID.randomUUID());
-
-  @Rule public final MockitoRule mockito = MockitoJUnit.rule();
-  @Mock private ConnectedDeviceManager mockConnectedDeviceManager;
-  @Mock private LoggingManager mockLoggingManager;
-
-  private ConnectedDeviceManagerBinder binder;
-
-  @Before
-  public void setUp() {
-    binder =
-        new ConnectedDeviceManagerBinder(
-            mockConnectedDeviceManager, mockLoggingManager);
-  }
-
-  @Test
-  public void registerActiveUserConnectionCallback_mirrorsConnectedDeviceManager() {
-    binder.registerActiveUserConnectionCallback(createConnectionCallback());
-    verify(mockConnectedDeviceManager)
-        .registerActiveUserConnectionCallback(any(ConnectionCallback.class), any(Executor.class));
-  }
-
-  @Test
-  public void unregisterConnectionCallback_mirrorsConnectedDeviceManager() {
-    IConnectionCallback callback = createConnectionCallback();
-    binder.registerActiveUserConnectionCallback(callback);
-    binder.unregisterConnectionCallback(callback);
-    verify(mockConnectedDeviceManager).unregisterConnectionCallback(any(ConnectionCallback.class));
-  }
-
-  @Test
-  public void registerDeviceCallback_mirrorsConnectedDeviceManager() {
-    IDeviceCallback deviceCallback = createDeviceCallback();
-    ConnectedDevice connectedDevice =
-        new ConnectedDevice(
-            UUID.randomUUID().toString(),
-            /* deviceName = */ null,
-            /* belongsToActiveUser = */ false,
-            /* hasSecureChannel = */ false);
-    binder.registerDeviceCallback(connectedDevice, recipientId, deviceCallback);
-    verify(mockConnectedDeviceManager)
-        .registerDeviceCallback(
-            refEq(connectedDevice), refEq(recipientId.getUuid()),
-            any(DeviceCallback.class), any(Executor.class));
-  }
-
-  @Test
-  public void unregisterDeviceCallback_mirrorsConnectedDeviceManager() {
-    IDeviceCallback deviceCallback = createDeviceCallback();
-    ConnectedDevice connectedDevice =
-        new ConnectedDevice(
-            UUID.randomUUID().toString(),
-            /* deviceName = */ null,
-            /* belongsToActiveUser = */ false,
-            /* hasSecureChannel = */ false);
-    binder.registerDeviceCallback(connectedDevice, recipientId, deviceCallback);
-    binder.unregisterDeviceCallback(connectedDevice, recipientId, deviceCallback);
-    verify(mockConnectedDeviceManager)
-        .unregisterDeviceCallback(
-            refEq(connectedDevice),
-            refEq(recipientId.getUuid()),
-            any(DeviceCallback.class));
-  }
-
-  @Test
-  public void registerOnLogRequestedListener_mirrorsLoggingManager() {
-    IOnLogRequestedListener listener = createOnLogRequestedListener();
-    int testLoggerId = 1;
-    binder.registerOnLogRequestedListener(testLoggerId, listener);
-    verify(mockLoggingManager)
-        .registerLogRequestedListener(
-            eq(testLoggerId), any(OnLogRequestedListener.class), any(Executor.class));
-  }
-
-  @Test
-  public void unregisterOnLogRequestedListener_mirrorsLoggingManager() {
-    IOnLogRequestedListener listener = createOnLogRequestedListener();
-    int testLoggerId = 1;
-    binder.registerOnLogRequestedListener(testLoggerId, listener);
-    binder.unregisterOnLogRequestedListener(testLoggerId, listener);
-    verify(mockLoggingManager)
-        .unregisterLogRequestedListener(eq(testLoggerId), any(OnLogRequestedListener.class));
-  }
-
-  @Test
-  public void sendMessage_mirrorsConnectedDeviceManager() {
-    ConnectedDevice connectedDevice =
-        new ConnectedDevice(
-            UUID.randomUUID().toString(),
-            /* deviceName = */ null,
-            /* belongsToActiveUser = */ false,
-            /* hasSecureChannel = */ true);
-    DeviceMessage message =
-        DeviceMessage.createOutgoingMessage(
-            recipientId.getUuid(),
-            /* isMessageEncrypted= */ true,
-            OperationType.CLIENT_MESSAGE,
-            ByteUtils.randomBytes(10));
-    binder.sendMessage(connectedDevice, message);
-    verify(mockConnectedDeviceManager).sendMessage(connectedDevice, message);
-  }
-
-
-  @Test
-  public void registerDeviceAssociationCallback_mirrorsConnectedDeviceManager() {
-    IDeviceAssociationCallback associationCallback =
-        createDeviceAssociationCallback();
-    binder.registerDeviceAssociationCallback(associationCallback);
-    verify(mockConnectedDeviceManager)
-        .registerDeviceAssociationCallback(
-            any(DeviceAssociationCallback.class), any(Executor.class));
-  }
-
-  @Test
-  public void unregisterDeviceAssociationCallback_mirrorsConnectedDeviceManager() {
-    IDeviceAssociationCallback associationCallback =
-        createDeviceAssociationCallback();
-    binder.registerDeviceAssociationCallback(associationCallback);
-    binder.unregisterDeviceAssociationCallback(associationCallback);
-    verify(mockConnectedDeviceManager)
-        .unregisterDeviceAssociationCallback(any(DeviceAssociationCallback.class));
-  }
-
-  @NonNull
-  private static IConnectionCallback createConnectionCallback() {
-    return spy(
-        new IConnectionCallback.Stub() {
-          @Override
-          public void onDeviceConnected(ConnectedDevice connectedDevice) {
-          }
-
-          @Override
-          public void onDeviceDisconnected(ConnectedDevice connectedDevice) {
-          }
-        });
-  }
-
-  @NonNull
-  private static IDeviceCallback createDeviceCallback() {
-    return spy(
-        new IDeviceCallback.Stub() {
-          @Override
-          public void onSecureChannelEstablished(ConnectedDevice connectedDevice) {
-          }
-
-          @Override
-          public void onMessageReceived(ConnectedDevice connectedDevice, DeviceMessage message) {
-          }
-
-          @Override
-          public void onDeviceError(ConnectedDevice connectedDevice, int error) {
-          }
-        });
-  }
-
-  @NonNull
-  private static IDeviceAssociationCallback createDeviceAssociationCallback() {
-    return spy(
-        new IDeviceAssociationCallback.Stub() {
-          @Override
-          public void onAssociatedDeviceAdded(AssociatedDevice device) {
-          }
-
-          @Override
-          public void onAssociatedDeviceRemoved(AssociatedDevice device) {
-          }
-
-          @Override
-          public void onAssociatedDeviceUpdated(AssociatedDevice device) {
-          }
-        });
-  }
-
-  @NonNull
-  private static IOnLogRequestedListener createOnLogRequestedListener() {
-    return spy(
-        new IOnLogRequestedListener.Stub() {
-          @Override
-          public void onLogRecordsRequested() {}
-        });
-  }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ConnectionProtocolTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ConnectionProtocolTest.kt
index 1aaf952..3cb93eb 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ConnectionProtocolTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ConnectionProtocolTest.kt
@@ -1,13 +1,13 @@
 package com.google.android.connecteddevice.transport
 
+import android.os.ParcelUuid
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.android.connecteddevice.util.ThreadSafeCallbacks
+import com.google.android.connecteddevice.core.util.mockToBeAlive
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.MoreExecutors.directExecutor
 import com.nhaarman.mockitokotlin2.mock
 import com.nhaarman.mockitokotlin2.never
 import com.nhaarman.mockitokotlin2.verify
-import java.util.UUID
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -15,10 +15,9 @@
 @RunWith(AndroidJUnit4::class)
 class ConnectionProtocolTest {
   private lateinit var testProtocol: TestConnectionProtocol
-  private val mockDataReceivedListener: ConnectionProtocol.DataReceivedListener = mock()
-  private val mockDisconnectedListener: ConnectionProtocol.DeviceDisconnectedListener = mock()
-  private val mockMaxDataSizeChangedListener: ConnectionProtocol.DeviceMaxDataSizeChangedListener =
-    mock()
+  private val mockDataReceivedListener = mockToBeAlive<IDataReceivedListener>()
+  private val mockDisconnectedListener = mockToBeAlive<IDeviceDisconnectedListener>()
+  private val mockMaxDataSizeChangedListener = mockToBeAlive<IDeviceMaxDataSizeChangedListener>()
 
   @Before
   fun setUp() {
@@ -29,11 +28,7 @@
   fun registerDataReceivedListener_invokeListenerWithCall() {
     val testProtocolId = "testProtocolId"
     val testData = "testData".toByteArray()
-    testProtocol.registerDataReceivedListener(
-      testProtocolId,
-      mockDataReceivedListener,
-      directExecutor()
-    )
+    testProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
 
     testProtocol.invokeDataReceivedListeners(testProtocolId, testData)
 
@@ -46,11 +41,7 @@
     val testData = "testData".toByteArray()
     testProtocol.notifyDataReceived(testProtocolId, testData)
 
-    testProtocol.registerDataReceivedListener(
-      testProtocolId,
-      mockDataReceivedListener,
-      directExecutor()
-    )
+    testProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
 
     verify(mockDataReceivedListener).onDataReceived(testProtocolId, testData)
   }
@@ -61,17 +52,9 @@
     val testData = "testData".toByteArray()
     testProtocol.notifyDataReceived(testProtocolId, testData)
 
-    testProtocol.registerDataReceivedListener(
-      testProtocolId,
-      mockDataReceivedListener,
-      directExecutor()
-    )
-    val mockSecondDataReceivedListener: ConnectionProtocol.DataReceivedListener = mock()
-    testProtocol.registerDataReceivedListener(
-      testProtocolId,
-      mockSecondDataReceivedListener,
-      directExecutor()
-    )
+    testProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
+    val mockSecondDataReceivedListener: IDataReceivedListener = mock()
+    testProtocol.registerDataReceivedListener(testProtocolId, mockSecondDataReceivedListener)
 
     verify(mockDataReceivedListener).onDataReceived(testProtocolId, testData)
     verify(mockSecondDataReceivedListener, never()).onDataReceived(testProtocolId, testData)
@@ -81,11 +64,7 @@
   fun unregisterDataReceivedListener_doNotInvokeListenerWithCall() {
     val testProtocolId = "testProtocolId"
     val testData = "testData".toByteArray()
-    testProtocol.registerDataReceivedListener(
-      testProtocolId,
-      mockDataReceivedListener,
-      directExecutor()
-    )
+    testProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
     testProtocol.unregisterDataReceivedListener(testProtocolId, mockDataReceivedListener)
 
     testProtocol.invokeDataReceivedListeners(testProtocolId, testData)
@@ -101,11 +80,7 @@
   @Test
   fun unregisterDataReceivedListener_onLastListenerUnregistered_keyCleared() {
     val testProtocolId = "testProtocolId"
-    testProtocol.registerDataReceivedListener(
-      testProtocolId,
-      mockDataReceivedListener,
-      directExecutor()
-    )
+    testProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
 
     testProtocol.unregisterDataReceivedListener(testProtocolId, mockDataReceivedListener)
 
@@ -115,11 +90,7 @@
   @Test
   fun registerDeviceDisconnectListener_invokeListenerWithCall() {
     val testProtocolId = "testProtocolId"
-    testProtocol.registerDeviceDisconnectedListener(
-      testProtocolId,
-      mockDisconnectedListener,
-      directExecutor()
-    )
+    testProtocol.registerDeviceDisconnectedListener(testProtocolId, mockDisconnectedListener)
 
     testProtocol.invokeDeviceDisconnectedListeners(testProtocolId)
 
@@ -129,11 +100,7 @@
   @Test
   fun unregisterDeviceDisconnectListener_doNotInvokeListenerWithCall() {
     val testProtocolId = "testProtocolId"
-    testProtocol.registerDeviceDisconnectedListener(
-      testProtocolId,
-      mockDisconnectedListener,
-      directExecutor()
-    )
+    testProtocol.registerDeviceDisconnectedListener(testProtocolId, mockDisconnectedListener)
     testProtocol.unregisterDeviceDisconnectListener(testProtocolId, mockDisconnectedListener)
 
     testProtocol.invokeDeviceDisconnectedListeners(testProtocolId)
@@ -149,11 +116,7 @@
   @Test
   fun unregisterDeviceDisconnectListener_onLastListenerUnregistered_keyCleared() {
     val testProtocolId = "testProtocolId"
-    testProtocol.registerDeviceDisconnectedListener(
-      testProtocolId,
-      mockDisconnectedListener,
-      directExecutor()
-    )
+    testProtocol.registerDeviceDisconnectedListener(testProtocolId, mockDisconnectedListener)
 
     testProtocol.unregisterDeviceDisconnectListener(testProtocolId, mockDisconnectedListener)
 
@@ -166,8 +129,7 @@
     val testSize = 20
     testProtocol.registerDeviceMaxDataSizeChangedListener(
       testProtocolId,
-      mockMaxDataSizeChangedListener,
-      directExecutor()
+      mockMaxDataSizeChangedListener
     )
 
     testProtocol.invokeDeviceMaxDataSizeChangedListener(testProtocolId, testSize)
@@ -181,8 +143,7 @@
     val testSize = 20
     testProtocol.registerDeviceMaxDataSizeChangedListener(
       testProtocolId,
-      mockMaxDataSizeChangedListener,
-      directExecutor()
+      mockMaxDataSizeChangedListener
     )
     testProtocol.unregisterDeviceMaxDataSizeChangedListener(
       testProtocolId,
@@ -208,8 +169,7 @@
     val testProtocolId = "testProtocolId"
     testProtocol.registerDeviceMaxDataSizeChangedListener(
       testProtocolId,
-      mockMaxDataSizeChangedListener,
-      directExecutor()
+      mockMaxDataSizeChangedListener
     )
 
     testProtocol.unregisterDeviceMaxDataSizeChangedListener(
@@ -225,20 +185,11 @@
     val testProtocolId = "testProtocolId"
     val testData = "testData".toByteArray()
     val testSize = 20
-    testProtocol.registerDataReceivedListener(
-      testProtocolId,
-      mockDataReceivedListener,
-      directExecutor()
-    )
-    testProtocol.registerDeviceDisconnectedListener(
-      testProtocolId,
-      mockDisconnectedListener,
-      directExecutor()
-    )
+    testProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
+    testProtocol.registerDeviceDisconnectedListener(testProtocolId, mockDisconnectedListener)
     testProtocol.registerDeviceMaxDataSizeChangedListener(
       testProtocolId,
-      mockMaxDataSizeChangedListener,
-      directExecutor()
+      mockMaxDataSizeChangedListener
     )
     testProtocol.reset()
 
@@ -259,46 +210,37 @@
     testProtocol.notifyDataReceived(testProtocolId, testData)
     testProtocol.reset()
 
-    testProtocol.registerDataReceivedListener(
-      testProtocolId,
-      mockDataReceivedListener,
-      directExecutor()
-    )
+    testProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
 
     verify(mockDataReceivedListener, never()).onDataReceived(testProtocolId, testData)
   }
 
-  class TestConnectionProtocol : ConnectionProtocol() {
-    override val isDeviceVerificationRequired = false
+  class TestConnectionProtocol : ConnectionProtocol(directExecutor()) {
+    override fun isDeviceVerificationRequired() = false
 
-    val deviceDisconnectedListenerList:
-      MutableMap<String, ThreadSafeCallbacks<DeviceDisconnectedListener>> =
-      deviceDisconnectedListeners
+    val deviceDisconnectedListenerList = deviceDisconnectedListeners
 
-    val dataReceivedListenerList: MutableMap<String, ThreadSafeCallbacks<DataReceivedListener>> =
-      dataReceivedListeners
+    val dataReceivedListenerList = dataReceivedListeners
 
-    val maxDataSizeChangedListenerList:
-      MutableMap<String, ThreadSafeCallbacks<DeviceMaxDataSizeChangedListener>> =
-      maxDataSizeChangedListeners
+    val maxDataSizeChangedListenerList = maxDataSizeChangedListeners
 
     override fun startAssociationDiscovery(
       name: String,
-      callback: DiscoveryCallback,
-      identifier: UUID
+      identifier: ParcelUuid,
+      callback: IDiscoveryCallback,
     ) {}
 
     override fun startConnectionDiscovery(
-      id: UUID,
+      id: ParcelUuid,
       challenge: ConnectChallenge,
-      callback: DiscoveryCallback
+      callback: IDiscoveryCallback
     ) {}
 
     override fun stopAssociationDiscovery() {}
 
-    override fun stopConnectionDiscovery(id: UUID) {}
+    override fun stopConnectionDiscovery(id: ParcelUuid) {}
 
-    override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {}
+    override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {}
 
     override fun disconnectDevice(protocolId: String) {}
 
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ble/BlePeripheralProtocolTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ble/BlePeripheralProtocolTest.kt
index 4168ef4..24e78e6 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ble/BlePeripheralProtocolTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ble/BlePeripheralProtocolTest.kt
@@ -6,14 +6,16 @@
 import android.bluetooth.BluetoothManager
 import android.bluetooth.le.AdvertiseCallback
 import android.content.Context
+import android.os.ParcelUuid
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.android.connecteddevice.transport.ConnectionProtocol.ConnectChallenge
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DataReceivedListener
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DataSendCallback
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DeviceDisconnectedListener
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DeviceMaxDataSizeChangedListener
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DiscoveryCallback
+import com.google.android.connecteddevice.core.util.mockToBeAlive
+import com.google.android.connecteddevice.transport.ConnectChallenge
+import com.google.android.connecteddevice.transport.IDataReceivedListener
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDeviceDisconnectedListener
+import com.google.android.connecteddevice.transport.IDeviceMaxDataSizeChangedListener
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
 import com.google.android.connecteddevice.util.ByteUtils
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.MoreExecutors.directExecutor
@@ -37,7 +39,7 @@
 
 @RunWith(AndroidJUnit4::class)
 class BlePeripheralProtocolTest {
-  private val testIdentifier = UUID.randomUUID()
+  private val testIdentifier = ParcelUuid(UUID.randomUUID())
   private val testReconnectServiceUuid = UUID.randomUUID()
   private val testReconnectDataUuid = UUID.randomUUID()
   private val testAdvertiseDataCharacteristicUuid = UUID.randomUUID()
@@ -48,13 +50,13 @@
   private val testChallenge =
     ConnectChallenge("TestChallenge".toByteArray(), "TestSalt".toByteArray())
 
-  private val mockBlePeripheralManager: BlePeripheralManager = mock()
-  private val mockDiscoveryCallback: DiscoveryCallback = mock()
-  private val mockDataSendCallback: DataSendCallback = mock()
-  private val mockSecondDataSendCallback: DataSendCallback = mock()
-  private val mockDisconnectedListener: DeviceDisconnectedListener = mock()
-  private val mockDataReceivedListener: DataReceivedListener = mock()
-  private val mockMaxDataSizeChangedListener: DeviceMaxDataSizeChangedListener = mock()
+  private val mockBlePeripheralManager = mock<BlePeripheralManager>()
+  private val mockDiscoveryCallback = mockToBeAlive<IDiscoveryCallback>()
+  private val mockDataSendCallback = mockToBeAlive<IDataSendCallback>()
+  private val mockSecondDataSendCallback = mockToBeAlive<IDataSendCallback>()
+  private val mockDisconnectedListener = mockToBeAlive<IDeviceDisconnectedListener>()
+  private val mockDataReceivedListener = mockToBeAlive<IDataReceivedListener>()
+  private val mockMaxDataSizeChangedListener = mockToBeAlive<IDeviceMaxDataSizeChangedListener>()
 
   private lateinit var blePeripheralProtocol: BlePeripheralProtocol
   private lateinit var testBluetoothDevice: BluetoothDevice
@@ -74,7 +76,8 @@
         testWriteCharacteristicUuid,
         testReadCharacteristicUuid,
         testMaxReconnectAdvertisementDuration,
-        TEST_DEFAULT_MTU_SIZE
+        TEST_DEFAULT_MTU_SIZE,
+        directExecutor()
       )
   }
 
@@ -82,13 +85,13 @@
   fun startAssociationDiscovery_startsWithIdentifier() {
     blePeripheralProtocol.startAssociationDiscovery(
       TEST_DEVICE_NAME,
-      mockDiscoveryCallback,
-      testIdentifier
+      testIdentifier,
+      mockDiscoveryCallback
     )
 
     argumentCaptor<BluetoothGattService>().apply {
       verify(mockBlePeripheralManager).startAdvertising(capture(), any(), any(), any())
-      assertThat(firstValue.uuid).isEqualTo(testIdentifier)
+      assertThat(firstValue.uuid).isEqualTo(testIdentifier.uuid)
     }
   }
 
@@ -96,8 +99,8 @@
   fun startAssociationDiscovery_startedSuccessfully() {
     blePeripheralProtocol.startAssociationDiscovery(
       TEST_DEVICE_NAME,
-      mockDiscoveryCallback,
-      testIdentifier
+      testIdentifier,
+      mockDiscoveryCallback
     )
     argumentCaptor<AdvertiseCallback>().apply {
       verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), capture())
@@ -110,8 +113,8 @@
   fun startAssociationDiscovery_failedToStart() {
     blePeripheralProtocol.startAssociationDiscovery(
       TEST_DEVICE_NAME,
-      mockDiscoveryCallback,
-      testIdentifier
+      testIdentifier,
+      mockDiscoveryCallback
     )
     argumentCaptor<AdvertiseCallback>().apply {
       verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), capture())
@@ -125,8 +128,8 @@
     establishConnection(testBluetoothDevice)
     blePeripheralProtocol.startAssociationDiscovery(
       TEST_DEVICE_NAME,
-      mockDiscoveryCallback,
-      testIdentifier
+      testIdentifier,
+      mockDiscoveryCallback
     )
     // Should only be invoked once for establishing connection.
     verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), any())
@@ -136,8 +139,8 @@
   fun startAssociationDiscovery_multipleTimesSucceed() {
     blePeripheralProtocol.startAssociationDiscovery(
       TEST_DEVICE_NAME,
-      mockDiscoveryCallback,
-      testIdentifier
+      testIdentifier,
+      mockDiscoveryCallback
     )
     argumentCaptor<AdvertiseCallback>().apply {
       verify(mockBlePeripheralManager, atLeastOnce())
@@ -147,8 +150,8 @@
     blePeripheralProtocol.stopAssociationDiscovery()
     blePeripheralProtocol.startAssociationDiscovery(
       TEST_DEVICE_NAME,
-      mockDiscoveryCallback,
-      testIdentifier
+      testIdentifier,
+      mockDiscoveryCallback
     )
     argumentCaptor<AdvertiseCallback>().apply {
       verify(mockBlePeripheralManager, atLeastOnce())
@@ -165,8 +168,8 @@
   fun stopAssociationDiscovery_stopSuccessfully() {
     blePeripheralProtocol.startAssociationDiscovery(
       TEST_DEVICE_NAME,
-      mockDiscoveryCallback,
-      testIdentifier
+      testIdentifier,
+      mockDiscoveryCallback
     )
     blePeripheralProtocol.stopAssociationDiscovery()
     val advertiseCallback =
@@ -178,9 +181,8 @@
 
   @Test
   fun startConnectionDiscovery_startedSuccessfully() {
-    val testDeviceId = UUID.randomUUID()
     blePeripheralProtocol.startConnectionDiscovery(
-      testDeviceId,
+      testIdentifier,
       testChallenge,
       mockDiscoveryCallback
     )
@@ -193,9 +195,8 @@
 
   @Test
   fun startConnectionDiscovery_failedToStart() {
-    val testDeviceId = UUID.randomUUID()
     blePeripheralProtocol.startConnectionDiscovery(
-      testDeviceId,
+      testIdentifier,
       testChallenge,
       mockDiscoveryCallback
     )
@@ -210,7 +211,7 @@
   fun startConnectionDiscovery_alreadyInConnection() {
     establishConnection(testBluetoothDevice)
     blePeripheralProtocol.startConnectionDiscovery(
-      UUID.randomUUID(),
+      testIdentifier,
       testChallenge,
       mockDiscoveryCallback
     )
@@ -220,13 +221,12 @@
 
   @Test
   fun stopConnectionDiscovery_stopSuccessfully() {
-    val testDeviceId = UUID.randomUUID()
     blePeripheralProtocol.startConnectionDiscovery(
-      testDeviceId,
+      testIdentifier,
       testChallenge,
       mockDiscoveryCallback
     )
-    blePeripheralProtocol.stopConnectionDiscovery(testDeviceId)
+    blePeripheralProtocol.stopConnectionDiscovery(testIdentifier)
     val advertiseCallback =
       argumentCaptor<AdvertiseCallback>()
         .apply { verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), capture()) }
@@ -299,8 +299,8 @@
   fun reset_stopStartedAdvertising() {
     blePeripheralProtocol.startAssociationDiscovery(
       TEST_DEVICE_NAME,
-      mockDiscoveryCallback,
-      testIdentifier
+      testIdentifier,
+      mockDiscoveryCallback
     )
     blePeripheralProtocol.reset()
     val advertiseCallback =
@@ -334,11 +334,7 @@
         BluetoothGattCharacteristic.PERMISSION_WRITE
       )
     val testProtocolId = establishConnection(testBluetoothDevice)
-    blePeripheralProtocol.registerDataReceivedListener(
-      testProtocolId,
-      mockDataReceivedListener,
-      directExecutor()
-    )
+    blePeripheralProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
     argumentCaptor<BlePeripheralManager.OnCharacteristicWriteListener>().apply {
       verify(mockBlePeripheralManager).addOnCharacteristicWriteListener(capture())
       firstValue.onCharacteristicWrite(testBluetoothDevice, testReadCharacteristic, testMessage)
@@ -351,8 +347,7 @@
     val testProtocolId = establishConnection(testBluetoothDevice)
     blePeripheralProtocol.registerDeviceDisconnectedListener(
       testProtocolId,
-      mockDisconnectedListener,
-      directExecutor()
+      mockDisconnectedListener
     )
     argumentCaptor<BlePeripheralManager.Callback>().apply {
       verify(mockBlePeripheralManager).registerCallback(capture())
@@ -367,8 +362,7 @@
     val testMtuSize = 20
     blePeripheralProtocol.registerDeviceMaxDataSizeChangedListener(
       testProtocolId,
-      mockMaxDataSizeChangedListener,
-      directExecutor()
+      mockMaxDataSizeChangedListener
     )
     argumentCaptor<BlePeripheralManager.Callback>().apply {
       verify(mockBlePeripheralManager).registerCallback(capture())
@@ -416,8 +410,8 @@
   private fun establishConnection(bluetoothDevice: BluetoothDevice): String {
     blePeripheralProtocol.startAssociationDiscovery(
       TEST_DEVICE_NAME,
-      mockDiscoveryCallback,
-      testIdentifier
+      testIdentifier,
+      mockDiscoveryCallback
     )
     argumentCaptor<BlePeripheralManager.Callback>().apply {
       verify(mockBlePeripheralManager).registerCallback(capture())
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/spp/SppProtocolTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/spp/SppProtocolTest.kt
index c199de5..c71988d 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/spp/SppProtocolTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/spp/SppProtocolTest.kt
@@ -18,14 +18,16 @@
 
 import android.bluetooth.BluetoothManager
 import android.content.Context
+import android.os.ParcelUuid
 import android.os.RemoteException
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.android.connecteddevice.transport.ConnectionProtocol.ConnectChallenge
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DataReceivedListener
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DataSendCallback
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DeviceDisconnectedListener
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DiscoveryCallback
+import com.google.android.connecteddevice.core.util.mockToBeAlive
+import com.google.android.connecteddevice.transport.ConnectChallenge
+import com.google.android.connecteddevice.transport.IDataReceivedListener
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDeviceDisconnectedListener
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
 import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder.OnErrorListener
 import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder.OnMessageReceivedListener
 import com.google.android.connecteddevice.transport.spp.PendingConnection.OnConnectedListener
@@ -45,72 +47,69 @@
 
 @RunWith(AndroidJUnit4::class)
 class SppProtocolTest {
-  private val mockPendingConnection: PendingConnection = mock()
-  private val mockDiscoveryCallback: DiscoveryCallback = mock()
-  private val mockDataSendCallback: DataSendCallback = mock()
-  private val mockDisconnectedListener: DeviceDisconnectedListener = mock()
-  private val mockDataReceivedListener: DataReceivedListener = mock()
-  private val mockSppBinder: ConnectedDeviceSppDelegateBinder = mock {
-    on { connectAsServer(any(), any()) } doReturn mockPendingConnection
-  }
-  private val invalidMockSppBinder: ConnectedDeviceSppDelegateBinder = mock()
+  private val mockPendingConnection = mock<PendingConnection>()
+  private val mockDiscoveryCallback = mockToBeAlive<IDiscoveryCallback>()
+  private val mockDataSendCallback = mockToBeAlive<IDataSendCallback>()
+  private val mockDisconnectedListener = mockToBeAlive<IDeviceDisconnectedListener>()
+  private val mockDataReceivedListener = mockToBeAlive<IDataReceivedListener>()
+  private val mockSppBinder =
+    mock<ConnectedDeviceSppDelegateBinder> {
+      on { connectAsServer(any(), any()) } doReturn mockPendingConnection
+    }
+  private val invalidMockSppBinder = mock<ConnectedDeviceSppDelegateBinder>()
   private val testMaxSize = 0
-  private val testIdentifier = UUID.randomUUID()
+  private val testIdentifier = ParcelUuid(UUID.randomUUID())
   private val testProtocolId = UUID.randomUUID()
   private val testName = "TestName"
   private val testMessage = "TestMessage".toByteArray()
   private val testChallenge =
     ConnectChallenge("TestChallenge".toByteArray(), "TestSalt".toByteArray())
-  private val sppProtocol = SppProtocol(mockSppBinder, testMaxSize)
+  private val sppProtocol = SppProtocol(mockSppBinder, testMaxSize, directExecutor())
   private val invalidSppProtocol = SppProtocol(invalidMockSppBinder, testMaxSize)
 
   @Test
   fun startAssociationDiscovery_startedSuccessfully() {
-    sppProtocol.startAssociationDiscovery(testName, mockDiscoveryCallback, testIdentifier)
+    sppProtocol.startAssociationDiscovery(testName, testIdentifier, mockDiscoveryCallback)
 
-    verify(mockSppBinder).connectAsServer(eq(testIdentifier), any())
+    verify(mockSppBinder).connectAsServer(eq(testIdentifier.uuid), any())
     verify(mockDiscoveryCallback).onDiscoveryStartedSuccessfully()
   }
 
   @Test
   fun startAssociationDiscovery_startsWithIdentifier() {
-    val testIdentifier = UUID.randomUUID()
+    sppProtocol.startAssociationDiscovery(testName, testIdentifier, mockDiscoveryCallback)
 
-    sppProtocol.startAssociationDiscovery(testName, mockDiscoveryCallback, testIdentifier)
-
-    verify(mockSppBinder).connectAsServer(eq(testIdentifier), any())
+    verify(mockSppBinder).connectAsServer(eq(testIdentifier.uuid), any())
   }
 
   @Test
   fun startAssociationDiscovery_startedFailed() {
-    invalidSppProtocol.startAssociationDiscovery(testName, mockDiscoveryCallback, testIdentifier)
+    invalidSppProtocol.startAssociationDiscovery(testName, testIdentifier, mockDiscoveryCallback)
 
     verify(mockDiscoveryCallback).onDiscoveryFailedToStart()
   }
 
   @Test
   fun startConnectionDiscovery_startedSuccessfully() {
-    val testDeviceId = UUID.randomUUID()
+    sppProtocol.startConnectionDiscovery(testIdentifier, testChallenge, mockDiscoveryCallback)
 
-    sppProtocol.startConnectionDiscovery(testDeviceId, testChallenge, mockDiscoveryCallback)
-
-    verify(mockSppBinder).connectAsServer(eq(testDeviceId), any())
+    verify(mockSppBinder).connectAsServer(eq(testIdentifier.uuid), any())
     verify(mockDiscoveryCallback).onDiscoveryStartedSuccessfully()
   }
 
   @Test
   fun stopAssociationDiscovery_stopSuccessfully() {
-    sppProtocol.startAssociationDiscovery(testName, mockDiscoveryCallback, testIdentifier)
+    sppProtocol.startAssociationDiscovery(testName, testIdentifier, mockDiscoveryCallback)
+
     sppProtocol.stopAssociationDiscovery()
+
     verify(mockSppBinder).cancelConnectionAttempt(mockPendingConnection)
   }
 
   @Test
   fun stopConnectionDiscovery_stopSuccessfully() {
-    val testDeviceId = UUID.randomUUID()
-
-    sppProtocol.startConnectionDiscovery(testDeviceId, testChallenge, mockDiscoveryCallback)
-    sppProtocol.stopConnectionDiscovery(testDeviceId)
+    sppProtocol.startConnectionDiscovery(testIdentifier, testChallenge, mockDiscoveryCallback)
+    sppProtocol.stopConnectionDiscovery(testIdentifier)
 
     verify(mockSppBinder).cancelConnectionAttempt(mockPendingConnection)
   }
@@ -144,7 +143,7 @@
 
   @Test
   fun reset_cancelConnectionAttempt() {
-    sppProtocol.startAssociationDiscovery(testName, mockDiscoveryCallback, testIdentifier)
+    sppProtocol.startAssociationDiscovery(testName, testIdentifier, mockDiscoveryCallback)
     sppProtocol.reset()
 
     verify(mockSppBinder).cancelConnectionAttempt(mockPendingConnection)
@@ -166,7 +165,7 @@
   @Test
   fun onMessageReceived_informCallback() {
     val protocolId = establishConnection()
-    sppProtocol.registerDataReceivedListener(protocolId, mockDataReceivedListener, directExecutor())
+    sppProtocol.registerDataReceivedListener(protocolId, mockDataReceivedListener)
 
     argumentCaptor<OnMessageReceivedListener>().apply {
       verify(mockSppBinder).setOnMessageReceivedListener(any(), capture())
@@ -182,18 +181,14 @@
 
     argumentCaptor<UUID>().apply {
       verify(mockSppBinder).registerConnectionCallback(capture(), any())
-      assertThat(firstValue).isEqualTo(testIdentifier)
+      assertThat(firstValue).isEqualTo(testIdentifier.uuid)
     }
   }
 
   @Test
   fun onDeviceDisconnected_informCallback() {
     val protocolId = establishConnection()
-    sppProtocol.registerDeviceDisconnectedListener(
-      protocolId,
-      mockDisconnectedListener,
-      directExecutor()
-    )
+    sppProtocol.registerDeviceDisconnectedListener(protocolId, mockDisconnectedListener)
 
     val connection =
       argumentCaptor<Connection>()
@@ -211,7 +206,7 @@
   @Test
   fun startDiscovery_throwException_startDiscoveryFailed() {
     doThrow(RemoteException()).`when`(mockSppBinder).connectAsServer(any(), any())
-    sppProtocol.startAssociationDiscovery(testName, mockDiscoveryCallback, testIdentifier)
+    sppProtocol.startAssociationDiscovery(testName, testIdentifier, mockDiscoveryCallback)
 
     verify(mockDiscoveryCallback).onDiscoveryFailedToStart()
   }
@@ -265,10 +260,10 @@
       ApplicationProvider.getApplicationContext<Context>()
         .getSystemService(BluetoothManager::class.java)
     val testBluetoothDevice = bluetoothManager.adapter.getRemoteDevice(testMacAddress)
-    sppProtocol.startAssociationDiscovery(testName, mockDiscoveryCallback, testIdentifier)
+    sppProtocol.startAssociationDiscovery(testName, testIdentifier, mockDiscoveryCallback)
     argumentCaptor<OnConnectedListener>().apply {
       verify(mockPendingConnection).setOnConnectedListener(capture())
-      firstValue.onConnected(testIdentifier, testBluetoothDevice, false, testName)
+      firstValue.onConnected(testIdentifier.uuid, testBluetoothDevice, false, testName)
     }
     return argumentCaptor<String>()
       .apply { verify(mockDiscoveryCallback).onDeviceConnected(capture()) }
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceAgentServiceTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceAgentServiceTest.kt
new file mode 100644
index 0000000..04f3c81
--- /dev/null
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceAgentServiceTest.kt
@@ -0,0 +1,130 @@
+package com.google.android.connecteddevice.trust
+
+import android.app.ActivityManager
+import android.app.KeyguardManager
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import com.google.android.connecteddevice.trust.api.ITrustedDeviceAgentDelegate
+import com.google.android.connecteddevice.trust.api.ITrustedDeviceManager
+import com.google.android.connecteddevice.util.ByteUtils
+import com.nhaarman.mockitokotlin2.argumentCaptor
+import com.nhaarman.mockitokotlin2.atLeastOnce
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.never
+import com.nhaarman.mockitokotlin2.verify
+import java.lang.IllegalStateException
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.shadows.ShadowKeyguardManager
+
+@RunWith(RobolectricTestRunner::class)
+class TrustedDeviceAgentServiceTest {
+
+  private val context = ApplicationProvider.getApplicationContext<Context>()
+
+  private val userId = ActivityManager.getCurrentUser()
+
+  private val mockTrustedDeviceManager = mock<ITrustedDeviceManager>()
+
+  private lateinit var keyguardManager: ShadowKeyguardManager
+
+  private lateinit var service: TestTrustedDeviceAgentService
+
+  private lateinit var delegate: ITrustedDeviceAgentDelegate
+
+  @Before
+  fun setup() {
+    keyguardManager =
+      shadowOf(context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager)
+    service =
+      Robolectric.buildService(TestTrustedDeviceAgentService::class.java)
+        .apply { get().trustedDeviceManager = mockTrustedDeviceManager }
+        .create()
+        .get()
+    service.onCreate()
+    argumentCaptor<ITrustedDeviceAgentDelegate> {
+      verify(mockTrustedDeviceManager, atLeastOnce()).setTrustedDeviceAgentDelegate(capture())
+      delegate = firstValue
+    }
+  }
+
+  @Test
+  fun unlockUserWithToken_invokesCallbackAfterTokenReceived_DeviceAndUserAreLocked() {
+    lockUser()
+
+    sendToken()
+    unlockUser()
+
+    verify(mockTrustedDeviceManager).onUserUnlocked()
+  }
+
+  @Test
+  fun unlockUserWithToken_invokesCallbackAfterTokenReceived_DeviceIsLockedAndUserIsUnlocked() {
+    keyguardManager.setIsDeviceLocked(true)
+    service.isUserUnlocked = true
+
+    sendToken()
+
+    verify(mockTrustedDeviceManager).onUserUnlocked()
+  }
+
+  @Test
+  fun unlockUserWithToken_doesNotInvokeCallbackIfDeviceIsNotLocked() {
+    keyguardManager.setIsDeviceLocked(false)
+
+    sendToken()
+    unlockUser()
+
+    verify(mockTrustedDeviceManager, never()).onUserUnlocked()
+  }
+
+  @Test
+  fun onUserUnlock_doesNotInvokeCallbackIfTokenWasNotUsed() {
+    keyguardManager.setIsDeviceLocked(true)
+
+    unlockUser()
+
+    verify(mockTrustedDeviceManager, never()).onUserUnlocked()
+  }
+
+  private fun unlockUser() {
+    service.isUserUnlocked = true
+    service.maybeDismissLockscreen()
+  }
+
+  private fun lockUser() {
+    keyguardManager.setIsDeviceLocked(true)
+    service.isUserUnlocked = false
+  }
+
+  private fun sendToken() {
+    try {
+      delegate.unlockUserWithToken(ByteUtils.randomBytes(ESCROW_TOKEN_LENGTH), HANDLE, userId)
+    } catch (e: IllegalStateException) {
+      // This is expected because we aren't actually connected to a real TrustAgent.
+    }
+  }
+
+  companion object {
+    private const val ESCROW_TOKEN_LENGTH = 8
+
+    private const val HANDLE = 10L
+  }
+}
+
+class TestTrustedDeviceAgentService : TrustedDeviceAgentService() {
+
+  var isUserUnlocked = true
+
+  override fun bindToService() {
+    setupManager()
+  }
+
+  override fun isUserUnlocked(userId: Int): Boolean {
+    return isUserUnlocked
+  }
+}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceManagerTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceManagerTest.java
index 1260ba3..edfc1fc 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceManagerTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceManagerTest.java
@@ -3,6 +3,9 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -24,15 +27,19 @@
 import com.google.android.connecteddevice.trust.api.ITrustedDeviceCallback;
 import com.google.android.connecteddevice.trust.api.ITrustedDeviceEnrollmentCallback;
 import com.google.android.connecteddevice.trust.api.TrustedDevice;
+import com.google.android.connecteddevice.trust.proto.PhoneAuthProto.PhoneCredentials;
 import com.google.android.connecteddevice.trust.proto.TrustedDeviceMessageProto.TrustedDeviceMessage;
 import com.google.android.connecteddevice.trust.proto.TrustedDeviceMessageProto.TrustedDeviceMessage.MessageType;
 import com.google.android.connecteddevice.trust.proto.TrustedDeviceMessageProto.TrustedDeviceState;
 import com.google.android.connecteddevice.trust.storage.TrustedDeviceDatabase;
 import com.google.android.connecteddevice.trust.storage.TrustedDeviceEntity;
+import com.google.android.connecteddevice.trust.storage.TrustedDeviceTokenEntity;
+import com.google.android.connecteddevice.util.ByteUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.protobuf.ByteString;
 import java.util.Arrays;
 import java.util.List;
+import java.util.UUID;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -44,7 +51,7 @@
 
 @RunWith(AndroidJUnit4.class)
 public final class TrustedDeviceManagerTest {
-  private static final String DEFAULT_DEVICE_ID = "deviceId";
+  private static final String DEFAULT_DEVICE_ID = UUID.randomUUID().toString();
 
   // Note: This token needs to be of length 8 to be valid.
   private static final byte[] FAKE_TOKEN = "12345678".getBytes(UTF_8);
@@ -58,7 +65,7 @@
       new ConnectedDevice(
           DEFAULT_DEVICE_ID,
           "secureConnectedDevice",
-          /* belongsToActiveUser= */ true,
+          /* belongsToDriver= */ true,
           /* hasSecureChannel= */ true);
 
   private final Connector fakeConnector = spy(new FakeConnector());
@@ -73,6 +80,7 @@
   private TrustedDeviceManager manager;
   private TrustedDeviceFeature feature;
   private TrustedDeviceDatabase database;
+  private TrustedDeviceFeature.Callback featureCallback;
 
   @Before
   public void setUp() throws RemoteException {
@@ -102,6 +110,10 @@
     manager.setTrustedDeviceAgentDelegate(trustAgentDelegate);
     manager.registerTrustedDeviceEnrollmentCallback(enrollmentCallback);
     manager.registerTrustedDeviceCallback(trustedDeviceCallback);
+    ArgumentCaptor<TrustedDeviceFeature.Callback> captor =
+        ArgumentCaptor.forClass(TrustedDeviceFeature.Callback.class);
+    verify(feature).setCallback(captor.capture());
+    featureCallback = captor.getValue();
   }
 
   @After
@@ -135,6 +147,17 @@
   }
 
   @Test
+  public void enrollment_storesHashedToken() throws RemoteException {
+    triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
+
+    executeAndVerifyValidEnrollFlow();
+
+    TrustedDeviceTokenEntity entity =
+        database.trustedDeviceDao().getTrustedDeviceHashedToken(DEFAULT_DEVICE_ID);
+    assertThat(entity).isNotNull();
+  }
+
+  @Test
   public void testStateSync_removesStoredTrustedDevice() throws RemoteException {
     triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
     executeAndVerifyValidEnrollFlow();
@@ -174,10 +197,7 @@
 
     ConnectedDevice unenrolledDevice =
         new ConnectedDevice(
-            "unenrolled",
-            "unenrolled",
-            /* belongsToActiveUser= */ true,
-            /* hasSecureChannel= */ true);
+            "unenrolled", "unenrolled", /* belongsToDriver= */ true, /* hasSecureChannel= */ true);
 
     manager.featureCallback.onMessageReceived(
         unenrolledDevice, createStateSyncMessage(/* enabled= */ false));
@@ -324,6 +344,105 @@
     assertThat(trustedDeviceListCaptor.getValue()).containsExactly(expectedTrustedDevice);
   }
 
+  @Test
+  public void unlock_validTokenPassedToDelegate() throws RemoteException {
+    triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
+    executeAndVerifyValidEnrollFlow();
+
+    TrustedDeviceMessage unlockMessage =
+        TrustedDeviceMessage.newBuilder()
+            .setType(MessageType.UNLOCK_CREDENTIALS)
+            .setPayload(
+                PhoneCredentials.newBuilder()
+                    .setEscrowToken(ByteString.copyFrom(FAKE_TOKEN))
+                    .setHandle(ByteString.copyFrom(ByteUtils.longToBytes(FAKE_HANDLE)))
+                    .build()
+                    .toByteString())
+            .build();
+    featureCallback.onMessageReceived(SECURE_CONNECTED_DEVICE, unlockMessage.toByteArray());
+
+    verify(trustAgentDelegate).unlockUserWithToken(FAKE_TOKEN, FAKE_HANDLE, DEFAULT_USER_ID);
+  }
+
+  @Test
+  public void unlock_invalidTokenIsNotPassedToDelegate() throws RemoteException {
+    triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
+    executeAndVerifyValidEnrollFlow();
+    byte[] incorrectToken = ByteUtils.randomBytes(8);
+
+    TrustedDeviceMessage unlockMessage =
+        TrustedDeviceMessage.newBuilder()
+            .setType(MessageType.UNLOCK_CREDENTIALS)
+            .setPayload(
+                PhoneCredentials.newBuilder()
+                    .setEscrowToken(ByteString.copyFrom(incorrectToken))
+                    .setHandle(ByteString.copyFrom(ByteUtils.longToBytes(FAKE_HANDLE)))
+                    .build()
+                    .toByteString())
+            .build();
+    featureCallback.onMessageReceived(SECURE_CONNECTED_DEVICE, unlockMessage.toByteArray());
+
+    verify(trustAgentDelegate, never()).unlockUserWithToken(any(), anyLong(), anyInt());
+  }
+
+  @Test
+  public void unlock_missingHashedTokenIsNotPassedToDelegate() throws RemoteException {
+    triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
+    executeAndVerifyValidEnrollFlow();
+    database.trustedDeviceDao().removeTrustedDeviceHashedToken(DEFAULT_DEVICE_ID);
+
+    TrustedDeviceMessage unlockMessage =
+        TrustedDeviceMessage.newBuilder()
+            .setType(MessageType.UNLOCK_CREDENTIALS)
+            .setPayload(
+                PhoneCredentials.newBuilder()
+                    .setEscrowToken(ByteString.copyFrom(FAKE_TOKEN))
+                    .setHandle(ByteString.copyFrom(ByteUtils.longToBytes(FAKE_HANDLE)))
+                    .build()
+                    .toByteString())
+            .build();
+    featureCallback.onMessageReceived(SECURE_CONNECTED_DEVICE, unlockMessage.toByteArray());
+
+    verify(trustAgentDelegate, never()).unlockUserWithToken(any(), anyLong(), anyInt());
+  }
+
+  @Test
+  public void removeTrustedDevice_removesHashedToken() throws RemoteException {
+    triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
+    executeAndVerifyValidEnrollFlow();
+    ArgumentCaptor<TrustedDevice> captor = ArgumentCaptor.forClass(TrustedDevice.class);
+    verify(trustedDeviceCallback).onTrustedDeviceAdded(captor.capture());
+    TrustedDevice enrolledDevice = captor.getValue();
+
+    manager.removeTrustedDevice(enrolledDevice);
+
+    TrustedDeviceTokenEntity entity =
+        database.trustedDeviceDao().getTrustedDeviceHashedToken(DEFAULT_DEVICE_ID);
+    assertThat(entity).isNull();
+  }
+
+  @Test
+  public void setTrustedDeviceAgentDelegate_generatesFeatureStateAndRemovesAllInvalidDevices()
+      throws RemoteException {
+    String deviceId = UUID.randomUUID().toString();
+    TrustedDeviceEntity entity =
+        new TrustedDeviceEntity(
+            new TrustedDevice(deviceId, DEFAULT_USER_ID, FAKE_HANDLE), /* isValid= */ false);
+    database.trustedDeviceDao().addOrReplaceTrustedDevice(entity);
+    TrustedDeviceManager manager =
+        new TrustedDeviceManager(
+            database,
+            feature,
+            /* databaseExecutor= */ directExecutor(),
+            /* remoteCallbackExecutor= */ directExecutor());
+
+    manager.setTrustedDeviceAgentDelegate(trustAgentDelegate);
+
+    assertThat(database.trustedDeviceDao().getFeatureState(deviceId)).isNotNull();
+    assertThat(database.trustedDeviceDao().getTrustedDevice(deviceId)).isNull();
+    verify(trustAgentDelegate).removeEscrowToken(FAKE_HANDLE, DEFAULT_USER_ID);
+  }
+
   /**
    * Runs through a valid enrollment flow and verifies that the current flow is run.
    *
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/storage/MigrationTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/storage/MigrationTest.java
index 146db85..8dabeeb 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/storage/MigrationTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/storage/MigrationTest.java
@@ -1,11 +1,10 @@
 package com.google.android.connecteddevice.trust.storage;
 
+import static com.google.android.connecteddevice.trust.storage.TrustedDeviceDatabaseProvider.DATABASE_NAME;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 
 import android.content.ContentValues;
 import android.database.sqlite.SQLiteDatabase;
-import androidx.room.Room;
 import androidx.room.migration.Migration;
 import androidx.room.testing.MigrationTestHelper;
 import androidx.sqlite.db.SupportSQLiteDatabase;
@@ -14,24 +13,29 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.platform.app.InstrumentationRegistry;
 import java.io.IOException;
+import java.util.UUID;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @RunWith(AndroidJUnit4.class)
 public class MigrationTest {
-  private static final String TEST_DB_NAME = "migration_test";
+  private static final int CURRENT_VERSION = 4;
+
   private static final String TRUSTED_DEVICES_TABLE = "trusted_devices";
 
   private static final String TRUSTED_DEVICES_ID_COLUMN = "id";
   private static final String TRUSTED_DEVICES_USER_ID_COLUMN = "userId";
   private static final String TRUSTED_DEVICES_HANDLE_COLUMN = "handle";
+  private static final String TRUSTED_DEVICES_IS_VALID_COLUMN = "isValid";
 
-  private static final String DEFAULT_DEVICE_ID = "id";
+  private static final String DEFAULT_DEVICE_ID = UUID.randomUUID().toString();
   private static final int DEFAULT_USER_ID = 11;
   private static final long DEFAULT_HANDLE = 11L;
   private static final boolean DEFAULT_IS_VALID = true;
 
+  private static final int VERSION_IS_VALID_ADDED = 3;
+
   @Rule
   public MigrationTestHelper helper =
       new MigrationTestHelper(
@@ -40,77 +44,93 @@
           new FrameworkSQLiteOpenHelperFactory());
 
   @Test
-  public void testMigrate1To3() throws IOException {
+  public void testMigrate1To4() throws IOException {
+    int startingVersion = 1;
     // Create the database in version 1
-    SupportSQLiteDatabase db = helper.createDatabase(TEST_DB_NAME, /* version= */ 1);
-    // Insert test data
-    insertTrustedDevice(db);
-    // Prepare for the next version
-    db.close();
+    try (SupportSQLiteDatabase db = helper.createDatabase(DATABASE_NAME, startingVersion)) {
+      insertTrustedDevice(db, startingVersion);
+    }
 
-    // Re-open the database with version 3 and provide MIGRATION_1_2 and
-    // MIGRATION_2_3 as the migration process.
-    helper.runMigrationsAndValidate(
-        TEST_DB_NAME, /* version= */ 3, /* validateDroppedTables= */ true, ALL_MIGRATIONS);
-
-    // MigrationTestHelper automatically verifies the schema changes
     TrustedDeviceEntity dbEntity =
         getMigratedRoomDatabase().trustedDeviceDao().getTrustedDevice(DEFAULT_DEVICE_ID);
-
     TrustedDeviceEntity expectedDevice =
         new TrustedDeviceEntity(
-            DEFAULT_DEVICE_ID, DEFAULT_USER_ID, DEFAULT_HANDLE, DEFAULT_IS_VALID);
+            DEFAULT_DEVICE_ID, DEFAULT_USER_ID, DEFAULT_HANDLE, /* isValid= */ false);
     assertThat(dbEntity).isEqualTo(expectedDevice);
   }
 
   @Test
-  public void testMigrate2To3() throws IOException {
+  public void testMigrate2To4() throws IOException {
+    int startingVersion = 2;
     // Create the database in version 2
-    SupportSQLiteDatabase db = helper.createDatabase(TEST_DB_NAME, /* version= */ 2);
-    // Insert test data
-    insertTrustedDevice(db);
-    // Prepare for the next version
-    db.close();
+    try (SupportSQLiteDatabase db = helper.createDatabase(DATABASE_NAME, startingVersion)) {
+      insertTrustedDevice(db, startingVersion);
+    }
 
-    // Re-open the database with version 3 and provide MIGRATION_1_2 and
-    // MIGRATION_2_3 as the migration process.
-    helper.runMigrationsAndValidate(
-        TEST_DB_NAME, /* version= */ 3, /* validateDroppedTables= */ true, ALL_MIGRATIONS);
-
-    // MigrationTestHelper automatically verifies the schema changes
     TrustedDeviceEntity dbEntity =
         getMigratedRoomDatabase().trustedDeviceDao().getTrustedDevice(DEFAULT_DEVICE_ID);
 
     TrustedDeviceEntity expectedDevice =
         new TrustedDeviceEntity(
-            DEFAULT_DEVICE_ID, DEFAULT_USER_ID, DEFAULT_HANDLE, DEFAULT_IS_VALID);
+            DEFAULT_DEVICE_ID, DEFAULT_USER_ID, DEFAULT_HANDLE, /* isValid= */ false);
     assertThat(dbEntity).isEqualTo(expectedDevice);
   }
 
+  @Test
+  public void testMigrate3To4() throws IOException {
+    int startingVersion = 3;
+    // Create the database in version 3
+    try (SupportSQLiteDatabase db = helper.createDatabase(DATABASE_NAME, startingVersion)) {
+      insertTrustedDevice(db, startingVersion);
+    }
+
+    TrustedDeviceEntity dbEntity =
+        getMigratedRoomDatabase().trustedDeviceDao().getTrustedDevice(DEFAULT_DEVICE_ID);
+
+    TrustedDeviceEntity expectedDevice =
+        new TrustedDeviceEntity(
+            DEFAULT_DEVICE_ID, DEFAULT_USER_ID, DEFAULT_HANDLE, /* isValid= */ false);
+    assertThat(dbEntity).isEqualTo(expectedDevice);
+  }
+
+  @Test
+  public void validateSchemaChanges() throws IOException {
+    int startingVersion = 1;
+    try (SupportSQLiteDatabase db = helper.createDatabase(DATABASE_NAME, startingVersion)) {
+      insertTrustedDevice(db, startingVersion);
+    }
+
+    for (int version = startingVersion + 1; version <= CURRENT_VERSION; version++) {
+      helper.runMigrationsAndValidate(
+          DATABASE_NAME, version, /* validateDroppedTables= */ true, ALL_MIGRATIONS);
+    }
+  }
+
   private TrustedDeviceDatabase getMigratedRoomDatabase() {
     TrustedDeviceDatabase database =
-        Room.databaseBuilder(
-                ApplicationProvider.getApplicationContext(),
-                TrustedDeviceDatabase.class,
-                TEST_DB_NAME)
-            .allowMainThreadQueries()
-            .setQueryExecutor(directExecutor())
-            .build();
+        new TrustedDeviceDatabaseProvider(
+                ApplicationProvider.getApplicationContext(), /* allowMainThreadQueries= */ true)
+            .database;
     // close the database and release any stream resources when the test finishes
     helper.closeWhenFinished(database);
     return database;
   }
 
-  private void insertTrustedDevice(SupportSQLiteDatabase db) {
+  private void insertTrustedDevice(SupportSQLiteDatabase db, int version) {
     ContentValues values = new ContentValues();
     values.put(TRUSTED_DEVICES_ID_COLUMN, DEFAULT_DEVICE_ID);
     values.put(TRUSTED_DEVICES_USER_ID_COLUMN, DEFAULT_USER_ID);
     values.put(TRUSTED_DEVICES_HANDLE_COLUMN, DEFAULT_HANDLE);
+    if (version >= VERSION_IS_VALID_ADDED) {
+      values.put(TRUSTED_DEVICES_IS_VALID_COLUMN, DEFAULT_IS_VALID);
+    }
     db.insert(TRUSTED_DEVICES_TABLE, SQLiteDatabase.CONFLICT_REPLACE, values);
   }
 
   private static final Migration[] ALL_MIGRATIONS =
       new Migration[] {
-        TrustedDeviceDatabaseProvider.MIGRATION_1_2, TrustedDeviceDatabaseProvider.MIGRATION_2_3
+        TrustedDeviceDatabaseProvider.MIGRATION_1_2,
+        TrustedDeviceDatabaseProvider.MIGRATION_2_3,
+        TrustedDeviceDatabaseProvider.MIGRATION_3_4
       };
 }
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ui/AssociatedDeviceDetailsTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ui/AssociatedDeviceDetailsTest.kt
new file mode 100644
index 0000000..2320b4d
--- /dev/null
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ui/AssociatedDeviceDetailsTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2021 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 com.google.android.connecteddevice.ui
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.connecteddevice.model.AssociatedDevice
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState.CONNECTED
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState.NOT_DETECTED
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+final class AssociatedDeviceDetailsTest {
+  @Test
+  fun testEquality_associatedDeviceEquals() {
+    val details1 = AssociatedDeviceDetails(createAssociatedDevice(), CONNECTED)
+    val details2 = AssociatedDeviceDetails(createAssociatedDevice(), CONNECTED)
+
+    assertThat(details1).isEqualTo(details2)
+    assertThat(details1.hashCode()).isEqualTo(details2.hashCode())
+  }
+
+  @Test
+  fun testEquality_associatedDeviceEquals_differentConnectionState() {
+    val details1 = AssociatedDeviceDetails(createAssociatedDevice(), CONNECTED)
+    val details2 = AssociatedDeviceDetails(createAssociatedDevice(), NOT_DETECTED)
+
+    assertThat(details1).isEqualTo(details2)
+    assertThat(details1.hashCode()).isEqualTo(details2.hashCode())
+  }
+
+  @Test
+  fun testEquality_associatedDeviceDifferent() {
+    val details1 = AssociatedDeviceDetails(createAssociatedDevice(), CONNECTED)
+    val details2 = AssociatedDeviceDetails(createAssociateddetails2(), CONNECTED)
+
+    assertThat(details1).isNotEqualTo(details2)
+  }
+
+  @Test
+  fun testGetters_matchAssociatedDevice() {
+    val associatedDevice = createAssociatedDevice()
+    val connectionState = CONNECTED
+    val details = AssociatedDeviceDetails(associatedDevice, connectionState)
+
+    assertThat(details.deviceId).isEqualTo(associatedDevice.deviceId)
+    assertThat(details.deviceName).isEqualTo(associatedDevice.deviceName)
+    assertThat(details.deviceAddress).isEqualTo(associatedDevice.deviceAddress)
+    assertThat(details.isConnectionEnabled).isEqualTo(associatedDevice.isConnectionEnabled)
+    assertThat(details.userId).isEqualTo(associatedDevice.userId)
+    assertThat(details.connectionState).isEqualTo(connectionState)
+  }
+
+  private companion object {
+    const val TEST_ASSOCIATED_DEVICE_ID = "test_device_id"
+    const val TEST_ASSOCIATED_DEVICE_NAME = "test_device_name"
+    const val TEST_ASSOCIATED_DEVICE_ADDRESS = "test_device_address"
+
+    const val TEST_ASSOCIATED_DEVICE_ID_2 = "test_device_id_2"
+    const val TEST_ASSOCIATED_DEVICE_NAME_2 = "test_device_name_2"
+    const val TEST_ASSOCIATED_DEVICE_ADDRESS_2 = "test_device_address_2"
+
+    fun createAssociatedDevice(): AssociatedDevice =
+      AssociatedDevice(
+        TEST_ASSOCIATED_DEVICE_ID,
+        TEST_ASSOCIATED_DEVICE_ADDRESS,
+        TEST_ASSOCIATED_DEVICE_NAME,
+        /* isConnectionEnabled= */ true
+      )
+
+    /**
+     * Returns an [AssociatedDevice] that is different from that returned by
+     * [createAssociatedDevice].
+     */
+    fun createAssociateddetails2(): AssociatedDevice =
+      AssociatedDevice(
+        TEST_ASSOCIATED_DEVICE_ID_2,
+        TEST_ASSOCIATED_DEVICE_ADDRESS_2,
+        TEST_ASSOCIATED_DEVICE_NAME_2,
+        /* isConnectionEnabled= */ true
+      )
+  }
+}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModelTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModelTest.java
index a73c2a2..e2d5c89 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModelTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModelTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.Application;
 import android.bluetooth.BluetoothAdapter;
@@ -36,11 +37,13 @@
 import com.google.android.connecteddevice.api.FakeConnector;
 import com.google.android.connecteddevice.api.IAssociationCallback;
 import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.AssociatedDeviceDetails;
 import com.google.android.connecteddevice.model.ConnectedDevice;
 import com.google.android.connecteddevice.model.OobData;
 import com.google.android.connecteddevice.model.StartAssociationResponse;
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState;
 import com.google.android.connecteddevice.ui.AssociatedDeviceViewModel.AssociationState;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
 import java.util.UUID;
 import org.junit.Before;
 import org.junit.Rule;
@@ -75,7 +78,7 @@
   private final FakeConnector fakeConnector = spy(new FakeConnector());
 
   @Mock private Observer<AssociationState> mockAssociationStateObserver;
-  @Mock private Observer<AssociatedDeviceDetails> mockDeviceDetailsObserver;
+  @Mock private Observer<List<AssociatedDeviceDetails>> mockDeviceDetailsObserver;
   @Mock private Observer<String> mockCarNameObserver;
   @Mock private Observer<String> mockPairingCodeObserver;
   @Mock private Observer<AssociatedDevice> mockRemovedDeviceObserver;
@@ -88,7 +91,11 @@
   public void setUp() throws RemoteException {
     viewModel =
         new AssociatedDeviceViewModel(
-            application, /* isSppEnabled= */ false, TEST_BLE_DEVICE_NAME_PREFIX, fakeConnector);
+            application,
+            /* isSppEnabled= */ false,
+            TEST_BLE_DEVICE_NAME_PREFIX,
+            /* isPassengerEnabled= */ false,
+            fakeConnector);
     adapter.enable();
   }
 
@@ -99,23 +106,26 @@
   }
 
   @Test
-  public void removeCurrentDevice() {
-    fakeConnector.addAssociatedDevice(createAssociatedDevice(/* isConnectionEnabled= */ true));
-    viewModel.removeCurrentDevice();
+  public void removeDevice() {
+    AssociatedDevice device = createAssociatedDevice(/* isConnectionEnabled= */ true);
+    fakeConnector.addAssociatedDevice(device);
+    viewModel.removeDevice(device);
     verify(fakeConnector).removeAssociatedDevice(eq(TEST_ASSOCIATED_DEVICE_ID));
   }
 
   @Test
-  public void toggleConnectionStatusForCurrentDevice_disableDevice() {
-    fakeConnector.addAssociatedDevice(createAssociatedDevice(/* isConnectionEnabled= */ true));
-    viewModel.toggleConnectionStatusForCurrentDevice();
+  public void toggleConnectionStatusForDevice_disableDevice() {
+    AssociatedDevice device = createAssociatedDevice(/* isConnectionEnabled= */ true);
+    fakeConnector.addAssociatedDevice(device);
+    viewModel.toggleConnectionStatusForDevice(device);
     verify(fakeConnector).disableAssociatedDeviceConnection(eq(TEST_ASSOCIATED_DEVICE_ID));
   }
 
   @Test
-  public void toggleConnectionStatusForCurrentDevice_enableDevice() {
-    fakeConnector.addAssociatedDevice(createAssociatedDevice(/* isConnectionEnabled= */ false));
-    viewModel.toggleConnectionStatusForCurrentDevice();
+  public void toggleConnectionStatusForDevice_enableDevice() {
+    AssociatedDevice device = createAssociatedDevice(/* isConnectionEnabled= */ false);
+    fakeConnector.addAssociatedDevice(device);
+    viewModel.toggleConnectionStatusForDevice(device);
     verify(fakeConnector).enableAssociatedDeviceConnection(eq(TEST_ASSOCIATED_DEVICE_ID));
   }
 
@@ -201,12 +211,72 @@
   }
 
   @Test
-  public void retrieveAssociatedDevice() {
+  public void retrieveAssociatedDeviceForDriver() {
     AssociatedDevice testDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
     fakeConnector.addAssociatedDevice(testDevice);
-    viewModel.getCurrentDeviceDetails().observeForever(mockDeviceDetailsObserver);
-    assertThat(viewModel.getCurrentDeviceDetails().getValue().getAssociatedDevice())
-        .isEqualTo(testDevice);
+
+    AssociatedDeviceDetails expectedDetails =
+        new AssociatedDeviceDetails(testDevice, ConnectionState.NOT_DETECTED);
+    viewModel.getAssociatedDevicesDetails().observeForever(mockDeviceDetailsObserver);
+    assertThat(viewModel.getAssociatedDevicesDetails().getValue()).containsExactly(expectedDetails);
+  }
+
+  @Test
+  public void retrieveAssociatedDeviceForDriver_onConnection() {
+    // Adding devices before the creation of the ViewModel since the connector is connected
+    // upon construction.
+    AssociatedDevice driverDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
+    fakeConnector.addAssociatedDevice(driverDevice);
+    fakeConnector.claimAssociatedDevice(driverDevice.getDeviceId());
+
+    AssociatedDevice nonClaimedDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
+    fakeConnector.addAssociatedDevice(nonClaimedDevice);
+
+    viewModel =
+        new AssociatedDeviceViewModel(
+            application,
+            /* isSppEnabled= */ false,
+            TEST_BLE_DEVICE_NAME_PREFIX,
+            /* isPassengerEnabled= */ false,
+            fakeConnector);
+    adapter.enable();
+
+    AssociatedDeviceDetails expectedDetails =
+        new AssociatedDeviceDetails(driverDevice, ConnectionState.NOT_DETECTED);
+    viewModel.getAssociatedDevicesDetails().observeForever(mockDeviceDetailsObserver);
+
+    // Only the driver device should show up with passenger mode disabled.
+    assertThat(viewModel.getAssociatedDevicesDetails().getValue()).containsExactly(expectedDetails);
+  }
+
+  @Test
+  public void retrieveAssociatedDevices_onConnection_passengerEnabled() {
+    // Adding devices before the creation of the ViewModel since the connector is connected
+    // upon construction.
+    AssociatedDevice driverDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
+    fakeConnector.addAssociatedDevice(driverDevice);
+    fakeConnector.claimAssociatedDevice(driverDevice.getDeviceId());
+
+    AssociatedDevice nonClaimedDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
+    fakeConnector.addAssociatedDevice(nonClaimedDevice);
+
+    viewModel =
+        new AssociatedDeviceViewModel(
+            application,
+            /* isSppEnabled= */ false,
+            TEST_BLE_DEVICE_NAME_PREFIX,
+            /* isPassengerEnabled= */ true,
+            fakeConnector);
+    adapter.enable();
+
+    AssociatedDeviceDetails driverDetails =
+        new AssociatedDeviceDetails(driverDevice, ConnectionState.NOT_DETECTED);
+    AssociatedDeviceDetails passengerDetails =
+        new AssociatedDeviceDetails(nonClaimedDevice, ConnectionState.NOT_DETECTED);
+
+    viewModel.getAssociatedDevicesDetails().observeForever(mockDeviceDetailsObserver);
+    assertThat(viewModel.getAssociatedDevicesDetails().getValue()).containsExactly(
+        driverDetails, passengerDetails);
   }
 
   @Test
@@ -219,17 +289,20 @@
             TEST_ASSOCIATED_DEVICE_ADDRESS,
             TEST_ASSOCIATED_DEVICE_NAME_2,
             /* isConnectionEnabled= */ true);
+
+    AssociatedDeviceDetails expectedDetails =
+        new AssociatedDeviceDetails(updatedDevice, ConnectionState.NOT_DETECTED);
+
     fakeConnector.getCallback().onAssociatedDeviceUpdated(updatedDevice);
-    viewModel.getCurrentDeviceDetails().observeForever(mockDeviceDetailsObserver);
-    assertThat(viewModel.getCurrentDeviceDetails().getValue().getDeviceName())
-        .isEqualTo(TEST_ASSOCIATED_DEVICE_NAME_2);
+    viewModel.getAssociatedDevicesDetails().observeForever(mockDeviceDetailsObserver);
+    assertThat(viewModel.getAssociatedDevicesDetails().getValue()).containsExactly(expectedDetails);
   }
 
   @Test
   public void removeAssociatedDevice() {
     AssociatedDevice testDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
     fakeConnector.addAssociatedDevice(testDevice);
-    viewModel.removeCurrentDevice();
+    viewModel.removeDevice(testDevice);
     verify(fakeConnector).removeAssociatedDevice(eq(TEST_ASSOCIATED_DEVICE_ID));
   }
 
@@ -245,28 +318,49 @@
   }
 
   @Test
-  public void associatedDeviceConnected() {
+  public void onDeviceConnected_updateConnectionStatusToDetected() {
     AssociatedDevice testAssociatedDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
     fakeConnector.addAssociatedDevice(testAssociatedDevice);
-    ConnectedDevice testConnectedDevice = createConnectedDevice();
+    ConnectedDevice testConnectedDevice = createDetectedDevice();
+    when(fakeConnector.getConnectedDevices()).thenReturn(ImmutableList.of(testConnectedDevice));
 
     fakeConnector.getCallback().onDeviceConnected(testConnectedDevice);
 
-    viewModel.getCurrentDeviceDetails().observeForever(mockDeviceDetailsObserver);
-    assertThat(viewModel.getCurrentDeviceDetails().getValue().isConnected()).isTrue();
+    AssociatedDeviceDetails expectedDetails =
+        new AssociatedDeviceDetails(testAssociatedDevice, ConnectionState.DETECTED);
+
+    viewModel.getAssociatedDevicesDetails().observeForever(mockDeviceDetailsObserver);
+    assertThat(viewModel.getAssociatedDevicesDetails().getValue()).containsExactly(expectedDetails);
   }
 
   @Test
-  public void associatedDeviceDisconnected() {
+  public void onSecureChannelEstablished_updatesConnectionStatusToConnected() {
+    AssociatedDevice testAssociatedDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
+    fakeConnector.addAssociatedDevice(testAssociatedDevice);
+    ConnectedDevice testConnectedDevice = createConnectedDevice();
+    when(fakeConnector.getConnectedDevices()).thenReturn(ImmutableList.of(testConnectedDevice));
+
+    fakeConnector.getCallback().onSecureChannelEstablished(testConnectedDevice);
+
+    AssociatedDeviceDetails expectedDetails =
+        new AssociatedDeviceDetails(testAssociatedDevice, ConnectionState.CONNECTED);
+    viewModel.getAssociatedDevicesDetails().observeForever(mockDeviceDetailsObserver);
+    assertThat(viewModel.getAssociatedDevicesDetails().getValue()).containsExactly(expectedDetails);
+  }
+
+  @Test
+  public void onDeviceDisconnected_updatesConnectionStatusToNotDetected() {
     AssociatedDevice testAssociatedDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
     fakeConnector.addAssociatedDevice(testAssociatedDevice);
     ConnectedDevice testConnectedDevice = createConnectedDevice();
 
-    fakeConnector.getCallback().onDeviceConnected(testConnectedDevice);
     fakeConnector.getCallback().onDeviceDisconnected(testConnectedDevice);
 
-    viewModel.getCurrentDeviceDetails().observeForever(mockDeviceDetailsObserver);
-    assertThat(viewModel.getCurrentDeviceDetails().getValue().isConnected()).isFalse();
+    AssociatedDeviceDetails expectedDetails =
+        new AssociatedDeviceDetails(testAssociatedDevice, ConnectionState.NOT_DETECTED);
+
+    viewModel.getAssociatedDevicesDetails().observeForever(mockDeviceDetailsObserver);
+    assertThat(viewModel.getAssociatedDevicesDetails().getValue()).containsExactly(expectedDetails);
   }
 
   @Test
@@ -275,10 +369,31 @@
         new AssociatedDeviceViewModel(
             ApplicationProvider.getApplicationContext(),
             /* isSppEnabled= */ false,
-            TEST_BLE_DEVICE_NAME_PREFIX);
+            TEST_BLE_DEVICE_NAME_PREFIX,
+            /* isPassengerEnabled= */ false);
     associatedDeviceViewModel.onCleared();
   }
 
+  @Test
+  public void claimDevice_claimsAssociatedDevice() {
+    AssociatedDevice device = createAssociatedDevice(/* isConnectionEnabled= */ true);
+    fakeConnector.addAssociatedDevice(device);
+
+    viewModel.claimDevice(device);
+
+    verify(fakeConnector).claimAssociatedDevice(device.getDeviceId());
+  }
+
+  @Test
+  public void removeClaimOnDevice_removesAssociatedDeviceClaim() {
+    AssociatedDevice device = createAssociatedDevice(/* isConnectionEnabled= */ true);
+    fakeConnector.addAssociatedDevice(device);
+
+    viewModel.removeClaimOnDevice(device);
+
+    verify(fakeConnector).removeAssociatedDeviceClaim(device.getDeviceId());
+  }
+
   private void captureAssociationCallback() {
     ArgumentCaptor<IAssociationCallback> associationCallbackCaptor =
         ArgumentCaptor.forClass(IAssociationCallback.class);
@@ -298,7 +413,15 @@
     return new ConnectedDevice(
         TEST_ASSOCIATED_DEVICE_ID,
         TEST_ASSOCIATED_DEVICE_NAME_1,
-        /* belongsToActiveUser= */ true,
+        /* belongsToDriver= */ true,
         /* hasSecureChannel= */ true);
   }
+
+  private static ConnectedDevice createDetectedDevice() {
+    return new ConnectedDevice(
+        TEST_ASSOCIATED_DEVICE_ID,
+        TEST_ASSOCIATED_DEVICE_NAME_1,
+        /* belongsToDriver= */ true,
+        /* hasSecureChannel= */ false);
+  }
 }