Project import generated by Copybara

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

Release-Id: aae-companiondevice-android_20230424.01_RC00
Change-Id: I45e2e5250d74df34caa3e0a6123bf85d11d222d7
diff --git a/companiondevice/AndroidManifest.xml b/companiondevice/AndroidManifest.xml
index a3796cb..4e921f9 100644
--- a/companiondevice/AndroidManifest.xml
+++ b/companiondevice/AndroidManifest.xml
@@ -223,8 +223,6 @@
                 android:resource="@string/trusted_device_notification_title"/>
             <meta-data android:name="com.google.android.connecteddevice.trust.enrollment_notification_content"
                 android:resource="@string/trusted_device_notification_content"/>
-            <meta-data android:name="com.google.android.connecteddevice.trust.enrollment_notification_color"
-                android:resource="@color/car_red_300"/>
         </service>
         <activity
             android:name=".trust.TrustedDeviceActivity"
diff --git a/companiondevice/build.gradle b/companiondevice/build.gradle
index e87477e..c48dc67 100644
--- a/companiondevice/build.gradle
+++ b/companiondevice/build.gradle
@@ -14,7 +14,7 @@
         applicationId "com.google.android.companiondevicesupport"
         minSdkVersion 29
         targetSdkVersion 33
-        versionCode 1981
+        versionCode 2077
         versionName "1.0"
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
diff --git a/companiondevice/res/layout-w1240dp-land/suw_companion_landing_fragment.xml b/companiondevice/res/layout-w1240dp-land/suw_companion_landing_fragment.xml
deleted file mode 100644
index 44ee88d..0000000
--- a/companiondevice/res/layout-w1240dp-land/suw_companion_landing_fragment.xml
+++ /dev/null
@@ -1,71 +0,0 @@
-<?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.
-  -->
-<LinearLayout 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"
-    android:orientation="horizontal"
-    android:layout_marginHorizontal="?attr/pageMarginHorizontal">
-  <ScrollView
-      android:layout_width="0dp"
-      android:layout_height="match_parent"
-      android:layout_weight="@integer/suw_title_column_weight">
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="vertical"
-        android:paddingVertical="?attr/pageMarginVertical"
-        android:layout_marginEnd="@dimen/suw_column_inner_padding_horizontal">
-      <ImageView
-          android:id="@+id/device_icon"
-          android:layout_width="?attr/companionPrimaryIconSize"
-          android:layout_height="?attr/companionPrimaryIconSize"
-          android:src="@drawable/ic_smartphone_24dp"
-          android:tint="?attr/companionColorAccent"
-          tools:ignore="ContentDescription" />
-      <TextView
-          android:id="@+id/add_associated_device_title"
-          android:text="@string/add_associated_device_title"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:layout_marginVertical="?attr/contentMarginVertical"
-          style="@style/CompanionTitleTextStyle" />
-      <TextView
-          android:id="@+id/add_associated_device_subtitle"
-          android:text="@string/add_associated_device_subtitle"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          style="@style/CompanionSubtitleTextStyle" />
-      <View
-          android:layout_marginTop="?attr/companionDividerMargin"
-          style="@style/HorizontalDividerStyle" />
-      <include layout="@layout/add_associated_device_button"/>
-      <View style="@style/HorizontalDividerStyle" />
-    </LinearLayout>
-  </ScrollView>
-  <View style="@style/SuwVerticalDividerStyle"/>
-  <ScrollView
-      android:layout_width="0dp"
-      android:layout_height="match_parent"
-      android:layout_weight="@integer/suw_content_column_weight">
-    <include layout="@layout/association_instructions_vertical"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginVertical="?attr/pageMarginVertical"
-        android:layout_marginStart="@dimen/suw_column_inner_padding_horizontal" />
-  </ScrollView>
-</LinearLayout>
diff --git a/companiondevice/res/layout-w1240dp-land/suw_companion_setup_profile_fragment.xml b/companiondevice/res/layout-w1240dp-land/suw_companion_setup_profile_fragment.xml
index ebb110e..a333398 100644
--- a/companiondevice/res/layout-w1240dp-land/suw_companion_setup_profile_fragment.xml
+++ b/companiondevice/res/layout-w1240dp-land/suw_companion_setup_profile_fragment.xml
@@ -28,6 +28,14 @@
         android:layout_weight="@integer/suw_title_column_weight"
         android:paddingVertical="?attr/pageMarginVertical"
         android:layout_marginEnd="@dimen/suw_column_inner_padding_horizontal">
+      <ImageView
+        android:id="@+id/device_icon"
+        android:layout_width="?attr/companionPrimaryIconSize"
+        android:layout_height="?attr/companionPrimaryIconSize"
+        android:gravity="center"
+        android:src="@drawable/ic_smartphone_24dp"
+        android:tint="?attr/companionColorAccent"
+        tools:ignore="ContentDescription" />
       <TextView
           android:id="@+id/suw_setup_profile_title"
           android:text="@string/suw_setup_profile_title"
@@ -44,10 +52,9 @@
           android:gravity="left"
           style="@style/CompanionSubtitleTextStyle" />
     </LinearLayout>
-    <View style="@style/SuwVerticalDividerStyle"/>
     <LinearLayout
         android:layout_width="0dp"
-        android:layout_height="wrap_content"
+        android:layout_height="match_parent"
         android:orientation="vertical"
         android:gravity="center"
         android:layout_weight="@integer/suw_content_column_weight"
@@ -61,7 +68,7 @@
       <TextView
           android:id="@+id/connect_to_car_instruction"
           android:text="@string/suw_qr_instruction_text"
-          android:layout_width="match_parent"
+          android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_marginVertical="?attr/contentMarginVertical"
           android:gravity="center"
diff --git a/companiondevice/res/layout-w1240dp-land/suw_confirm_code_fragment.xml b/companiondevice/res/layout-w1240dp-land/suw_confirm_code_fragment.xml
index 9272112..e6f32a0 100644
--- a/companiondevice/res/layout-w1240dp-land/suw_confirm_code_fragment.xml
+++ b/companiondevice/res/layout-w1240dp-land/suw_confirm_code_fragment.xml
@@ -52,7 +52,6 @@
           style="@style/CompanionSubtitleTextStyle" />
     </LinearLayout>
   </ScrollView>
-  <View style="@style/SuwVerticalDividerStyle"/>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="0dp"
diff --git a/companiondevice/res/layout/companion_landing_fragment.xml b/companiondevice/res/layout/companion_landing_fragment.xml
deleted file mode 100644
index c79058f..0000000
--- a/companiondevice/res/layout/companion_landing_fragment.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-<?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.
-  -->
-
-<ScrollView 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"
-    android:fillViewport="true">
-  <LinearLayout
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      android:orientation="vertical"
-      android:gravity="center_horizontal"
-      android:paddingVertical="?attr/pageMarginVertical"
-      android:layout_marginHorizontal="?attr/pageMarginHorizontal">
-    <ImageView
-        android:id="@+id/device_icon"
-        android:layout_width="?attr/companionPrimaryIconSize"
-        android:layout_height="?attr/companionPrimaryIconSize"
-        android:gravity="center"
-        android:src="@drawable/ic_smartphone_24dp"
-        android:tint="?attr/companionColorAccent"
-        tools:ignore="ContentDescription" />
-    <TextView
-        android:id="@+id/add_associated_device_title"
-        android:text="@string/add_associated_device_title"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:gravity="center"
-        android:layout_marginVertical="?attr/contentMarginVertical"
-        style="@style/CompanionTitleTextStyle" />
-    <TextView
-        android:id="@+id/add_associated_device_subtitle"
-        android:text="@string/add_associated_device_subtitle"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:gravity="center"
-        style="@style/CompanionSubtitleTextStyle" />
-    <View
-        android:layout_marginTop="?attr/companionDividerMargin"
-        style="@style/HorizontalDividerStyle" />
-    <include layout="@layout/add_associated_device_button"/>
-    <View
-        android:layout_marginBottom="?attr/companionDividerMargin"
-        style="@style/HorizontalDividerStyle" />
-    <include layout="@layout/association_instructions_horizontal"/>
-  </LinearLayout>
-</ScrollView>
diff --git a/companiondevice/res/layout/suw_companion_landing_fragment.xml b/companiondevice/res/layout/suw_companion_landing_fragment.xml
deleted file mode 100644
index e110246..0000000
--- a/companiondevice/res/layout/suw_companion_landing_fragment.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?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"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent" >
-  <include layout="@layout/companion_landing_fragment" />
-</FrameLayout>
diff --git a/companiondevice/res/layout/suw_companion_setup_profile_fragment.xml b/companiondevice/res/layout/suw_companion_setup_profile_fragment.xml
index a1a12d2..20d03ad 100644
--- a/companiondevice/res/layout/suw_companion_setup_profile_fragment.xml
+++ b/companiondevice/res/layout/suw_companion_setup_profile_fragment.xml
@@ -27,6 +27,14 @@
       android:gravity="center_horizontal"
       android:paddingVertical="?attr/pageMarginVertical"
       android:layout_marginHorizontal="?attr/pageMarginHorizontal">
+    <ImageView
+        android:id="@+id/device_icon"
+        android:layout_width="?attr/companionPrimaryIconSize"
+        android:layout_height="?attr/companionPrimaryIconSize"
+        android:gravity="center"
+        android:src="@drawable/ic_smartphone_24dp"
+        android:tint="?attr/companionColorAccent"
+        tools:ignore="ContentDescription" />
     <TextView
         android:id="@+id/suw_setup_profile_title"
         android:text="@string/suw_setup_profile_title"
@@ -42,9 +50,6 @@
         android:layout_height="wrap_content"
         android:gravity="center"
         style="@style/CompanionSubtitleTextStyle" />
-    <View
-        android:layout_marginBottom="?attr/companionDividerMargin"
-        style="@style/HorizontalDividerStyle" />
     <ImageView
         android:id="@+id/qr_code"
         android:layout_width="@dimen/qr_code_size"
diff --git a/companiondevice/res/values/colors.xml b/companiondevice/res/values/colors.xml
index 14520d0..26fe15f 100644
--- a/companiondevice/res/values/colors.xml
+++ b/companiondevice/res/values/colors.xml
@@ -21,7 +21,6 @@
     <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>
 
     <!-- Settings -->
diff --git a/companiondevice/res/values/config.xml b/companiondevice/res/values/config.xml
index 9a9c95c..5b089c2 100644
--- a/companiondevice/res/values/config.xml
+++ b/companiondevice/res/values/config.xml
@@ -54,6 +54,5 @@
     </string-array>
 
     <bool name="enable_proxy">false</bool>
-    <bool name="enable_qr_code">true</bool>
     <bool name="enable_passenger">false</bool>
 </resources>
diff --git a/companiondevice/res/values/strings.xml b/companiondevice/res/values/strings.xml
index 2044a5a..44921fd 100644
--- a/companiondevice/res/values/strings.xml
+++ b/companiondevice/res/values/strings.xml
@@ -39,7 +39,7 @@
     <!-- Instruction for connecting to car [CHAR LIMIT=100] -->
     <string name="connect_to_targe_car_instruction_text">Connect to &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>
     <!-- Instruction for scanning QR code [CHAR LIMIT=150] -->
-    <string name="qr_instruction_text">Use your phone to scan the QR code or open MyCompanion to connect to &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>
+    <string name="qr_instruction_text">Use your phone to scan the QR code or open MyCompanion to connect to &lt;b><xliff:g id="advertised_car_name" example="Vehicle 0000">%1$s</xliff:g>&lt;/b></string>
     <!-- Instruction for connecting to car [CHAR LIMIT=100] -->
     <string name="connect_to_car_instruction_text">Connect to the&#160;car</string>
     <!-- Title for confirm pairing code fragment [CHAR LIMIT=60]-->
@@ -207,12 +207,12 @@
     <!-- QR code uri path. -->
     <string name="uri_path"  translatable="false">associate</string>
 
-    <!-- Car SUW setup profile page title. [CHAR LIMIT=40]-->
-    <string name="suw_setup_profile_title">Setup profile with phone</string>
+    <!-- Car SUW setup profile page title. [CHAR LIMIT=100]-->
+    <string name="suw_setup_profile_title">Finish setup on your phone or car</string>
     <!-- Car SUW setup profile page content. [CHAR LIMIT=200]-->
-    <string name="suw_setup_profile_content">Scan QR code to get started. If you don\'t have a MyCompanion app, you can download it on Google Play and the App Store.</string>
+    <string name="suw_setup_profile_content">If you\’ve already started set up on your phone, open your Companion App and scan the QR code.\n\nOr, you can finish setting up your profile in your car.</string>
     <!-- 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>
+    <string name="suw_qr_instruction_text">Scan to connect to &lt;b><xliff:g id="advertised_car_name" example="Vehicle 0000">%1$s</xliff:g>&lt;/b></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] -->
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDeviceDetailFragment.java b/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDeviceDetailFragment.java
index 754a491..5a1619d 100644
--- a/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDeviceDetailFragment.java
+++ b/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDeviceDetailFragment.java
@@ -169,17 +169,16 @@
 
   private void setDisabledConnectionStatus(AssociatedDeviceDetails deviceDetails) {
     if (deviceDetails.getConnectionState() == ConnectionState.CONNECTED) {
-      // The connection status will remain connected during the disconnecting process until the
-      // device get disconnected, which means when the connection state is
-      // {@code ConnectionState.DISCONNECTED}.
+      // The connection status will changed to disconnecting until the device get disconnected.
       setConnectionStatus(
-          ContextCompat.getColor(context, R.color.connection_color_connected),
-          getString(R.string.connected),
-          ContextCompat.getDrawable(context, R.drawable.ic_phonelink_erase_24dp),
-          getString(R.string.disconnecting));
+          ContextCompat.getColor(context, R.color.connection_color_disconnected),
+          getString(R.string.disconnecting),
+          ContextCompat.getDrawable(context, R.drawable.ic_phonelink_ring_24dp),
+          getString(R.string.connect));
       // Disable the button to avoid user action interrupting the process. The button will be
       // re-enabled when the connection state is updated.
       this.connectionButton.setEnabled(false);
+      this.connectionButton.setAlpha(.46f);
     } else {
       setConnectionStatus(
           ContextCompat.getColor(context, R.color.connection_color_disconnected),
@@ -202,6 +201,10 @@
           getString(R.string.detected),
           ContextCompat.getDrawable(context, R.drawable.ic_phonelink_erase_24dp),
           getString(R.string.disconnect));
+      // Disable the button to avoid user action interrupting the process. The button will be
+      // re-enabled when the connection state is updated.
+      this.connectionButton.setEnabled(false);
+      this.connectionButton.setAlpha(.46f);
     } else {
       setConnectionStatus(
           ContextCompat.getColor(context, R.color.connection_color_not_detected),
@@ -217,6 +220,7 @@
       Drawable connectionIcon,
       String connectionText) {
     this.connectionButton.setEnabled(true);
+    this.connectionButton.setAlpha(1.0f);
     this.connectionStatusText.setText(connectionStatusText);
     connectionStatusIndicator.setColorFilter(connectionStatusColor);
     this.connectionText.setText(connectionText);
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/AssociationBaseActivity.java b/companiondevice/src/com/google/android/companiondevicesupport/AssociationBaseActivity.java
index ddb63a0..22d256b 100644
--- a/companiondevice/src/com/google/android/companiondevicesupport/AssociationBaseActivity.java
+++ b/companiondevice/src/com/google/android/companiondevicesupport/AssociationBaseActivity.java
@@ -160,9 +160,7 @@
   @Override
   protected void onStop() {
     super.onStop();
-    // Resets the UI/model when activity goes into the background during association.
     model.stopAssociation();
-    finish();
   }
 
   /** Companion activity is not supported under guest profile. */
@@ -256,7 +254,7 @@
                   runOnUiThread(
                       () ->
                           Toast.makeText(
-                                  getApplicationContext(),
+                                  this,
                                   getString(R.string.continue_setup_toast_text),
                                   Toast.LENGTH_SHORT)
                               .show());
@@ -451,24 +449,7 @@
 
   private void showCompanionLandingFragment() {
     maybeClearDetailsFragmentFromBackstack();
-
-    if (getResources().getBoolean(R.bool.enable_qr_code)) {
-      logd(TAG, "Showing LandingFragment with QR code.");
-      showCompanionQrCodeLandingFragment();
-      return;
-    }
-    CompanionLandingFragment fragment =
-        (CompanionLandingFragment)
-            getSupportFragmentManager().findFragmentByTag(COMPANION_LANDING_FRAGMENT_TAG);
-    if (fragment != null && fragment.isVisible()) {
-      return;
-    }
-    dismissButtons();
-    fragment = CompanionLandingFragment.newInstance(isStartedForSuw);
-    launchFragment(fragment, COMPANION_LANDING_FRAGMENT_TAG);
-  }
-
-  private void showCompanionQrCodeLandingFragment() {
+    logd(TAG, "Showing LandingFragment with QR code.");
     CompanionQrCodeLandingFragment fragment =
         (CompanionQrCodeLandingFragment)
             getSupportFragmentManager().findFragmentByTag(COMPANION_LANDING_FRAGMENT_TAG);
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/CompanionLandingFragment.java b/companiondevice/src/com/google/android/companiondevicesupport/CompanionLandingFragment.java
deleted file mode 100644
index 2d21bb3..0000000
--- a/companiondevice/src/com/google/android/companiondevicesupport/CompanionLandingFragment.java
+++ /dev/null
@@ -1,108 +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.companiondevicesupport;
-
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import android.bluetooth.BluetoothAdapter;
-import android.os.Bundle;
-import androidx.fragment.app.Fragment;
-import android.text.Html;
-import android.text.Spanned;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-import androidx.annotation.LayoutRes;
-import androidx.lifecycle.ViewModelProvider;
-import com.google.android.connecteddevice.model.TransportProtocols;
-import com.google.android.connecteddevice.ui.AssociatedDeviceViewModel;
-import com.google.android.connecteddevice.ui.AssociatedDeviceViewModelFactory;
-import java.util.Arrays;
-import java.util.List;
-
-/** Fragment that provides association instructions. */
-public class CompanionLandingFragment extends Fragment {
-  private static final String IS_STARTED_FOR_SUW_KEY = "isStartedForSuw";
-  private static final String TAG = "CompanionLandingFragment";
-
-  /**
-   * Creates a new instance of {@link CompanionLandingFragment}.
-   *
-   * @param isStartedForSUW If the fragment is created for car setup wizard.
-   * @return {@link CompanionLandingFragment} instance.
-   */
-  static CompanionLandingFragment newInstance(boolean isStartedForSUW) {
-    Bundle bundle = new Bundle();
-    bundle.putBoolean(IS_STARTED_FOR_SUW_KEY, isStartedForSUW);
-    CompanionLandingFragment fragment = new CompanionLandingFragment();
-    fragment.setArguments(bundle);
-    return fragment;
-  }
-
-  @Override
-  public View onCreateView(
-      LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-    @LayoutRes
-    int layout =
-        getArguments().getBoolean(IS_STARTED_FOR_SUW_KEY)
-            ? R.layout.suw_companion_landing_fragment
-            : R.layout.companion_landing_fragment;
-    return inflater.inflate(layout, container, false);
-  }
-
-  @Override
-  public void onViewCreated(View view, Bundle bundle) {
-    List<String> transportProtocols =
-        Arrays.asList(getResources().getStringArray(R.array.transport_protocols));
-    AssociatedDeviceViewModel model =
-        new ViewModelProvider(
-                requireActivity(),
-                new AssociatedDeviceViewModelFactory(
-                    requireActivity().getApplication(),
-                    transportProtocols.contains(TransportProtocols.PROTOCOL_SPP),
-                    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
-        .getAdvertisedCarName()
-        .observe(/* owner= */ this, carName -> setCarName(connectToCarTextView, carName));
-    view.findViewById(R.id.add_associated_device_button)
-        .setOnClickListener(l -> ((AssociationBaseActivity) getActivity()).startAssociation());
-  }
-
-  private void setCarName(TextView textView, String carName) {
-    if (textView == null) {
-      loge(TAG, "No valid TextView to show device name.");
-      return;
-    }
-    if (carName == null) {
-      return;
-    }
-    if (!carName.isEmpty()) {
-      // Embedded BLE name inside the parenthesis indicating it is just another representatives of
-      // the device.
-      carName = "(" + carName + ")";
-    }
-    String bluetoothName = BluetoothAdapter.getDefaultAdapter().getName();
-    String connectToCarText =
-        getString(R.string.connect_to_targe_car_instruction_text, bluetoothName, carName);
-    Spanned styledConnectToCarText = Html.fromHtml(connectToCarText, Html.FROM_HTML_MODE_LEGACY);
-    textView.setText(styledConnectToCarText);
-  }
-}
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/CompanionQrCodeLandingFragment.java b/companiondevice/src/com/google/android/companiondevicesupport/CompanionQrCodeLandingFragment.java
index ac04238..80d9a2a 100644
--- a/companiondevice/src/com/google/android/companiondevicesupport/CompanionQrCodeLandingFragment.java
+++ b/companiondevice/src/com/google/android/companiondevicesupport/CompanionQrCodeLandingFragment.java
@@ -19,7 +19,6 @@
 import static com.google.android.connecteddevice.util.SafeLog.logd;
 import static com.google.android.connecteddevice.util.SafeLog.loge;
 
-import android.bluetooth.BluetoothAdapter;
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Bundle;
@@ -127,15 +126,9 @@
     if (carName == null) {
       return;
     }
-    if (!carName.isEmpty()) {
-      // Embedded BLE name inside the parenthesis indicating it is just another representatives of
-      // the device.
-      carName = "(" + carName + ")";
-    }
-    String bluetoothName = BluetoothAdapter.getDefaultAdapter().getName();
     int textId =
         isStartedForSetupProfile ? R.string.suw_qr_instruction_text : R.string.qr_instruction_text;
-    String connectToCarText = getString(textId, bluetoothName, carName);
+    String connectToCarText = getString(textId, carName);
     Spanned styledConnectToCarText = Html.fromHtml(connectToCarText, Html.FROM_HTML_MODE_LEGACY);
     textView.setText(styledConnectToCarText);
   }
diff --git a/libs/companionprotos/src/android_build.gradle b/libs/companionprotos/src/android_build.gradle
new file mode 100644
index 0000000..2954677
--- /dev/null
+++ b/libs/companionprotos/src/android_build.gradle
@@ -0,0 +1,34 @@
+apply plugin: 'java-library'
+apply plugin: 'com.google.protobuf'
+
+sourceCompatibility = '1.8'
+
+sourceSets {
+    main {
+        proto {
+            srcDir 'src'
+        }
+    }
+}
+
+dependencies {
+    implementation "com.google.protobuf:protobuf-java:3.21.12"
+}
+
+protobuf {
+    protoc {
+        artifact = rootProject.ext.protocVersion
+    }
+    generateProtoTasks {
+        all().each { task ->
+            task.builtins {
+                java {
+                    option "lite"
+                }
+                kotlin {
+                    option "lite"
+                }
+            }
+        }
+    }
+}
diff --git a/libs/companionprotos/src/system_query.proto b/libs/companionprotos/src/system_query.proto
index 286a96e..c02cace 100644
--- a/libs/companionprotos/src/system_query.proto
+++ b/libs/companionprotos/src/system_query.proto
@@ -19,7 +19,6 @@
 package com.google.companionprotos;
 
 option java_package = "com.google.android.companionprotos";
-
 option java_multiple_files = true;
 
 // Potential system query types.
@@ -38,12 +37,21 @@
 
   // Query that returns the current role of the user.
   USER_ROLE = 3;
+
+  // Query that returns whether the companion platform on the other side
+  // (phone/IHU) supports the queried features. Field |payloads| in the
+  // SystemQuery proto will be a list of strings that each represents a
+  // feature recipient UUID.
+  IS_FEATURE_SUPPORTED = 4;
 }
 
 // Definition proto for a system query.
 message SystemQuery {
   // Type indication for this query.
   SystemQueryType type = 1;
+
+  // Payload that accompanies the query.
+  repeated bytes payloads = 2;
 }
 
 // Potential user roles.
@@ -67,3 +75,17 @@
   // The user's current role.
   SystemUserRole role = 1;
 }
+
+// Response to a feature support status query.
+message FeatureSupportResponse {
+  repeated FeatureSupportStatus statuses = 1;
+}
+
+// The support status of a feature.
+message FeatureSupportStatus {
+  // The feature that is being queried.
+  string feature_id = 1;
+
+  // Whether the feature is supported.
+  bool is_supported = 2;
+}
diff --git a/libs/connecteddevice/res/values/config.xml b/libs/connecteddevice/res/values/config.xml
index 9496359..03290c8 100644
--- a/libs/connecteddevice/res/values/config.xml
+++ b/libs/connecteddevice/res/values/config.xml
@@ -17,7 +17,7 @@
 
 <resources>
     <!-- Current version of the SDK -->
-    <string name="hu_companion_sdk_version" translatable="false">1.4.5</string>
+    <string name="hu_companion_sdk_version" translatable="false">1.4.6</string>
     <integer name="hu_companion_binder_version" translatable="false">0</integer>
     <!-- Mobile SDK of later version should be compatible with the HU SDK. -->
     <string name="compatible_min_mobile_version_android" translatable="false">1.0.0</string>
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/CompanionApiProxy.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/api/CompanionApiProxy.kt
new file mode 100644
index 0000000..98c39c0
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/CompanionApiProxy.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 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.external.ISafeOnLogRequestedListener
+import com.google.android.connecteddevice.api.SafeConnector.QueryCallback
+import android.os.IInterface
+
+/** Wrapper class for ensuring any Feature Coordinator APIs called are version-compliant. */
+interface CompanionApiProxy {
+
+  val queryCallbacks: MutableMap<Int, QueryCallback>
+
+  val queryResponseRecipients: MutableMap<Int, ParcelUuid>
+
+  val listener: ISafeOnLogRequestedListener
+
+  /**
+   * Retrieves connected devices associated with the current user.
+   *
+   * @return list of identifiers of connected devices associated with the current user, null if this
+   *   action is not supported by the on-device companion platform.
+   */
+  fun getConnectedDevices(): List<String>?
+
+  /**
+   * Sends a message to a connected device.
+   *
+   * @param deviceId Identifier for the recipient connected device.
+   * @param message Message to send to the connected device.
+   * @return true if on-device companion platform supports this action, false otherwise.
+   */
+  fun sendMessage(deviceId: String, message: ByteArray): Boolean
+
+  /**
+   * Processes log records in the logger with given identifier so it can be combined with log
+   * records from other loggers.
+   *
+   * @param loggerId of the logger.
+   * @param logRecords to process.
+   * @return true if on-device companion platform supports this action, false otherwise.
+   */
+  fun processLogRecords(loggerId: Int, logRecords: ByteArray): Boolean
+
+  /**
+   * Retrieves all associated devices.
+   *
+   * @param listener that will be notified when the associated devices are retrieved.
+   * @return true if on-device companion platform supports this action, false otherwise.
+   */
+  fun retrieveAssociatedDevices(listener: IInterface): Boolean
+
+  /**
+   * Clears internal status, no Companion events will be notified after this call.
+   */
+  fun cleanUp()
+
+  companion object {
+    /** All legacy APIs will be designated version zero. */
+    const val LEGACY_VERSION = 0
+  }
+}
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 b8a7fd2..781062c 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/api/CompanionConnector.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/CompanionConnector.kt
@@ -27,6 +27,7 @@
 import android.os.RemoteException
 import androidx.annotation.GuardedBy
 import androidx.annotation.VisibleForTesting
+import com.google.android.companionprotos.FeatureSupportResponse
 import com.google.android.companionprotos.Query
 import com.google.android.companionprotos.QueryResponse
 import com.google.android.companionprotos.SystemQuery
@@ -48,16 +49,23 @@
 import com.google.android.connecteddevice.util.Logger
 import com.google.android.connecteddevice.util.SafeLog
 import com.google.android.connecteddevice.util.aliveOrNull
+import com.google.common.util.concurrent.ListenableFuture
 import com.google.protobuf.ByteString
 import com.google.protobuf.ExtensionRegistryLite
 import com.google.protobuf.InvalidProtocolBufferException
 import java.nio.charset.StandardCharsets
 import java.time.Duration
+import java.util.UUID
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.atomic.AtomicBoolean
 import java.util.concurrent.atomic.AtomicInteger
 import java.util.concurrent.locks.ReentrantLock
 import kotlin.concurrent.withLock
+import kotlin.coroutines.resume
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.guava.future
+import kotlinx.coroutines.suspendCancellableCoroutine
 
 /**
  * Class for establishing and maintaining a connection to the companion device platform.
@@ -440,6 +448,79 @@
     sendQuerySecurely(device, request, parameters, callback)
   }
 
+  override suspend fun isFeatureSupported(device: ConnectedDevice): Boolean {
+    val queriedFeatureId = featureId?.uuid ?: return false
+    val status =
+      queryFeatureSupportStatuses(device, listOf(queriedFeatureId)).firstOrNull {
+        it.first == queriedFeatureId
+      }
+    return status?.second ?: false
+  }
+
+  override suspend fun queryFeatureSupportStatuses(
+    device: ConnectedDevice,
+    queriedFeatures: List<UUID>
+  ): List<Pair<UUID, Boolean>> {
+    val payloads =
+      queriedFeatures.map {
+        logd("Batch querying support status for feature $it.")
+        ByteString.copyFrom(it.toString().toByteArray(StandardCharsets.UTF_8))
+      }
+
+    val systemQuery =
+      SystemQuery.newBuilder().run {
+        setType(SystemQueryType.IS_FEATURE_SUPPORTED)
+        addAllPayloads(payloads)
+        build()
+      }
+
+    return suspendCancellableCoroutine<List<Pair<UUID, Boolean>>> { continuation ->
+      sendQuerySecurelyInternal(
+        device,
+        SYSTEM_FEATURE_ID,
+        systemQuery.toByteArray(),
+        parameters = null,
+        object : QueryCallback {
+          override fun onSuccess(response: ByteArray) {
+            if (response.isEmpty()) {
+              loge("Received an empty response for feature support query.")
+              continuation.resume(emptyList())
+              return
+            }
+
+            val supportResponse =
+              try {
+                FeatureSupportResponse.parseFrom(response)
+              } catch (e: InvalidProtocolBufferException) {
+                loge("Could not parse query response as proto.", e)
+                continuation.resume(emptyList())
+                return
+              }
+            val statuses =
+              supportResponse.statusesList.map { status ->
+                Pair(UUID.fromString(status.featureId), status.isSupported)
+              }
+            continuation.resume(statuses)
+          }
+
+          override fun onError(response: ByteArray) {
+            loge("Received an error response when querying for feature support.")
+            continuation.resume(emptyList())
+          }
+
+          override fun onQueryFailedToSend(isTransient: Boolean) {
+            loge("Failed to send the query for the feature support status.")
+            continuation.resume(emptyList())
+          }
+        }
+      )
+    }
+  }
+
+  override fun isFeatureSupportedFuture(device: ConnectedDevice): ListenableFuture<Boolean> {
+    return CoroutineScope(Dispatchers.Main).future { isFeatureSupported(device) }
+  }
+
   override fun sendQuerySecurely(
     device: ConnectedDevice,
     request: ByteArray,
@@ -562,7 +643,7 @@
       systemQuery.toByteArray(),
       parameters = null,
       object : QueryCallback {
-        override fun onSuccess(response: ByteArray?) {
+        override fun onSuccess(response: ByteArray) {
           if (response == null || response.isEmpty()) {
             loge("Received a null or empty response for the application name.")
             callback.onError()
@@ -573,7 +654,7 @@
           callback.onNameReceived(appName)
         }
 
-        override fun onError(response: ByteArray?) {
+        override fun onError(response: ByteArray) {
           loge("Received an error response when querying for application name.")
           callback.onError()
         }
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 0ccbbdc..d1c74a6 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/api/Connector.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/Connector.kt
@@ -22,6 +22,8 @@
 import androidx.annotation.IntDef
 import com.google.android.connecteddevice.model.AssociatedDevice
 import com.google.android.connecteddevice.model.ConnectedDevice
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.UUID
 
 /** Class for establishing and maintaining a connection to the companion device platform. */
 interface Connector {
@@ -57,6 +59,29 @@
   /** Securely sends message to a device. */
   fun sendMessageSecurely(device: ConnectedDevice, message: ByteArray)
 
+  /** Returns whether this feature is supported by the [device]. */
+  suspend fun isFeatureSupported(device: ConnectedDevice): Boolean
+
+  /**
+   * Checks whether this feature is supported by the [device].
+   *
+   * This method is added for java-compatibility; prefer the suspend version in Kotlin.
+   */
+  fun isFeatureSupportedFuture(device: ConnectedDevice): ListenableFuture<Boolean>
+
+  /**
+   * Batch queries whether the listed features are supported by the [device].
+   *
+   * Returns an empty list if there was any error during the query.
+   *
+   * Use [isFeatureSupported] instead. This API is intended for the companion platform
+   * functionality.
+   */
+  suspend fun queryFeatureSupportStatuses(
+    device: ConnectedDevice,
+    queriedFeatures: List<UUID>
+  ): List<Pair<UUID, Boolean>>
+
   /** Securely send a query to a device and registers a [QueryCallback] for a response. */
   fun sendQuerySecurely(
     deviceId: String,
@@ -105,7 +130,7 @@
   /** Stops the association process. */
   fun stopAssociation()
 
-  /** Confirms the paring code. */
+  /** Confirms the pairing code. */
   fun acceptVerification()
 
   /** Remove the associated device of the given [deviceId]. */
@@ -172,7 +197,7 @@
      * @param deviceId Id of the device the message failed to send to.
      * @param message Message to send.
      * @param isTransient `true` if cause of failure is transient and can be retried. `false` if
-     * failure is permanent.
+     *   failure is permanent.
      */
     fun onMessageFailedToSend(deviceId: String, message: ByteArray, isTransient: Boolean) {}
 
@@ -203,10 +228,10 @@
   /** Callback for a query response. */
   interface QueryCallback {
     /** Invoked with a successful response to a query. */
-    fun onSuccess(response: ByteArray?) {}
+    fun onSuccess(response: ByteArray) {}
 
     /** Invoked with an unsuccessful response to a query. */
-    fun onError(response: ByteArray?) {}
+    fun onError(response: ByteArray) {}
 
     /**
      * Invoked when a query failed to send to the device. `isTransient` is set to `true` if cause of
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/FeatureConnector.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/api/FeatureConnector.kt
new file mode 100644
index 0000000..0056128
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/FeatureConnector.kt
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2023 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.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.content.pm.PackageManager
+import android.os.Handler
+import android.os.IBinder
+import android.os.IInterface
+import android.os.Looper
+import android.os.ParcelUuid
+import android.os.RemoteException
+import androidx.annotation.VisibleForTesting
+import com.google.android.companionprotos.Query
+import com.google.android.companionprotos.QueryResponse
+import com.google.android.companionprotos.SystemQuery
+import com.google.android.companionprotos.SystemQueryType
+import com.google.android.connecteddevice.api.SafeConnector.AppNameCallback
+import com.google.android.connecteddevice.api.SafeConnector.Companion.ACTION_BIND_FEATURE_COORDINATOR
+import com.google.android.connecteddevice.api.SafeConnector.Companion.ACTION_QUERY_API_VERSION
+import com.google.android.connecteddevice.api.SafeConnector.QueryCallback
+import com.google.android.connecteddevice.api.external.ISafeBinderVersion
+import com.google.android.connecteddevice.api.external.ISafeFeatureCoordinator
+import com.google.android.connecteddevice.api.external.ISafeOnAssociatedDevicesRetrievedListener
+import com.google.android.connecteddevice.api.external.ISafeOnLogRequestedListener
+import com.google.android.connecteddevice.util.ByteUtils
+import com.google.android.connecteddevice.util.Logger
+import com.google.android.connecteddevice.util.SafeLog
+import com.google.protobuf.ByteString
+import java.nio.charset.StandardCharsets
+import java.time.Duration
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicInteger
+import java.util.concurrent.locks.ReentrantLock
+
+/**
+ * Class for establishing and maintaining a connection between external features and the companion
+ * device platform.
+ *
+ * @param context [Context] of the hosting process.
+ * @param featureId Identifier of the feature that is running this connector.
+ * @param callback Callback associated with this connector.
+ * @param minSupportedVersion External feature's minimum supported Companion API version.
+ */
+class FeatureConnector (
+  private val context: Context,
+  override val featureId: ParcelUuid,
+  override val callback: SafeConnector.Callback,
+  private val minSupportedVersion: Int = 0
+) : SafeConnector {
+
+  @VisibleForTesting internal var bindAttempts = 0
+
+  private val lock = ReentrantLock()
+
+  private val retryHandler = Handler(Looper.getMainLooper())
+
+  private val loggerId = Logger.getLogger().loggerId
+
+  private val waitingForConnection = AtomicBoolean(true)
+
+  private val queryIdGenerator = QueryIdGenerator()
+
+  override val connectedDevices: List<String>
+    get() = coordinatorProxy?.getConnectedDevices() ?: emptyList()
+
+  /** [CompanionApiProxy] acting as a wrapper for feature coordinator calls */
+  @VisibleForTesting internal var coordinatorProxy: CompanionApiProxy? = null
+
+  @VisibleForTesting internal var platformVersion: Int? = null
+
+  @VisibleForTesting
+  internal val versionCheckConnection =
+    object : ServiceConnection {
+      override fun onServiceConnected(name: ComponentName, service: IBinder) {
+        if (service !is ISafeBinderVersion) {
+          logd("Unexpected binder received from platform. Aborting.")
+          callback.onFailedToConnect()
+          return
+        } else if (minSupportedVersion > service.getVersion()) {
+          loge("Incompatible client and platform versions detected. Aborting.")
+          callback.onApiNotSupported()
+          return
+        } else {
+          platformVersion = service.getVersion()
+          bindAttempts = 0
+          bindToService(ACTION_BIND_FEATURE_COORDINATOR, featureCoordinatorConnection)
+        }
+      }
+
+      override fun onServiceDisconnected(name: ComponentName) {
+        this@FeatureConnector.onServiceDisconnected()
+      }
+
+      override fun onNullBinding(name: ComponentName) {
+        logd("Connecting to a companion platform of version 0.")
+        platformVersion = 0
+        bindAttempts = 0
+        bindToService(ACTION_BIND_FEATURE_COORDINATOR, featureCoordinatorConnection)
+      }
+
+      override fun onBindingDied(name: ComponentName?) {
+        this@FeatureConnector.onBindingDied()
+      }
+    }
+
+  @VisibleForTesting
+  internal val featureCoordinatorConnection =
+    object : ServiceConnection {
+      override fun onServiceConnected(name: ComponentName, service: IBinder) {
+        logd("Feature coordinator binder connected.")
+        val platformVersion = platformVersion
+        if (platformVersion == null) {
+          loge("Incompatible companion platform version. Aborting.")
+          return
+        }
+        coordinatorProxy =
+          when {
+            platformVersion > 0 ->
+              SafeApiProxy(
+                ISafeFeatureCoordinator.Stub.asInterface(service),
+                featureId,
+                callback,
+                loggerId,
+                platformVersion
+              )
+            platformVersion == 0 ->
+              LegacyApiProxy(
+                IFeatureCoordinator.Stub.asInterface(service),
+                featureId,
+                callback,
+                loggerId,
+                platformVersion
+              )
+            else -> {
+              loge("Incompatible companion platform version. Aborting.")
+              return
+            }
+          }
+        logd("FeatureCoordinator initialized.")
+        waitingForConnection.set(false)
+        callback.onConnected()
+      }
+
+      override fun onServiceDisconnected(name: ComponentName) {
+        this@FeatureConnector.onServiceDisconnected()
+      }
+
+      override fun onNullBinding(name: ComponentName) {
+        this@FeatureConnector.onNullBinding()
+      }
+
+      override fun onBindingDied(name: ComponentName?) {
+        this@FeatureConnector.onBindingDied()
+      }
+    }
+
+  init {
+    logd("Initiating connection to companion platform.")
+    bindToService(ACTION_QUERY_API_VERSION, versionCheckConnection)
+  }
+
+  private fun bindToService(action: String, serviceConnection: ServiceConnection) {
+    val intent = resolveIntent(action)
+
+    if (intent == null) {
+      loge("No services found supporting companion device. Aborting.")
+      callback.onFailedToConnect()
+      return
+    }
+
+    val success = context.bindService(intent, serviceConnection, /* flag= */ 0)
+    if (success) {
+      logd("Successfully started binding with ${intent.action}.")
+      return
+    }
+
+    bindAttempts++
+    if (bindAttempts > MAX_BIND_ATTEMPTS) {
+      loge("Failed to bind to service after $bindAttempts attempts. Aborting.")
+      waitingForConnection.set(false)
+      callback.onFailedToConnect()
+      return
+    }
+    retryHandler.postDelayed(
+      {
+        logw("Unable to bind to service with action ${intent.action}. Trying again.")
+        bindToService(action, serviceConnection)
+      },
+      BIND_RETRY_DURATION.toMillis()
+    )
+  }
+
+  private fun resolveIntent(action: String): Intent? {
+    val packageManager = context.packageManager
+    val intent = Intent(action)
+    val services = packageManager.queryIntentServices(intent, PackageManager.MATCH_DEFAULT_ONLY)
+    if (services.isEmpty()) {
+      logw("There are no services supporting the $action action installed on this device.")
+      return null
+    }
+    logd("Found ${services.size} service(s) supporting $action. Choosing the first one.")
+    val service = services[0]
+    return intent.apply {
+      component = ComponentName(service.serviceInfo.packageName, service.serviceInfo.name)
+    }
+  }
+
+  override fun cleanUp() {
+    logd("Disconnecting from the companion platform.")
+    coordinatorProxy?.cleanUp()
+    coordinatorProxy = null
+    unbindFromService()
+    callback.onDisconnected()
+  }
+
+  private fun unbindFromService() {
+    retryHandler.removeCallbacksAndMessages(/* token= */ null)
+    try {
+      context.unbindService(featureCoordinatorConnection)
+    } catch (e: IllegalArgumentException) {
+      logw("Attempted to unbind an already unbound service.")
+    }
+    waitingForConnection.set(false)
+  }
+
+  private fun onServiceDisconnected() {
+    logd("Service has disconnected. Cleaning up.")
+    cleanUp()
+  }
+
+  private fun onNullBinding() {
+    loge("Received a null binding for FeatureCoordinator. Unbinding service.")
+    unbindFromService()
+    callback.onFailedToConnect()
+  }
+
+  private fun onBindingDied() {
+    logw("FeatureCoordinator binding died. Unbinding service.")
+    unbindFromService()
+    callback.onDisconnected()
+  }
+
+  override fun sendMessage(deviceId: String, message: ByteArray) {
+    val coordinatorProxy = coordinatorProxy
+    if (coordinatorProxy == null) {
+      loge("Unable to send message with a null feature coordinator.")
+      callback.onMessageFailedToSend(deviceId, message, isTransient = true)
+      return
+    }
+    if (!connectedDevices.contains(deviceId)) {
+      loge("No matching device found with id $deviceId when trying to send secure message.")
+      callback.onMessageFailedToSend(deviceId, message, isTransient = false)
+      return
+    }
+    if (!coordinatorProxy.sendMessage(deviceId, message)) {
+      loge("Feature coordinator failed to send message.")
+      callback.onMessageFailedToSend(deviceId, message, isTransient = false)
+    }
+  }
+
+  override fun sendQuery(
+    deviceId: String,
+    request: ByteArray,
+    parameters: ByteArray?,
+    queryCallback: QueryCallback
+  ) {
+    val coordinatorProxy = coordinatorProxy
+    if (coordinatorProxy == null) {
+      loge("Unable to send query with a null feature coordinator.")
+      queryCallback.onQueryFailedToSend(isTransient = false)
+      return
+    }
+    if (!connectedDevices.contains(deviceId)) {
+      loge("No matching device found with id $deviceId when trying to send a query.")
+      queryCallback.onQueryFailedToSend(isTransient = false)
+      return
+    }
+    val id = queryIdGenerator.next()
+    val builder =
+      Query.newBuilder()
+        .setId(id)
+        .setSender(ByteString.copyFrom(ByteUtils.uuidToBytes(featureId.uuid)))
+        .setRequest(ByteString.copyFrom(request))
+    if (parameters != null) {
+      builder.parameters = ByteString.copyFrom(parameters)
+    }
+    logd("Sending secure query with id $id.")
+    if (!coordinatorProxy.sendMessage(deviceId, builder.build().toByteArray())) {
+      loge("Error while sending query.")
+      queryCallback.onQueryFailedToSend(isTransient = false)
+    }
+    coordinatorProxy.queryCallbacks[id] = queryCallback
+  }
+
+  override fun respondToQuery(
+    deviceId: String,
+    queryId: Int,
+    success: Boolean,
+    response: ByteArray?
+  ) {
+    val coordinatorProxy = coordinatorProxy
+    if (coordinatorProxy == null) {
+      loge("Unable to respond to query with a null feature coordinator.")
+      return
+    }
+    val recipientId = coordinatorProxy.queryResponseRecipients.remove(queryId)
+    if (recipientId == null) {
+      loge("Unable to send response to unrecognized query $queryId.")
+      return
+    }
+    val builder = QueryResponse.newBuilder().setQueryId(queryId).setSuccess(success)
+    if (response != null) {
+      builder.response = ByteString.copyFrom(response)
+    }
+    val queryResponse = builder.build()
+    logd("Sending response to query $queryId to $recipientId.")
+    if (!coordinatorProxy.sendMessage(deviceId, queryResponse.toByteArray())) {
+      loge("Feature coordinator failed to send query.")
+      callback.onMessageFailedToSend(deviceId, queryResponse.toByteArray(), isTransient = false)
+    }
+  }
+
+  override fun retrieveCompanionApplicationName(
+    deviceId: String,
+    appNameCallback: AppNameCallback
+  ) {
+    val systemQuery = SystemQuery.newBuilder().setType(SystemQueryType.APP_NAME).build()
+    sendQuery(
+      deviceId,
+      systemQuery.toByteArray(),
+      parameters = null,
+      object : QueryCallback {
+        override fun onSuccess(response: ByteArray?) {
+          if (response == null || response.isEmpty()) {
+            loge("Received a null or empty response for the application name.")
+            appNameCallback.onError()
+            return
+          }
+          val appName = String(response, StandardCharsets.UTF_8)
+          logd("Received successful app name query response of $appName.")
+          appNameCallback.onNameReceived(appName)
+        }
+
+        override fun onError(response: ByteArray?) {
+          loge("Received an error response when querying for application name.")
+          appNameCallback.onError()
+        }
+
+        override fun onQueryFailedToSend(isTransient: Boolean) {
+          loge("Failed to send the query for the application name.")
+          appNameCallback.onError()
+        }
+      }
+    )
+  }
+
+  override fun retrieveAssociatedDevices(listener: IOnAssociatedDevicesRetrievedListener) {
+    retrieveAssociatedDevicesInternal(listener)
+  }
+
+  override fun retrieveAssociatedDevices(listener: ISafeOnAssociatedDevicesRetrievedListener) {
+    retrieveAssociatedDevicesInternal(listener)
+  }
+
+  private fun retrieveAssociatedDevicesInternal(listener: IInterface) {
+    val coordinatorProxy = coordinatorProxy
+    if (coordinatorProxy == null) {
+      loge("Unable to retrieve associated devices with a null feature coordinator.")
+      return
+    }
+    if (!coordinatorProxy.retrieveAssociatedDevices(listener)) {
+      logw("Failed to retrieve associated devices.")
+    }
+  }
+
+  private fun logd(message: String) {
+    SafeLog.logd(TAG, "$message [Feature ID: $featureId]")
+  }
+
+  private fun logw(message: String) {
+    SafeLog.logw(TAG, "$message [Feature ID: $featureId]")
+  }
+
+  private fun loge(message: String, e: Exception? = null) {
+    SafeLog.loge(TAG, "$message [Feature ID: $featureId]", e)
+  }
+
+  companion object {
+    private const val TAG = "FeatureConnector"
+
+    private val BIND_RETRY_DURATION = Duration.ofSeconds(1)
+
+    @VisibleForTesting internal const val MAX_BIND_ATTEMPTS = 3
+
+    // TODO(alwa) Move this (and QueryIdGenerator in CompanionConnector) to its own internal class.
+    /** A generator of unique IDs for queries. */
+    private class QueryIdGenerator {
+      private val messageId = AtomicInteger(0)
+      fun next(): Int {
+        val current = messageId.getAndIncrement()
+        messageId.compareAndSet(Int.MAX_VALUE, 0)
+        return current
+      }
+    }
+  }
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/IFeatureCoordinator.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/api/IFeatureCoordinator.aidl
index d77cedd..590498c 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/api/IFeatureCoordinator.aidl
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/IFeatureCoordinator.aidl
@@ -85,6 +85,9 @@
     /**
      * Registers a callback for a specific connectedDevice and recipient.
      *
+     * Duplicate registration with the same [recipientId] will block the
+     * recipient and prevent it from receiving callbacks.
+     *
      * @param connectedDevice {@link ConnectedDevice} to register triggers on.
      * @param recipientId {@link ParcelUuid} to register as recipient of.
      * @param callback {@link IDeviceCallback} to register.
@@ -112,7 +115,7 @@
             in DeviceMessage message) = 9;
 
     /**
-     * Registers a callback for associated devic related events.
+     * Registers a callback for associated device related events.
      *
      * @param callback {@link IDeviceAssociationCallback} to register.
      */
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/LegacyApiProxy.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/api/LegacyApiProxy.kt
new file mode 100644
index 0000000..2705cd2
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/LegacyApiProxy.kt
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2023 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.IInterface
+import android.os.ParcelUuid
+import android.os.RemoteException
+import androidx.annotation.VisibleForTesting
+import com.google.android.companionprotos.Query
+import com.google.android.companionprotos.QueryResponse
+import com.google.android.connecteddevice.api.CompanionApiProxy.Companion.LEGACY_VERSION
+import com.google.android.connecteddevice.api.SafeConnector.QueryCallback
+import com.google.android.connecteddevice.api.external.ISafeOnLogRequestedListener
+import com.google.android.connecteddevice.model.ConnectedDevice
+import com.google.android.connecteddevice.model.DeviceMessage
+import com.google.android.connecteddevice.util.ByteUtils
+import com.google.android.connecteddevice.util.Logger
+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.InvalidProtocolBufferException
+import java.util.concurrent.ConcurrentHashMap
+import java.util.UUID
+
+/**
+ * Wrapper for FeatureCoordinator APIs used to make sure that the Companion app's version is
+ * compatible with the APIs it is attempting to call before actually making the call. This class
+ * allows for backwards compatibility with legacy Companion platform versions, and will be deprecated
+ * once all devices have Companion platform updated to version 1.
+ */
+class LegacyApiProxy (
+  private val featureCoordinator: IFeatureCoordinator,
+  private val recipientId: ParcelUuid,
+  private val connectorCallback: SafeConnector.Callback,
+  private val loggerId: Int,
+  private val platformVersion: Int
+) : CompanionApiProxy {
+
+  // queryId -> callback
+  override val queryCallbacks: MutableMap<Int, QueryCallback> = ConcurrentHashMap()
+
+  // queryId -> original sender for response
+  override val queryResponseRecipients: MutableMap<Int, ParcelUuid> = ConcurrentHashMap()
+
+  private val connectionCallback =
+    object : IConnectionCallback.Stub() {
+      override fun onDeviceConnected(device: ConnectedDevice) {
+        logd(TAG, "Device ${device.deviceId} has connected. Notifying callback.")
+        connectorCallback.onDeviceConnected(device.deviceId)
+
+        if (device.hasSecureChannel()) {
+          connectorCallback.onSecureChannelEstablished(device.deviceId)
+        }
+
+        logd(TAG, "Registering device callback for $recipientId on device ${device.deviceId}.")
+        featureCoordinator.registerDeviceCallback(device, recipientId, deviceCallback)
+      }
+
+      override fun onDeviceDisconnected(device: ConnectedDevice) {
+        logd(TAG, "Device ${device.deviceId} has disconnected. Notifying callback.")
+        connectorCallback.onDeviceDisconnected(device.deviceId)
+
+        logd(TAG, "Unregistering device callback for $recipientId on device ${device.deviceId}.")
+        featureCoordinator.unregisterDeviceCallback(device, recipientId, deviceCallback)
+      }
+    }
+
+  @VisibleForTesting
+  internal val deviceCallback =
+    object : IDeviceCallback.Stub() {
+      override fun onSecureChannelEstablished(device: ConnectedDevice) {
+        logd(TAG, "Secure channel has been established on ${device.deviceId}. Notifying callback.")
+        connectorCallback.onSecureChannelEstablished(device.deviceId)
+      }
+
+      override fun onMessageReceived(device: ConnectedDevice, message: DeviceMessage) {
+        processIncomingMessage(device.deviceId, message)
+      }
+
+      override fun onDeviceError(device: ConnectedDevice, error: Int) {
+        logw(TAG, "Received a device error of $error from ${device.deviceId}.")
+        connectorCallback.onDeviceError(device.deviceId, error)
+      }
+    }
+
+  override val listener =
+    object : ISafeOnLogRequestedListener.Stub() {
+      override fun onLogRecordsRequested() {
+        val loggerBytes = Logger.getLogger().toByteArray()
+        if (!processLogRecords(loggerId, loggerBytes)) {
+          logw(TAG, "Failed to process log records for logger $loggerId.")
+        }
+      }
+    }
+
+  init {
+    if (platformVersion >= LEGACY_VERSION) {
+      featureCoordinator.registerAllConnectionCallback(connectionCallback)
+
+      val connectedDevices = getConnectedDevices() ?: emptyList()
+      for (deviceId in connectedDevices) {
+        val connectedDevice = getConnectedDeviceById(deviceId)
+        featureCoordinator.registerDeviceCallback(connectedDevice, recipientId, deviceCallback)
+      }
+
+      featureCoordinator.registerOnLogRequestedListener(loggerId, listener)
+    } else {
+      logw(TAG, "Incompatible Companion platform version.")
+    }
+  }
+
+  override fun getConnectedDevices(): List<String>? {
+    if (platformVersion < LEGACY_VERSION) {
+      logd(TAG, "getConnectedDevices invoked by outdated Companion platform.")
+      return null
+    }
+    return featureCoordinator.getConnectedDevicesForDriver().map { it.deviceId }
+  }
+
+  override fun sendMessage(deviceId: String, message: ByteArray): Boolean {
+    if (platformVersion < LEGACY_VERSION) {
+      logd(TAG, "sendMessage invoked by outdated Companion platform.")
+      return false
+    }
+    val connectedDevice = getConnectedDeviceById(deviceId)
+    if (connectedDevice == null) {
+      logw(TAG, "No connected device found with deviceId $deviceId. Cannot send message.")
+      return false
+    }
+    val deviceMessage =
+      DeviceMessage.createOutgoingMessage(
+        UUID.fromString(deviceId),
+        /* isMessageEncrypted= */ true,
+        DeviceMessage.OperationType.CLIENT_MESSAGE,
+        message
+      )
+    return try {
+      featureCoordinator.sendMessage(connectedDevice, deviceMessage)
+    } catch (e: RemoteException) {
+      loge(TAG, "sendMessage failed with RemoteException.", e)
+      false
+    }
+  }
+
+  private fun getConnectedDeviceById(deviceId: String): ConnectedDevice? {
+    return featureCoordinator
+      .getConnectedDevicesForDriver()
+      .firstOrNull { it.deviceId == deviceId }
+  }
+
+  override fun processLogRecords(loggerId: Int, logRecords: ByteArray): Boolean {
+    if (platformVersion < LEGACY_VERSION) {
+      logd(TAG, "processLogRecords invoked by outdated Companion platform.")
+      return false
+    }
+    try {
+      featureCoordinator.processLogRecords(loggerId, logRecords)
+    } catch (e: RemoteException) {
+      loge(TAG, "Failed to send log records for logger $loggerId.", e)
+      return false
+    }
+    return true
+  }
+
+  override fun retrieveAssociatedDevices(listener: IInterface): Boolean {
+    if (listener !is IOnAssociatedDevicesRetrievedListener) {
+      logd(TAG, "retrieveAssociatedDevices invoked with incorrect callback type.")
+      return false
+    }
+    if (platformVersion < LEGACY_VERSION) {
+      logd(TAG, "retrieveAssociatedDevices invoked by outdated Companion platform.")
+      return false
+    }
+    featureCoordinator.retrieveAssociatedDevices(listener)
+    return true
+  }
+
+  private fun processIncomingMessage(deviceId: String, deviceMessage: DeviceMessage) {
+    val operationType = deviceMessage.operationType
+    val message = deviceMessage.message
+    when (operationType) {
+      DeviceMessage.OperationType.CLIENT_MESSAGE -> {
+        logd(TAG, "Received client message. Passing on to feature.")
+        connectorCallback.onMessageReceived(deviceId, message)
+        return
+      }
+      DeviceMessage.OperationType.QUERY -> {
+        try {
+          val query = Query.parseFrom(message)
+          logd(TAG, "Received a new query with id ${query.id}. Passing on to feature.")
+          val sender = ParcelUuid(ByteUtils.bytesToUUID(query.sender.toByteArray()))
+          queryResponseRecipients[query.id] = sender
+          connectorCallback.onQueryReceived(
+            deviceId,
+            query.id,
+            query.request.toByteArray(),
+            query.parameters.toByteArray()
+          )
+        } catch (e: InvalidProtocolBufferException) {
+          loge(TAG, "Unable to parse query.", e)
+        }
+        return
+      }
+      DeviceMessage.OperationType.QUERY_RESPONSE -> {
+        try {
+          val response = QueryResponse.parseFrom(message)
+          logd(TAG, "Received a query response. Issuing registered callback.")
+          val callback = queryCallbacks.remove(response.queryId)
+          if (callback == null) {
+            loge(TAG, "Unable to locate callback for query ${response.queryId}.")
+            return
+          }
+          if (response.success) {
+            callback.onSuccess(response.response.toByteArray())
+          } else {
+            callback.onError(response.response.toByteArray())
+          }
+        } catch (e: InvalidProtocolBufferException) {
+          loge(TAG, "Unable to parse query response.", e)
+        }
+        return
+      }
+      else -> loge(TAG, "Received unknown type of message: $operationType. Ignoring.")
+    }
+  }
+
+  override fun cleanUp() {
+    if (platformVersion >= LEGACY_VERSION) {
+      featureCoordinator.unregisterConnectionCallback(connectionCallback)
+
+      val connectedDevices = getConnectedDevices() ?: emptyList()
+      for (deviceId in connectedDevices) {
+        val connectedDevice = getConnectedDeviceById(deviceId)
+        featureCoordinator.unregisterDeviceCallback(connectedDevice, recipientId, deviceCallback)
+      }
+
+      featureCoordinator.unregisterOnLogRequestedListener(loggerId, listener)
+    } else {
+      logw(TAG, "Incompatible Companion platform version.")
+    }
+  }
+
+  companion object {
+    private const val TAG = "LegacyApiProxy"
+  }
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/SafeApiProxy.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/api/SafeApiProxy.kt
new file mode 100644
index 0000000..915f26f
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/SafeApiProxy.kt
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2023 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.IInterface
+import android.os.ParcelUuid
+import android.os.RemoteException
+import androidx.annotation.VisibleForTesting
+import com.google.android.companionprotos.DeviceMessageProto
+import com.google.android.companionprotos.Query
+import com.google.android.companionprotos.QueryResponse
+import com.google.android.connecteddevice.api.SafeConnector.QueryCallback
+import com.google.android.connecteddevice.api.external.ISafeConnectionCallback
+import com.google.android.connecteddevice.api.external.ISafeDeviceCallback
+import com.google.android.connecteddevice.api.external.ISafeFeatureCoordinator
+import com.google.android.connecteddevice.api.external.ISafeOnAssociatedDevicesRetrievedListener
+import com.google.android.connecteddevice.api.external.ISafeOnLogRequestedListener
+import com.google.android.connecteddevice.model.DeviceMessage
+import com.google.android.connecteddevice.util.ByteUtils
+import com.google.android.connecteddevice.util.Logger
+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.InvalidProtocolBufferException
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * Wrapper for SafeFeatureCoordinator APIs used to make sure that the Companion app's version is
+ * compatible with the APIs it is attempting to call before actually making the call.
+ */
+class SafeApiProxy (
+  private val featureCoordinator: ISafeFeatureCoordinator,
+  private val recipientId: ParcelUuid,
+  private val connectorCallback: SafeConnector.Callback,
+  private val loggerId: Int,
+  private val platformVersion: Int
+) : CompanionApiProxy {
+
+  // queryId -> callback
+  override val queryCallbacks: MutableMap<Int, QueryCallback> = ConcurrentHashMap()
+
+  // queryId -> original sender for response
+  override val queryResponseRecipients: MutableMap<Int, ParcelUuid> = ConcurrentHashMap()
+
+  private val connectionCallback =
+    object : ISafeConnectionCallback.Stub() {
+      override fun onDeviceConnected(deviceId: String) {
+        logd(TAG, "Device ${deviceId} has connected. Notifying callback.")
+        connectorCallback.onDeviceConnected(deviceId)
+
+        logd(TAG, "Registering device callback for $recipientId on device ${deviceId}.")
+        featureCoordinator.registerDeviceCallback(deviceId, recipientId, deviceCallback)
+      }
+
+      override fun onDeviceDisconnected(deviceId: String) {
+        logd(TAG, "Device ${deviceId} has disconnected. Notifying callback.")
+        connectorCallback.onDeviceDisconnected(deviceId)
+
+        logd(TAG, "Unregistering device callback for $recipientId on device ${deviceId}.")
+        featureCoordinator.unregisterDeviceCallback(deviceId, recipientId, deviceCallback)
+      }
+    }
+
+  @VisibleForTesting
+  internal val deviceCallback =
+    object : ISafeDeviceCallback.Stub() {
+      override fun onSecureChannelEstablished(deviceId: String) {
+        logd(TAG, "Secure channel has been established on ${deviceId}. Notifying callback.")
+        connectorCallback.onSecureChannelEstablished(deviceId)
+      }
+
+      override fun onMessageReceived(deviceId: String, deviceMessage: ByteArray) {
+        processIncomingMessage(deviceId, deviceMessage)
+      }
+
+      override fun onDeviceError(deviceId: String, error: Int) {
+        logw(TAG, "Received a device error of $error from ${deviceId}.")
+        connectorCallback.onDeviceError(deviceId, error)
+      }
+    }
+
+  private fun processIncomingMessage(deviceId: String, deviceMessage: ByteArray) {
+    val parsedMessage =
+      try {
+        DeviceMessageProto.Message.parseFrom(deviceMessage)
+      } catch (e: InvalidProtocolBufferException) {
+        loge(TAG, "Cannot parse device message to send.", e)
+        return
+      }
+    val operationType = DeviceMessage.OperationType.fromValue(parsedMessage.operation.number)
+    val message = parsedMessage.payload.toByteArray()
+    when (operationType) {
+      DeviceMessage.OperationType.CLIENT_MESSAGE -> {
+        logd(TAG, "Received client message. Passing on to feature.")
+        connectorCallback.onMessageReceived(deviceId, message)
+        return
+      }
+      DeviceMessage.OperationType.QUERY -> {
+        try {
+          val query = Query.parseFrom(message)
+          logd(TAG, "Received a new query with id ${query.id}. Passing on to feature.")
+          val sender = ParcelUuid(ByteUtils.bytesToUUID(query.sender.toByteArray()))
+          queryResponseRecipients[query.id] = sender
+          connectorCallback.onQueryReceived(
+            deviceId,
+            query.id,
+            query.request.toByteArray(),
+            query.parameters.toByteArray()
+          )
+        } catch (e: InvalidProtocolBufferException) {
+          loge(TAG, "Unable to parse query.", e)
+        }
+        return
+      }
+      DeviceMessage.OperationType.QUERY_RESPONSE -> {
+        try {
+          val response = QueryResponse.parseFrom(message)
+          logd(TAG, "Received a query response. Issuing registered callback.")
+          val callback = queryCallbacks.remove(response.queryId)
+          if (callback == null) {
+            loge(TAG, "Unable to locate callback for query ${response.queryId}.")
+            return
+          }
+          if (response.success) {
+            callback.onSuccess(response.response.toByteArray())
+          } else {
+            callback.onError(response.response.toByteArray())
+          }
+        } catch (e: InvalidProtocolBufferException) {
+          loge(TAG, "Unable to parse query response.", e)
+        }
+        return
+      }
+      else -> loge(TAG, "Received unknown type of message: $operationType. Ignoring.")
+    }
+  }
+
+  override val listener =
+    object : ISafeOnLogRequestedListener.Stub() {
+      override fun onLogRecordsRequested() {
+        val loggerBytes = Logger.getLogger().toByteArray()
+        if (!processLogRecords(loggerId, loggerBytes)) {
+          logw(TAG, "Failed to process log records for logger $loggerId.")
+        }
+      }
+    }
+
+  init {
+    if (platformVersion >= 1) {
+      featureCoordinator.registerConnectionCallback(connectionCallback)
+
+      val connectedDevices = getConnectedDevices() ?: emptyList()
+      for (deviceId in connectedDevices) {
+        featureCoordinator.registerDeviceCallback(deviceId, recipientId, deviceCallback)
+      }
+
+      featureCoordinator.registerOnLogRequestedListener(loggerId, listener)
+    } else {
+      logd(TAG, "Feature coordinator created by outdated Companion platform.")
+    }
+  }
+
+  override fun getConnectedDevices(): List<String>? {
+    val introducedVersion = 1
+    if (platformVersion < introducedVersion) {
+      logd(TAG, "getConnectedDevices invoked by outdated Companion platform.")
+      return null
+    }
+    return featureCoordinator.getConnectedDevices()
+  }
+
+  override fun sendMessage(deviceId: String, message: ByteArray): Boolean {
+    val introducedVersion = 1
+    if (platformVersion < introducedVersion) {
+      logd(TAG, "sendMessage invoked by outdated Companion platform.")
+      return false
+    }
+    return try {
+      featureCoordinator.sendMessage(deviceId, message)
+    } catch (e: RemoteException) {
+      loge(TAG, "sendMessage failed with RemoteException.", e)
+      false
+    }
+  }
+
+  override fun processLogRecords(loggerId: Int, logRecords: ByteArray): Boolean {
+    val introducedVersion = 1
+    if (platformVersion < introducedVersion) {
+      logd(TAG, "processLogRecords invoked by outdated Companion platform.")
+      return false
+    }
+    try {
+      featureCoordinator.processLogRecords(loggerId, logRecords)
+    } catch (e: RemoteException) {
+      loge(TAG, "Failed to send log records for logger $loggerId.", e)
+      return false
+    }
+    return true
+  }
+
+  override fun retrieveAssociatedDevices(listener: IInterface): Boolean {
+    val introducedVersion = 1
+    if (listener !is ISafeOnAssociatedDevicesRetrievedListener) {
+      logd(TAG, "retrieveAssociatedDevices invoked with incorrect callback type.")
+      return false
+    }
+    if (platformVersion < introducedVersion) {
+      logd(TAG, "retrieveAssociatedDevices invoked by outdated Companion platform.")
+      return false
+    }
+    featureCoordinator.retrieveAssociatedDevices(listener)
+    return true
+  }
+
+  override fun cleanUp() {
+    if (platformVersion >= 1) {
+      featureCoordinator.unregisterConnectionCallback(connectionCallback)
+
+      val connectedDevices = getConnectedDevices() ?: emptyList()
+      for (deviceId in connectedDevices) {
+        featureCoordinator.unregisterDeviceCallback(deviceId, recipientId, deviceCallback)
+      }
+
+      featureCoordinator.unregisterOnLogRequestedListener(loggerId, listener)
+    } else {
+      logd(
+        TAG,
+        "Attempting to clean up feature coordinator through an outdated Companion platform."
+      )
+    }
+  }
+
+  companion object {
+    private const val TAG = "SafeApiProxy"
+  }
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/SafeConnector.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/api/SafeConnector.kt
new file mode 100644
index 0000000..43d7309
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/SafeConnector.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2023 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.content.Context
+import android.content.Intent
+import android.os.ParcelUuid
+import com.google.android.connecteddevice.api.external.ISafeOnAssociatedDevicesRetrievedListener
+import com.google.android.connecteddevice.model.AssociatedDevice
+
+/**
+ * Interface for establishing and maintaining a connection to the companion device platform. This is
+ * a subset of Connector, only implementing feature-related functions. It is meant for external
+ * feature usage. Fully backward and forward compatible to ensure robust version mismatch handling.
+ */
+interface SafeConnector {
+
+  /** Feature id for sending and receiving messages. */
+  val featureId: ParcelUuid
+
+  /** [Callback] for connection events. */
+  val callback: Callback
+
+  /** List of ids for the currently connected devices. */
+  val connectedDevices: List<String>
+
+  /**
+  * Cleans up services and feature coordinators attached to the companion platform.
+  * Calling cleanUp will disconnect the feature from the platform and prevent it from receiving
+  * Companion related events. Expectation is that cleanUp will only be called before the
+  * SafeConnector object is disposed.
+  */
+  fun cleanUp()
+
+  /** Sends message to a device. */
+  fun sendMessage(deviceId: String, message: ByteArray)
+
+  /** Sends a query to a device and registers a [QueryCallback] for a response. */
+  fun sendQuery(
+    deviceId: String,
+    request: ByteArray,
+    parameters: ByteArray?,
+    queryCallback: QueryCallback
+  )
+
+  /** Sends a response to a query with an indication of whether it was successful. */
+  fun respondToQuery(deviceId: String, queryId: Int, success: Boolean, response: ByteArray?)
+
+  /** Queries the [ConnectedDevice] for its companion application name. */
+  fun retrieveCompanionApplicationName(deviceId: String, appNameCallback: AppNameCallback)
+
+  /**
+   * Retrieves all associated devices with a [listener] that will be notified when the associated
+   * devices are retrieved.
+   */
+  fun retrieveAssociatedDevices(listener: IOnAssociatedDevicesRetrievedListener)
+
+  /**
+   * Retrieves all associated devices with a [listener] that will be notified when the associated
+   * devices are retrieved.
+   */
+  fun retrieveAssociatedDevices(listener: ISafeOnAssociatedDevicesRetrievedListener)
+
+  /** Callbacks invoked on connection events. */
+  interface Callback {
+    /** Invoked when a connection has been successfully established. */
+    fun onConnected() {}
+
+    /** Invoked when the connection to the platform has been lost. */
+    fun onDisconnected() {}
+
+    /** Invoked when no connection to the platform could be established. */
+    fun onFailedToConnect() {}
+
+    /** Invoked when the platform version is older than the minimum supported version. */
+    fun onApiNotSupported() {}
+
+    /** Invoked when a new connected device with id deviceId is connected. */
+    fun onDeviceConnected(deviceId: String) {}
+
+    /** Invoked when a connected device with id deviceId disconnects. */
+    fun onDeviceDisconnected(deviceId: String) {}
+
+    /**
+     * Invoked when a secure channel has been established with a connected device with id deviceId.
+     */
+    fun onSecureChannelEstablished(deviceId: String) {}
+
+    /**
+     * Invoked when a message fails to send to a device.
+     *
+     * @param deviceId Id of the device the message failed to send to.
+     * @param message Message to send.
+     * @param isTransient `true` if cause of failure is transient and can be retried. `false` if
+     *   failure is permanent.
+     */
+    fun onMessageFailedToSend(deviceId: String, message: ByteArray, isTransient: Boolean) {}
+
+    /** Invoked when a new [byte[]] message is received for this feature. */
+    fun onMessageReceived(deviceId: String, message: ByteArray) {}
+
+    /** Invoked when a new query is received for this feature. */
+    fun onQueryReceived(
+      deviceId: String,
+      queryId: Int,
+      request: ByteArray,
+      parameters: ByteArray?
+    ) {}
+
+    /** Invoked when an error has occurred with the connection. */
+    fun onDeviceError(deviceId: String, error: Int) {}
+
+    /** Invoked when a new [AssociatedDevice] is added for the given user. */
+    fun onAssociatedDeviceAdded(device: AssociatedDevice) {}
+
+    /** Invoked when an [AssociatedDevice] is removed for the given user. */
+    fun onAssociatedDeviceRemoved(device: AssociatedDevice) {}
+
+    /** Invoked when an [AssociatedDevice] is updated for the given user. */
+    fun onAssociatedDeviceUpdated(device: AssociatedDevice) {}
+  }
+
+  /** Callback for a query response. */
+  interface QueryCallback {
+    /** Invoked with a successful response to a query. */
+    fun onSuccess(response: ByteArray?) {}
+
+    /** Invoked with an unsuccessful response to a query. */
+    fun onError(response: ByteArray?) {}
+
+    /**
+     * Invoked when a query failed to send to the device. `isTransient` is set to `true` if cause of
+     * failure is transient and can be retried, or `false` if failure is permanent.
+     */
+    fun onQueryFailedToSend(isTransient: Boolean) {}
+  }
+
+  /** Callback for a query for the name of the companion application on the connected device. */
+  interface AppNameCallback {
+    /** Invoked with the name of the companion application on the connected device. */
+    fun onNameReceived(appName: String) {}
+
+    /** Invoked when the name failed to be retrieved from the connected device. */
+    fun onError() {}
+  }
+
+  companion object {
+    /**
+     * When a client calls [Context.bindService] to get the [IFeatureCoordinator], this action is
+     * required in the param [Intent].
+     */
+    const val ACTION_BIND_FEATURE_COORDINATOR =
+      "com.google.android.connecteddevice.api.BIND_FEATURE_COORDINATOR"
+
+    /**
+     * When a client queries for the platform's API version, this action is required in the param
+     * [Intent]
+     */
+    const val ACTION_QUERY_API_VERSION = "com.google.android.connecteddevice.api.QUERY_API_VERSION"
+
+    /** Id for the system query feature. */
+    val SYSTEM_FEATURE_ID = ParcelUuid.fromString("892ac5d9-e9a5-48dc-874a-c01e3cb00d5d")
+  }
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/external/ISafeFeatureCoordinator.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/api/external/ISafeFeatureCoordinator.aidl
index bf029c4..fdd4771 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/api/external/ISafeFeatureCoordinator.aidl
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/external/ISafeFeatureCoordinator.aidl
@@ -62,6 +62,9 @@
     /**
      * Registers a callback for a specific connectedDevice and recipient.
      *
+     * Duplicate registration with the same [recipientId] will block the
+     * recipient and prevent it from receiving callbacks.
+     *
      * @param deviceId {@link String} to register triggers on.
      * @param recipientId {@link ParcelUuid} to register as recipient of.
      * @param callback {@link ISafeDeviceCallback} to register.
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/external/ISafeFeatureCoordinatorListener.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/api/external/ISafeFeatureCoordinatorListener.aidl
new file mode 100644
index 0000000..01aee6f
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/external/ISafeFeatureCoordinatorListener.aidl
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 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.external;
+
+import com.google.android.connecteddevice.api.external.ISafeFeatureCoordinator;
+
+/**
+ * Listener for feature coordinator initialization.
+ *
+ * Only make additive changes to maintain backward compatibility.
+ * The added function needs to be assigned the transaction value noted below,
+ * and the value needs to be appropriately incremented.
+ *
+ * Next transaction value: 1
+ */
+interface ISafeFeatureCoordinatorListener {
+    /** Callback when feature coordinator is initialized. */
+    void onFeatureCoordinatorInitialized(
+            ISafeFeatureCoordinator featureCoordinator) = 0;
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/external/ISafeFeatureCoordinatorStatusNotifier.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/api/external/ISafeFeatureCoordinatorStatusNotifier.aidl
new file mode 100644
index 0000000..fae24ef
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/external/ISafeFeatureCoordinatorStatusNotifier.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 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.external;
+
+import com.google.android.connecteddevice.api.external.ISafeFeatureCoordinatorListener;
+
+/**
+ * Creates listeners for feature coordinator initialization.
+ *
+ * Only make additive changes to maintain backward compatibility.
+ * The added function needs to be assigned the transaction value noted below,
+ * and the value needs to be appropriately incremented.
+ *
+ * Next transaction value: 2
+ */
+interface ISafeFeatureCoordinatorStatusNotifier {
+    /**
+    * Registers listeners to be notified when feature coordinator is
+    * initialized. Eeach listener will receive a feature coordinator upon
+    * notification.
+    */
+    void registerFeatureCoordinatorListener(
+            ISafeFeatureCoordinatorListener listener) = 0;
+
+    /** Unregisters listener. */
+    void unregisterFeatureCoordinatorListener(
+            ISafeFeatureCoordinatorListener listeners) = 1;
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/CalendarSyncService.java b/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/CalendarSyncService.java
index a241204..564823d 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/CalendarSyncService.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/CalendarSyncService.java
@@ -77,8 +77,9 @@
       StrictMode.setThreadPolicy(
           new StrictMode.ThreadPolicy.Builder().detectAll().penaltyDialog().build());
 
-      // Settings for the entire application process.
-      StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyDeath().build());
+      // Settings for the entire application process. StrictMode.VmPolicy.Builder().penaltyDeath()
+      // will block general companion testing.
+      StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build());
     }
   }
 
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/android/EventContentDelegate.java b/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/android/EventContentDelegate.java
index ed7456c..6794328 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/android/EventContentDelegate.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/android/EventContentDelegate.java
@@ -445,6 +445,7 @@
   /** The type of event regarding recurrence. */
   private enum RecurrenceType {
 
+    // LINT.IfChange
     /** An event that only occurs once. */
     SINGLE("S"),
 
@@ -456,6 +457,7 @@
      * regular meeting.
      */
     EXCEPTION("X");
+    // LINT.ThenChange(//depot/google3/third_party/swift/AndroidAutoCalendarSync/Sources/AndroidAutoCalendarSync/EKEventStore+Extensions.swift)
 
     /** The code to include in the key. */
     final String code;
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/common/BaseCalendarSync.java b/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/common/BaseCalendarSync.java
index 065ab48..b6f059a 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/common/BaseCalendarSync.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/common/BaseCalendarSync.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 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.calendarsync.common;
 
 import static com.google.common.collect.Sets.difference;
@@ -115,6 +131,7 @@
     }
     DeviceState state = getOrCreateState(deviceId);
     state.version = update.getVersion();
+    logger.info("The state version is " + state.version);
 
     logger.info(
         "Received %s with %d calendars and size %d from device %s",
@@ -168,7 +185,7 @@
    * <p>Must be called before using the other methods of this class.
    */
   public void start() {
-    logger.debug("Start");
+    logger.debug("Start.");
     calendarsObservationHandle = calendarsObservable.observe(this::onCalendarsChanged);
   }
 
@@ -178,7 +195,7 @@
    * <p>Updating the calendar can result in many change events being fired in rapid succession.
    */
   private void onCalendarsChanged() {
-    logger.debug("Calendars changed");
+    logger.debug("Calendars changed.");
     if (delayedChangeUpdate != null) {
       delayedChangeUpdate.cancel();
     }
@@ -235,6 +252,7 @@
       calendarStore.store(state.deviceId, currentCalendar);
       currentCalendars.add(currentCalendar);
     }
+    logger.info("Send device change update.");
 
     // Keep the store up-to-date with the calendars on the remove device.
     SetView<String> removedCalendarKeys =
@@ -299,7 +317,7 @@
     private final Map<String, Range<Instant>> calendarKeyToTimeRange = new HashMap<>();
 
     /**
-     * The protocol version used by this remove device.
+     * The protocol version used by this remote device.
      *
      * <p>Until a response is received from the remote device the version is assumed to be version 0
      * which does not support sending updates.
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/common/update.proto b/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/common/update.proto
index 85bb29a..1585698 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/common/update.proto
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/common/update.proto
@@ -1,10 +1,26 @@
 /*
+ * Copyright (C) 2023 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.
+ */
+
+/*
  * This file must remain backwards wire compatible with ../proto/calendar.proto
  */
 
 syntax = "proto3";
 
-package com.google.android.connecteddevice.calendarsync;
+package aae.calendarsync;
 
 option java_package = "com.google.android.connecteddevice.calendarsync";
 option java_multiple_files = true;
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/proto/calendar.proto b/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/proto/calendar.proto
index ef24ff1..dcda1e3 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/proto/calendar.proto
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/calendarsync/proto/calendar.proto
@@ -16,7 +16,7 @@
 
 syntax = "proto3";
 
-package aae.calendarsync;
+package aae.calendarsync.deprecated;
 
 option java_package = "com.google.android.connecteddevice.calendarsync.proto";
 option java_multiple_files = true;
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/core/FeatureCoordinator.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/core/FeatureCoordinator.kt
index dba9723..8e17584 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/core/FeatureCoordinator.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/core/FeatureCoordinator.kt
@@ -59,8 +59,9 @@
 constructor(
   private val controller: DeviceController,
   private val storage: ConnectedDeviceStorage,
+  private val systemQueryCache: SystemQueryCache = SystemQueryCache.create(),
   private val loggingManager: LoggingManager,
-  private val callbackExecutor: Executor = Executors.newCachedThreadPool()
+  private val callbackExecutor: Executor = Executors.newCachedThreadPool(),
 ) : IFeatureCoordinator.Stub() {
 
   private val deviceAssociationCallbacks = AidlThreadSafeCallbacks<IDeviceAssociationCallback>()
@@ -159,6 +160,11 @@
       }
 
       override fun sendMessage(deviceId: String, message: ByteArray): Boolean {
+        val connectedDevice = controller.connectedDevices.firstOrNull { it.deviceId == deviceId }
+        if (connectedDevice == null) {
+          loge(TAG, "Device $deviceId not found. Unable to send message.")
+          return false
+        }
         // TODO(b/265862484): Deprecate DeviceMessage in favor of byte arrays.
         val parsedMessage =
           try {
@@ -174,6 +180,20 @@
             DeviceMessage.OperationType.fromValue(parsedMessage.operation.number),
             parsedMessage.payload.toByteArray()
           )
+        val cachedResponse = systemQueryCache.getCachedResponse(connectedDevice, deviceMessage)
+        if (cachedResponse != null) {
+          // If a system query has a cached answer, short-circuit the query/response flow by faking
+          // a response. Using the cached response allows us to speed up queries by features when
+          // the response time is limited (e.g. time for SecondDeviceSignInUrlFeature to be
+          // "ready").
+          //
+          // Schedule the response callback on a different executor to avoid the callback is
+          // delivered before this sendMessage method completes/returns.
+          callbackExecutor.execute {
+            onMessageReceivedInternal(connectedDevice, cachedResponse, shouldCacheMessage = false)
+          }
+          return true
+        }
         return controller.sendMessage(UUID.fromString(deviceId), deviceMessage)
       }
 
@@ -309,12 +329,9 @@
         }
       }
 
-    @Suppress("UNCHECKED_CAST") // Cast will always succeed because of the type check above.
     val previousCallback =
-      (recipientCallbacks as? MutableMap<ParcelUuid, IInterface>)?.putIfAbsent(
-        recipientId,
-        callback
-      )
+      deviceCallbacks[connectedDevice.deviceId]?.get(recipientId)
+        ?: safeDeviceCallbacks[connectedDevice.deviceId]?.get(recipientId)
 
     // 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
@@ -346,7 +363,8 @@
       TAG,
       "New callback registered on device ${connectedDevice.deviceId} for recipient $recipientId."
     )
-
+    @Suppress("UNCHECKED_CAST") // Cast will always succeed because of the type check above.
+    (recipientCallbacks as? MutableMap<ParcelUuid, IInterface>)?.put(recipientId, callback)
     return true
   }
 
@@ -455,8 +473,22 @@
     logd(TAG, "Device callback unregistered on device ${deviceId} for recipient " + "$recipientId.")
   }
 
-  override fun sendMessage(connectedDevice: ConnectedDevice, message: DeviceMessage): Boolean =
-    controller.sendMessage(UUID.fromString(connectedDevice.deviceId), message)
+  override fun sendMessage(connectedDevice: ConnectedDevice, message: DeviceMessage): Boolean {
+    val cachedResponse = systemQueryCache.getCachedResponse(connectedDevice, message)
+    if (cachedResponse != null) {
+      // If a system query has a cached answer, short-circuit the query/response flow by faking a
+      // response. Using the cached response allows us to speed up queries by features when the
+      // response time is limited (e.g. time for SecondDeviceSignInUrlFeature to be "ready").
+      //
+      // Schedule the response callback on a different executor to avoid the callback is delivered
+      // before this sendMessage method completes/returns.
+      callbackExecutor.execute {
+        onMessageReceivedInternal(connectedDevice, cachedResponse, shouldCacheMessage = false)
+      }
+      return true
+    }
+    return controller.sendMessage(UUID.fromString(connectedDevice.deviceId), message)
+  }
 
   override fun registerDeviceAssociationCallback(callback: IDeviceAssociationCallback) {
     deviceAssociationCallbacks.add(callback, callbackExecutor)
@@ -590,6 +622,8 @@
 
   @VisibleForTesting
   internal fun onDeviceDisconnectedInternal(connectedDevice: ConnectedDevice) {
+    systemQueryCache.clearCache(connectedDevice)
+
     if (connectedDevice.isAssociatedWithDriver) {
       logd(TAG, "Notifying callbacks that a device has disconnected for the driver.")
       driverConnectionCallbacks.invoke { it.onDeviceDisconnected(connectedDevice) }
@@ -598,6 +632,8 @@
       passengerConnectionCallbacks.invoke { it.onDeviceDisconnected(connectedDevice) }
     }
     allConnectionCallbacks.invoke { it.onDeviceDisconnected(connectedDevice) }
+    // Clear blocked recipients for the next connection so the state is easier to recover.
+    lock.withLock { blockedRecipients.clear() }
   }
 
   @VisibleForTesting
@@ -630,7 +666,16 @@
   }
 
   @VisibleForTesting
-  internal fun onMessageReceivedInternal(connectedDevice: ConnectedDevice, message: DeviceMessage) {
+  internal fun onMessageReceivedInternal(
+    connectedDevice: ConnectedDevice,
+    message: DeviceMessage,
+    shouldCacheMessage: Boolean = true
+  ) {
+    if (shouldCacheMessage) {
+      // Cache the received message for a faster response if queried again by another feature.
+      systemQueryCache.maybeCacheResponse(connectedDevice, message)
+    }
+
     if (message.recipient == null) {
       loge(
         TAG,
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 ec9b4e1..a19ada8 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/core/MultiProtocolDeviceController.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/core/MultiProtocolDeviceController.kt
@@ -15,6 +15,7 @@
  */
 package com.google.android.connecteddevice.core
 
+import android.content.Context
 import android.database.sqlite.SQLiteCantOpenDatabaseException
 import android.os.ParcelUuid
 import androidx.annotation.GuardedBy
@@ -26,6 +27,7 @@
 import com.google.android.connecteddevice.connection.MultiProtocolSecureChannel.ShowVerificationCodeListener
 import com.google.android.connecteddevice.connection.ProtocolStream
 import com.google.android.connecteddevice.core.DeviceController.Callback
+import com.google.android.connecteddevice.metrics.EventMetricLogger
 import com.google.android.connecteddevice.model.AssociatedDevice
 import com.google.android.connecteddevice.model.ConnectedDevice
 import com.google.android.connecteddevice.model.DeviceMessage
@@ -72,17 +74,18 @@
  * @property storage Storage necessary to generate reconnect challenge.
  * @property enablePassenger Whether passenger devices automatically connect. When `true`, newly
  *   associated devices will remain unclaimed by default.
- * @property callbackExecutor Executor on which callbacks are executed.
+ * @property storageExecutor Executor on which storage related tasks are executed.
  */
 class MultiProtocolDeviceController
 @JvmOverloads
 constructor(
+  private val context: Context,
   private val protocolDelegate: ProtocolDelegate,
   private val storage: ConnectedDeviceStorage,
   private val oobRunner: OobRunner,
   private val associationServiceUuid: UUID,
   private val enablePassenger: Boolean,
-  private val callbackExecutor: Executor = Executors.newSingleThreadExecutor()
+  private val storageExecutor: Executor = Executors.newSingleThreadExecutor()
 ) : DeviceController {
   private val connectedRemoteDevices = ConcurrentHashMap<UUID, ConnectedRemoteDevice>()
 
@@ -91,24 +94,29 @@
   @VisibleForTesting internal val associationPendingDeviceId = AtomicReference<UUID?>(null)
 
   private val lock = ReentrantLock()
+  private val metricLogger = EventMetricLogger(context)
 
   @GuardedBy("lock") private val associatedDevices = mutableListOf<AssociatedDevice>()
   @GuardedBy("lock") private val driverDevices = mutableListOf<AssociatedDevice>()
+  @GuardedBy("lock") private val passengerDevices = mutableListOf<AssociatedDevice>()
 
   private val storageCallback =
     object : ConnectedDeviceStorage.AssociatedDeviceCallback {
       override fun onAssociatedDeviceAdded(device: AssociatedDevice) {
-        // Device population is handled locally when a new device is added.
+        logd(TAG, "An associated device has been added. Repopulating devices from storage.")
+        populateDevicesWithExecutor()
+        // Make sure the internal status are synched from storage before invoking callbacks.
+        storageExecutor.execute { invokeCallbacksWithAssociatedDevice(device) }
       }
 
       override fun onAssociatedDeviceRemoved(device: AssociatedDevice) {
         logd(TAG, "An associated device has been removed. Repopulating devices from storage.")
-        populateDevices()
+        populateDevicesWithExecutor()
       }
 
       override fun onAssociatedDeviceUpdated(device: AssociatedDevice) {
         logd(TAG, "An associated device has been updated. Repopulating devices from storage.")
-        populateDevices()
+        populateDevicesWithExecutor()
       }
     }
 
@@ -149,7 +157,7 @@
 
   override fun start() {
     logd(TAG, "Starting controller and initiating connections with driver devices.")
-    populateDevices()
+    populateDevicesWithExecutor()
     val driverDevices = storage.driverAssociatedDevices
     for (device in driverDevices) {
       if (device.isConnectionEnabled) {
@@ -324,20 +332,29 @@
     callbacks.remove(callback)
   }
 
-  private fun populateDevices() {
-    callbackExecutor.execute {
+  /**
+   * Populates associated devices from the storage.
+   *
+   * Any logic following this which relies on the data refreshness needs to run on the
+   * same executor to avoid race conditions.
+   */
+  private fun populateDevicesWithExecutor() {
+    storageExecutor.execute {
       while (true) {
         try {
           logd(TAG, "Populating associated devices from storage.")
 
           // Fetch devices prior to applying lock to reduce lock time.
           val driverOnlyDevices = storage.driverAssociatedDevices
+          val passengerOnlyDevices = storage.passengerAssociatedDevices
           val allDevices = storage.allAssociatedDevices
           lock.withLock {
             associatedDevices.clear()
             associatedDevices.addAll(allDevices)
             driverDevices.clear()
             driverDevices.addAll(driverOnlyDevices)
+            passengerDevices.clear()
+            passengerDevices.addAll(passengerOnlyDevices)
           }
           logd(TAG, "Devices populated successfully.")
           break
@@ -589,7 +606,7 @@
     invokeCallbacksWithDevice(device) { connectedDevice, callback ->
       callback.onDeviceDisconnected(connectedDevice)
     }
-    callbackExecutor.execute {
+    storageExecutor.execute {
       val associatedDevice = storage.getAssociatedDevice(disconnectedDeviceId.toString())
       if (associatedDevice == null) {
         loge(
@@ -643,6 +660,7 @@
           callback.onSecureChannelEstablished(connectedDevice)
         }
         EventLog.onSecureChannelEstablished()
+        metricLogger.onSecureChannelEstablished()
       }
 
       override fun onEstablishSecureChannelFailure(error: ChannelError) {
@@ -693,36 +711,33 @@
       handleAssociationError(ChannelError.CHANNEL_ERROR_INVALID_DEVICE_ID.ordinal, device)
       return
     }
-
-    connectedRemoteDevices.computeIfAbsent(deviceId) {
-      logd(TAG, "Assigning newly-associated device to its real device id.")
-      val newDevice = convertTempAssociationDeviceToRealDevice(device, deviceId)
-      logd(TAG, "Received device id and secret from $deviceId.")
-      try {
-        storage.saveChallengeSecret(
-          deviceId.toString(),
-          deviceMessage.message.copyOfRange(DEVICE_ID_BYTES, deviceMessage.message.size)
-        )
-      } catch (e: InvalidParameterException) {
-        loge(TAG, "Error saving challenge secret.", e)
-        // Call on old device since it had the original association callback.
-        handleAssociationError(ChannelError.CHANNEL_ERROR_INVALID_ENCRYPTION_KEY.ordinal, device)
-        return@computeIfAbsent newDevice
-      }
-      connectedRemoteDevices.remove(pendingDeviceId)
-      associationPendingDeviceId.set(null)
-      oobRunner.reset()
-      newDevice.secureChannel?.setDeviceIdDuringAssociation(deviceId)
-      persistAssociatedDevice(deviceId.toString())
-      newDevice.callback?.onAssociationCompleted()
-      invokeCallbacksWithDevice(newDevice) { connectedDevice, callback ->
-        callback.onDeviceConnected(connectedDevice)
-      }
-      invokeCallbacksWithDevice(newDevice) { connectedDevice, callback ->
-        callback.onSecureChannelEstablished(connectedDevice)
-      }
-      newDevice
+    if (connectedRemoteDevices.containsKey(deviceId)) {
+      logd(TAG, "This device $deviceId already exist in the connected device list. Ignore.")
+      return
     }
+    logd(TAG, "Assigning newly-associated device to its real device id.")
+    val newDevice = convertTempAssociationDeviceToRealDevice(device, deviceId)
+    logd(TAG, "Received device id and secret from $deviceId.")
+    try {
+      storage.saveChallengeSecret(
+        deviceId.toString(),
+        deviceMessage.message.copyOfRange(DEVICE_ID_BYTES, deviceMessage.message.size)
+      )
+    } catch (e: InvalidParameterException) {
+      loge(TAG, "Error saving challenge secret.", e)
+      // Call on old device since it had the original association callback.
+      handleAssociationError(ChannelError.CHANNEL_ERROR_INVALID_ENCRYPTION_KEY.ordinal, device)
+      return
+    }
+    connectedRemoteDevices.remove(pendingDeviceId)
+    associationPendingDeviceId.set(null)
+    connectedRemoteDevices.put(deviceId, newDevice)
+    oobRunner.reset()
+    newDevice.secureChannel?.setDeviceIdDuringAssociation(deviceId)
+    persistAssociatedDevice(deviceId.toString())
+
+    // Invoke callbacks after internal states are updated to avoid race conditions.
+    device.callback?.onAssociationCompleted()
   }
 
   private fun convertTempAssociationDeviceToRealDevice(
@@ -771,10 +786,29 @@
     device: ConnectedRemoteDevice,
     onCallback: (ConnectedDevice, Callback) -> Unit
   ) {
-    val connectedDevice = lock.withLock { device.toConnectedDevice(driverDevices) }
+    val connectedDevice = lock.withLock { device.toConnectedDevice(passengerDevices) }
     callbacks.invoke { onCallback(connectedDevice, it) }
   }
 
+  private fun invokeCallbacksWithAssociatedDevice(associatedDevice: AssociatedDevice) {
+    logd(TAG, "Invovke callbacks with associated device")
+    val hasSecureChannel =
+      connectedRemoteDevices.get(UUID.fromString(associatedDevice.deviceId))?.secureChannel != null
+    lock.withLock {
+      val belongsToDriver =
+        passengerDevices.none { device -> device.deviceId == associatedDevice.deviceId }
+      val connectedDevice =
+        ConnectedDevice(
+          associatedDevice.deviceId,
+          associatedDevice.deviceName,
+          belongsToDriver,
+          hasSecureChannel
+        )
+      callbacks.invoke { it.onDeviceConnected(connectedDevice) }
+      callbacks.invoke { it.onSecureChannelEstablished(connectedDevice) }
+    }
+  }
+
   /** Container class to hold information about a connected device. */
   internal data class ConnectedRemoteDevice(
     val deviceId: UUID,
@@ -786,8 +820,12 @@
     var channelResolver: ChannelResolver? = null
 
     /** Returns the [ConnectedDevice] equivalent or `null` if the conversion failed. */
-    fun toConnectedDevice(driverDevices: List<AssociatedDevice>): ConnectedDevice {
-      val belongsToDriver = driverDevices.any { device -> device.deviceId == deviceId.toString() }
+    fun toConnectedDevice(passengerDevices: List<AssociatedDevice>): ConnectedDevice {
+      // TODO(b/269484772): During device removal, the device record might have already been cleared
+      // so we have to do a reversed check against passenger device. This value may still be true if
+      // device is already disassociated.
+      val belongsToDriver =
+        passengerDevices.none { device -> device.deviceId == deviceId.toString() }
       val hasSecureChannel = secureChannel != null
       return ConnectedDevice(deviceId.toString(), name, belongsToDriver, hasSecureChannel)
     }
@@ -807,6 +845,7 @@
       return newDevice
     }
   }
+
   companion object {
     private const val TAG = "MultiProtocolDeviceController"
     private const val SALT_BYTES = 8
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/core/SystemQueryCache.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/core/SystemQueryCache.kt
new file mode 100644
index 0000000..d59e5ce
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/core/SystemQueryCache.kt
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2023 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.core
+
+import androidx.annotation.VisibleForTesting
+import com.google.android.companionprotos.FeatureSupportResponse
+import com.google.android.companionprotos.FeatureSupportStatus
+import com.google.android.companionprotos.Query
+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.SYSTEM_FEATURE_ID
+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 com.google.android.connecteddevice.util.SafeLog.loge
+import com.google.android.connecteddevice.util.SafeLog.logi
+import com.google.protobuf.ByteString
+import com.google.protobuf.InvalidProtocolBufferException
+import java.nio.charset.StandardCharsets
+import java.util.UUID
+
+/**
+ * A cache for System Queries.
+ *
+ * We assume the responses for system query from a remote device are stable across queries, so their
+ * results can be cached on IHU. Caching the responses for system queries enables short-circuiting
+ * the query/response message flow when it's queried again.
+ *
+ * Responses of value `null` are ignored.
+ */
+interface SystemQueryCache {
+  /**
+   * Optionally caches the device message.
+   *
+   * Inspects the device message to check if it is a system query.
+   */
+  fun maybeCacheResponse(device: ConnectedDevice, message: DeviceMessage)
+
+  /**
+   * Checks if the message can be responded to by cached query response.
+   *
+   * Generates a response for the device message if a response has been cached. Returns `null` if
+   * there is no cached response, or the message is not a query.
+   */
+  fun getCachedResponse(device: ConnectedDevice, message: DeviceMessage): DeviceMessage?
+
+  /** Clears cached response from device. */
+  fun clearCache(device: ConnectedDevice)
+
+  companion object {
+    fun create(): SystemQueryCache = SystemQueryCacheImpl()
+  }
+}
+
+internal class SystemQueryCacheImpl : SystemQueryCache {
+  @VisibleForTesting internal val deviceCaches = mutableMapOf<UUID, DeviceSystemQueryCache>()
+
+  override fun maybeCacheResponse(device: ConnectedDevice, message: DeviceMessage) {
+    deviceCaches
+      .getOrPut(UUID.fromString(device.deviceId)) { DeviceSystemQueryCache() }
+      .cache(message)
+  }
+
+  override fun getCachedResponse(device: ConnectedDevice, message: DeviceMessage): DeviceMessage? {
+    val deviceCache =
+      deviceCaches.getOrPut(UUID.fromString(device.deviceId)) { DeviceSystemQueryCache() }
+    return deviceCache.getCached(message)
+  }
+
+  override fun clearCache(device: ConnectedDevice) {
+    logi(TAG, "Clearing cache for $device.")
+    val deviceCache = deviceCaches.remove(UUID.fromString(device.deviceId))
+    deviceCache?.clear()
+  }
+
+  companion object {
+    private const val TAG = "SystemQueryCache"
+  }
+}
+
+/** Caches system query responses from a device. */
+internal class DeviceSystemQueryCache {
+  @VisibleForTesting internal var appName: String? = null
+  @VisibleForTesting internal var deviceName: String? = null
+  @VisibleForTesting internal val supportedFeatures = mutableSetOf<UUID>()
+
+  // This map tracks the SystemQueryType sent by each <featureId, queryId>.
+  //
+  // This map is necessary due to the way the queries are answered: when a feature makes a system
+  // query, it sends the query with its featureId as `query.sender`, and internally tracks the
+  // answer with `queryId`. When the response is received, it's routed to `query.sender` with the
+  // same `queryId`. The type of the system query is only known to the sender.
+  // So here by inspecting the query, we can track the type of the response to parse it properly.
+  @VisibleForTesting
+  internal val trackedQueryTypes = mutableMapOf<Pair<UUID, Int>, SystemQueryType>()
+
+  /**
+   * Attempts to cache an inbound message if it was a system query response.
+   *
+   * Parses the message to inspect its message type. Messages that are not system query responses
+   * will be ignored. Infers the type of the system query response based on previous outbound system
+   * queries. Caches the response if the type can be inferred.
+   */
+  fun cache(message: DeviceMessage) {
+    if (!message.isSystemQueryResponse()) {
+      // Do not log here - we check every message. It'd be very spammy.
+      return
+    }
+
+    val queryResponse = message.parseMessageAsQueryResponse() ?: return
+
+    val querySender = message.recipient
+    val queryId = queryResponse.queryId
+
+    val systemQueryType = trackedQueryTypes.remove(Pair(querySender, queryId))
+    if (systemQueryType == null) {
+      // All outgoing system queries should be cached so an unrecognized query response is
+      // unexpected. Log in detail to debug.
+      loge(
+        TAG,
+        "Received SystemQuery response with unrecognized query type. " +
+          "Intended receiver is $querySender with $queryId."
+      )
+      return
+    }
+
+    // Only query types intended for the phone side are listed.
+    when (systemQueryType) {
+      SystemQueryType.APP_NAME -> {
+        appName = String(queryResponse.response.toByteArray(), StandardCharsets.UTF_8)
+        logi(TAG, "Caching app name $appName.")
+      }
+      SystemQueryType.DEVICE_NAME -> {
+        deviceName = String(queryResponse.response.toByteArray(), StandardCharsets.UTF_8)
+        logi(TAG, "Caching device name $deviceName.")
+      }
+      SystemQueryType.IS_FEATURE_SUPPORTED -> {
+        val featureSupportResponse = FeatureSupportResponse.parseFrom(queryResponse.response)
+        logi(TAG, "Caching feature support response $featureSupportResponse.")
+        for (status in featureSupportResponse.getStatusesList()) {
+          if (status.isSupported) {
+            supportedFeatures.add(UUID.fromString(status.featureId))
+            logi(TAG, "Caching feature ${status.featureId} is supported.")
+          } else {
+            logi(TAG, "Feature ${status.featureId} is not supported.")
+          }
+        }
+      }
+      else -> {
+        loge(TAG, "Could not cache unspported system query type $systemQueryType.")
+      }
+    }
+  }
+
+  /**
+   * Attempts to generate an inbound message as the response to the outbound system query.
+   *
+   * Returns `null` if there is no cached response, or the message is not a system query.
+   *
+   * Parses the message to inspect its message type. Messages that are not system queries are
+   * ignored. If the query does not have a cached response, internally tracks the query type so that
+   * the eventual inbound response can be cached.
+   */
+  fun getCached(message: DeviceMessage): DeviceMessage? {
+    if (!message.isSystemQuery()) {
+      // Do not log here - we check every message. It'd be very spammy.
+      return null
+    }
+    logi(TAG, "Looking for cached response for $message.")
+
+    val query = message.parseMessageAsQuery() ?: return null
+    val queryId = query.id
+    val sender = ByteUtils.bytesToUUID(query.sender.toByteArray()) ?: return null
+
+    val systemQuery = query.parseRequestAsSystemQuery() ?: return null
+    val type = systemQuery.type
+
+    val cachedResponse: DeviceMessage? =
+      when (type) {
+        SystemQueryType.APP_NAME -> handleAppNameSystemQuery(sender, queryId)
+        SystemQueryType.DEVICE_NAME -> handleDeviceNameSystemQuery(sender, queryId)
+        SystemQueryType.IS_FEATURE_SUPPORTED ->
+          handleIsFeatureSupportedSystemQuery(sender, queryId, systemQuery)
+        else -> {
+          loge(TAG, "Unspported system query ${type} in cache.")
+          null
+        }
+      }
+
+    if (cachedResponse != null) {
+      logi(TAG, "Returning cached response for $type to <$sender, $queryId>.")
+      return cachedResponse
+    }
+
+    // The query hasn't been cached yet, track the query type so that when the response is
+    // received, it can be properly parsed.
+    logi(TAG, "No cached response for $type. Tracking by <$sender, $queryId>.")
+    trackedQueryTypes[Pair(sender, queryId)] = type
+
+    return null
+  }
+
+  private fun handleAppNameSystemQuery(sender: UUID, queryId: Int): DeviceMessage? {
+    val cached = appName
+    if (cached == null) {
+      logi(TAG, "No cached response for system query AppName.")
+      return null
+    }
+
+    logi(TAG, "Returning cached app name: $cached")
+    return createCachedResponseForString(cached, sender, queryId)
+  }
+
+  private fun handleDeviceNameSystemQuery(sender: UUID, queryId: Int): DeviceMessage? {
+    val cached = deviceName
+    if (cached == null) {
+      logi(TAG, "No cached response for system query DeviceName.")
+      return null
+    }
+
+    logi(TAG, "Returning cached device name: $cached")
+    return createCachedResponseForString(cached, sender, queryId)
+  }
+
+  private fun createCachedResponseForString(
+    cached: String,
+    sender: UUID,
+    queryId: Int
+  ): DeviceMessage {
+    // Generate a device message that contains a QueryResponse that contains the cached value.
+    val queryResponse =
+      QueryResponse.newBuilder()
+        .setQueryId(queryId)
+        .setSuccess(true)
+        .setResponse(ByteString.copyFrom(cached.toByteArray()))
+        .build()
+
+    val deviceMessage =
+      DeviceMessage.createIncomingMessage(
+        /* recipient= */ sender,
+        /* isMessageEncrypted= */ false,
+        /* operationType= */ OperationType.QUERY_RESPONSE,
+        /* message= */ queryResponse.toByteArray(),
+        // OK to ignore original message size.
+        /* originalMessageSize= */ 0,
+      )
+
+    return deviceMessage
+  }
+
+  private fun handleIsFeatureSupportedSystemQuery(
+    sender: UUID,
+    queryId: Int,
+    systemQuery: SystemQuery
+  ): DeviceMessage? {
+    val queriedFeatureIds =
+      systemQuery.getPayloadsList().map {
+        UUID.fromString(String(it.toByteArray(), StandardCharsets.UTF_8))
+      }
+    for (featureId in queriedFeatureIds) {
+      // Default to false to allow features with a cached unsupported status to be queried again.
+      if (featureId !in supportedFeatures) {
+        logi(TAG, "Queried feature $featureId is not supported or cached. Returning null.")
+        return null
+      }
+    }
+
+    val cachedStatuses =
+      queriedFeatureIds.map {
+        logi(TAG, "Cached support status for feature $it.")
+        FeatureSupportStatus.newBuilder()
+          .setFeatureId(it.toString())
+          // Since we ignore "unsupported" feature support status, all cached results are true.
+          .setIsSupported(true)
+          .build()
+      }
+    val featureSupportResponse =
+      FeatureSupportResponse.newBuilder().addAllStatuses(cachedStatuses).build()
+
+    val queryResponse =
+      QueryResponse.newBuilder()
+        .setQueryId(queryId)
+        .setSuccess(true)
+        .setResponse(ByteString.copyFrom(featureSupportResponse.toByteArray()))
+        .build()
+
+    val deviceMessage =
+      DeviceMessage.createIncomingMessage(
+        /* recipient= */ sender,
+        /* isMessageEncrypted= */ false,
+        /* operationType= */ OperationType.QUERY_RESPONSE,
+        /* message= */ queryResponse.toByteArray(),
+        // OK to ignore original message size.
+        /* originalMessageSize= */ 0,
+      )
+    logi(TAG, "Returning cached support status for $queriedFeatureIds.")
+    return deviceMessage
+  }
+
+  private fun DeviceMessage.parseMessageAsQuery(): Query? =
+    try {
+      Query.parseFrom(message)
+    } catch (e: InvalidProtocolBufferException) {
+      loge(TAG, "Unable to parse DeviceMessage as Query.", e)
+      null
+    }
+
+  private fun DeviceMessage.isSystemQueryResponse(): Boolean =
+    operationType == OperationType.QUERY_RESPONSE
+
+  private fun DeviceMessage.isSystemQuery(): Boolean =
+    operationType == OperationType.QUERY && recipient == SYSTEM_FEATURE
+
+  private fun DeviceMessage.parseMessageAsQueryResponse(): QueryResponse? =
+    try {
+      QueryResponse.parseFrom(message)
+    } catch (e: InvalidProtocolBufferException) {
+      loge(TAG, "Unable to parse DeviceMessage as QueryResponse.", e)
+      null
+    }
+
+  private fun Query.parseRequestAsSystemQuery(): SystemQuery? =
+    try {
+      SystemQuery.parseFrom(request)
+    } catch (e: InvalidProtocolBufferException) {
+      loge(TAG, "Unable to parse Query as SystemQuery.", e)
+      null
+    }
+
+  fun clear() {
+    logi(TAG, "Clearing cached responses.")
+
+    trackedQueryTypes.clear()
+
+    appName = null
+    deviceName = null
+    supportedFeatures.clear()
+  }
+
+  companion object {
+    private const val TAG = "DeviceSystemQueryCache"
+    private val SYSTEM_FEATURE: UUID = SYSTEM_FEATURE_ID.uuid
+  }
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/metrics/EventMetricLogger.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/metrics/EventMetricLogger.kt
new file mode 100644
index 0000000..bd3b013
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/metrics/EventMetricLogger.kt
@@ -0,0 +1,19 @@
+package com.google.android.connecteddevice.metrics
+
+import android.content.Context
+import android.os.Build
+
+/** Logger to report Companion events to statsd */
+class EventMetricLogger(private val context: Context) {
+  private val uid = context.packageManager.getPackageUid(context.packageName, 0)
+  fun onSecureChannelEstablished() {
+    // TODO(b/273968556): unable to find related System API on Android Q build.
+    if(Build.VERSION.SDK_INT < Build.VERSION_CODES.R ) return
+    CompanionStatsLog.write(
+      CompanionStatsLog.COMPANION_STATUS_CHANGED,
+      uid,
+      CompanionStatsLog.COMPANION_STATUS_CHANGED__COMPANION_STATUS__SECURE_CHANNEL_ESTABLISHED,
+      0
+    )
+  }
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/NotificationMsgDelegate.java b/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/NotificationMsgDelegate.java
index fac77e2..ff1f229 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/NotificationMsgDelegate.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/NotificationMsgDelegate.java
@@ -39,7 +39,6 @@
 import com.google.android.connecteddevice.notificationmsg.common.SenderKey;
 import com.google.android.connecteddevice.notificationmsg.common.Utils;
 import com.google.android.connecteddevice.notificationmsg.proto.NotificationMsg.Action;
-import com.google.android.connecteddevice.notificationmsg.proto.NotificationMsg.AvatarIconSync;
 import com.google.android.connecteddevice.notificationmsg.proto.NotificationMsg.CarToPhoneMessage;
 import com.google.android.connecteddevice.notificationmsg.proto.NotificationMsg.ClearAppDataRequest;
 import com.google.android.connecteddevice.notificationmsg.proto.NotificationMsg.ConversationNotification;
@@ -75,8 +74,6 @@
    * ConnectedDevice#getDeviceId()}.
    */
   private String connectedDeviceBluetoothAddress;
-  /** Maps a Bitmap of a sender's Large Icon to the sender's unique key for 1-1 conversations. */
-  protected final Map<SenderKey, Bitmap> oneOnOneConversationAvatarMap = new HashMap<>();
 
   /** Tracks whether a projection application is active in the foreground. */
   private ProjectionStateListener projectionStateListener;
@@ -136,9 +133,7 @@
         // TODO (b/144924164): implement Action Request tracking logic.
         return;
       case AVATAR_ICON_SYNC:
-        storeIcon(
-            new ConversationKey(device.getDeviceId(), notificationKey),
-            message.getAvatarIconSync());
+        // Deprecated
         return;
       case PHONE_METADATA:
         connectedDeviceBluetoothAddress = message.getPhoneMetadata().getBluetoothDeviceAddress();
@@ -203,7 +198,6 @@
     // after the feature is stopped.
     cleanupMessagesAndNotifications(key -> true);
     projectionStateListener.destroy();
-    oneOnOneConversationAvatarMap.clear();
     appNameToChannel.clear();
     connectedDeviceBluetoothAddress = null;
   }
@@ -211,9 +205,6 @@
   protected void onDeviceDisconnected(String deviceId) {
     connectedDeviceBluetoothAddress = null;
     cleanupMessagesAndNotifications(key -> key.matches(deviceId));
-    oneOnOneConversationAvatarMap
-        .entrySet()
-        .removeIf(conversationKey -> conversationKey.getKey().matches(deviceId));
   }
 
   private void initializeNewConversation(
@@ -252,7 +243,7 @@
         convoKey,
         convoInfo,
         getChannelId(appDisplayName),
-        getAvatarIcon(convoKey, latestMessage),
+        getAvatarIcon(latestMessage),
         contentTextResourceId,
         defaultDisplayName,
         groupTitleSeparator,
@@ -285,7 +276,7 @@
         convoKey,
         convoInfo,
         getChannelId(convoInfo.getAppDisplayName()),
-        getAvatarIcon(convoKey, messagingStyleMessage),
+        getAvatarIcon(messagingStyleMessage),
         contentTextResourceId,
         defaultDisplayName,
         groupTitleSeparator,
@@ -299,40 +290,14 @@
   }
 
   @Nullable
-  private Bitmap getAvatarIcon(ConversationKey convoKey, MessagingStyleMessage message) {
-    ConversationNotificationInfo notificationInfo = notificationInfos.get(convoKey);
-    if (!notificationInfo.isGroupConvo()) {
-      return oneOnOneConversationAvatarMap.get(
-          SenderKey.createSenderKey(convoKey, message.getSender()));
-    } else if (!message.getSender().getAvatar().isEmpty()) {
+  private Bitmap getAvatarIcon(MessagingStyleMessage message) {
+    if (!message.getSender().getAvatar().isEmpty()) {
       byte[] iconArray = message.getSender().getAvatar().toByteArray();
       return BitmapFactory.decodeByteArray(iconArray, 0, iconArray.length);
     }
     return null;
   }
 
-  private void storeIcon(ConversationKey convoKey, AvatarIconSync iconSync) {
-    if (!Utils.isValidAvatarIconSync(iconSync)) {
-      logw(TAG, "storeIcon: invalid AvatarIconSync obj.");
-      return;
-    }
-    if (!notificationInfos.containsKey(convoKey)) {
-      logw(TAG, "storeIcon: no conversation found.");
-      return;
-    }
-    if (notificationInfos.get(convoKey).isGroupConvo()) {
-      return;
-    }
-    byte[] iconArray = iconSync.getPerson().getAvatar().toByteArray();
-    Bitmap bitmap = BitmapFactory.decodeByteArray(iconArray, /* offset= */ 0, iconArray.length);
-    if (bitmap != null) {
-      oneOnOneConversationAvatarMap.put(
-          SenderKey.createSenderKey(convoKey, iconSync.getPerson()), bitmap);
-    } else {
-      logw(TAG, "storeIcon: Bitmap could not be created from byteArray");
-    }
-  }
-
   private String getChannelId(String appDisplayName) {
     if (!appNameToChannel.containsKey(appDisplayName)) {
       appNameToChannel.put(appDisplayName, new NotificationChannelWrapper(appDisplayName));
@@ -346,19 +311,12 @@
 
   private void createNewMessage(
       String deviceAddress, MessagingStyleMessage messagingStyleMessage, ConversationKey convoKey) {
-    String appPackageName = notificationInfos.get(convoKey).getAppPackageName();
     Message message =
         Message.parseFromMessage(
             deviceAddress,
             messagingStyleMessage,
             SenderKey.createSenderKey(convoKey, messagingStyleMessage.getSender()));
     addMessageToNotificationInfo(message, convoKey);
-    AvatarIconSync iconSync =
-        AvatarIconSync.newBuilder()
-            .setPerson(messagingStyleMessage.getSender())
-            .setMessagingAppPackageName(appPackageName)
-            .build();
-    storeIcon(convoKey, iconSync);
   }
 
   private void clearAppData(String deviceId, String packageName) {
@@ -369,9 +327,6 @@
       return;
     }
     cleanupMessagesAndNotifications(key -> key.matches(deviceId));
-    oneOnOneConversationAvatarMap
-        .entrySet()
-        .removeIf(conversationKey -> conversationKey.getKey().matches(deviceId));
   }
 
   /** Creates notification channels per unique messaging application. */
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/NotificationMsgFeature.java b/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/NotificationMsgFeature.java
index 81469c4..45a371c 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/NotificationMsgFeature.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/NotificationMsgFeature.java
@@ -18,6 +18,7 @@
 
 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.logi;
 import static com.google.android.connecteddevice.util.SafeLog.logw;
 
 import android.content.Context;
@@ -58,6 +59,7 @@
     // clear all notifications and local data on start of the feature.
     notificationMsgDelegate.cleanupMessagesAndNotifications(key -> true);
     super.start();
+    logi(TAG, "Feature started");
   }
 
   @Override
@@ -66,6 +68,17 @@
     // after the feature is stopped.
     notificationMsgDelegate.onDestroy();
     super.stop();
+    logi(TAG, "Feature stopped");
+  }
+
+  @Override
+  public void onReady() {
+    logi(TAG, "Feature ready");
+  }
+
+  @Override
+  public void onNotReady() {
+    logi(TAG, "Feature not ready");
   }
 
   @Override
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 f3c4f47..c011372 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceFgUserService.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceFgUserService.kt
@@ -26,6 +26,7 @@
 import android.os.UserManager
 import com.google.android.connecteddevice.api.CompanionConnector
 import com.google.android.connecteddevice.api.Connector
+import com.google.android.connecteddevice.api.SafeConnector
 import com.google.android.connecteddevice.util.SafeLog.logd
 import com.google.android.connecteddevice.util.SafeLog.loge
 import java.time.Duration
@@ -91,7 +92,7 @@
   override fun onBind(intent: Intent): IBinder? {
     logd(TAG, "Service bound. Action: ${intent.action}")
     val action = intent.action ?: return null
-    if (action == ACTION_QUERY_API_VERSION) {
+    if (action == SafeConnector.ACTION_QUERY_API_VERSION) {
       logd(TAG, "Return binder version to remote process")
       return binderVersion.asBinder()
     }
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 045a3e8..bd36f00 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceService.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceService.java
@@ -19,13 +19,19 @@
 import static com.google.android.connecteddevice.util.SafeLog.logd;
 import static com.google.android.connecteddevice.util.SafeLog.loge;
 
+import android.annotation.SuppressLint;
+import android.content.BroadcastReceiver;
+import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.os.IBinder;
+import android.os.UserHandle;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import com.google.android.connecteddevice.R;
 import com.google.android.connecteddevice.api.CompanionConnector;
 import com.google.android.connecteddevice.api.Connector;
+import com.google.android.connecteddevice.api.FeatureConnector;
 import com.google.android.connecteddevice.core.DeviceController;
 import com.google.android.connecteddevice.core.FeatureCoordinator;
 import com.google.android.connecteddevice.core.MultiProtocolDeviceController;
@@ -39,9 +45,10 @@
 import com.google.android.connecteddevice.util.EventLog;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.List;
 import java.util.UUID;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /** Early start service that hosts the core companion platform. */
@@ -69,11 +76,20 @@
 
   private final AtomicBoolean isEveryFeatureInitialized = new AtomicBoolean(false);
 
-  private final ScheduledExecutorService scheduledExecutorService =
-      Executors.newSingleThreadScheduledExecutor();
+  private final ExecutorService databaseExecutor = Executors.newSingleThreadExecutor();
 
   private final ProtocolDelegate protocolDelegate = new ProtocolDelegate();
 
+  private final BroadcastReceiver userRemovedBroadcastReceiver =
+      new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+          logd(TAG, "Received USER_REMOVED broadcast.");
+          UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER);
+          onUserRemoved(userHandle);
+        }
+      };
+
   private LoggingManager loggingManager;
 
   private FeatureCoordinator featureCoordinator;
@@ -85,6 +101,7 @@
   private LoggingFeature loggingFeature;
 
   @Override
+  @SuppressLint("UnprotectedReceiver") // ACTION_USER_REMOVED is a protected broadcast.
   public void onCreate() {
     super.onCreate();
     logd(
@@ -113,8 +130,9 @@
     storage = new ConnectedDeviceStorage(this);
 
     initializeFeatureCoordinator();
-
     populateFeatures();
+    logd(TAG, "Registering broadcast receiver for intent " + Intent.ACTION_USER_REMOVED);
+    registerReceiver(userRemovedBroadcastReceiver, new IntentFilter(Intent.ACTION_USER_REMOVED));
   }
 
   private void initializeFeatureCoordinator() {
@@ -129,7 +147,7 @@
     OobRunner oobRunner = new OobRunner(protocolDelegate, oobProtocolName);
     DeviceController deviceController =
         new MultiProtocolDeviceController(
-            protocolDelegate, storage, oobRunner, associationUuid, enablePassenger);
+            this, protocolDelegate, storage, oobRunner, associationUuid, enablePassenger);
     featureCoordinator = new FeatureCoordinator(deviceController, storage, loggingManager);
     logd(TAG, "Wrapping FeatureCoordinator in legacy binders for backwards compatibility.");
   }
@@ -150,6 +168,24 @@
                 this, Connector.USER_TYPE_ALL, featureCoordinator));
   }
 
+  private void onUserRemoved(UserHandle userHandle) {
+    databaseExecutor.execute(
+        () -> {
+          FeatureCoordinator featurecoordinator = this.featureCoordinator;
+          if (featurecoordinator == null) {
+            logd(TAG, "User removed before feature coordinator is initiated. Ignored");
+            return;
+          }
+
+          int userId = userHandle.getIdentifier();
+          List<String> deviceIds = storage.getAssociatedDeviceIdsForUser(userId);
+          for (String deviceId : deviceIds) {
+            logd(TAG, "Delete data from database; userId=" + userId + ", deviceId=" + deviceId);
+            featurecoordinator.removeAssociatedDevice(deviceId);
+          }
+        });
+  }
+
   @Nullable
   @Override
   public IBinder onBind(Intent intent) {
@@ -164,7 +200,7 @@
         return protocolDelegate;
       case CompanionConnector.ACTION_BIND_FEATURE_COORDINATOR:
         return featureCoordinator;
-      case  ACTION_QUERY_API_VERSION:
+      case FeatureConnector.ACTION_QUERY_API_VERSION:
         logd(TAG, "Return binder version to remote process");
         return binderVersion.asBinder();
       default:
@@ -176,7 +212,8 @@
   @Override
   public void onDestroy() {
     logd(TAG, "Service was destroyed.");
-    scheduledExecutorService.shutdown();
+    unregisterReceiver(userRemovedBroadcastReceiver);
+    databaseExecutor.shutdown();
     cleanup();
     super.onDestroy();
   }
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 f6d2b14..f278259 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/TrunkService.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/service/TrunkService.java
@@ -50,10 +50,6 @@
   @VisibleForTesting
   static final int MAX_BIND_ATTEMPTS = 3;
 
-  // TODO(b/263392730): Move this to FeatureConnector so that client app has access.
-  protected static final String ACTION_QUERY_API_VERSION =
-      "com.google.android.connecteddevice.api.QUERY_API_VERSION";
-
   private static final Duration BIND_RETRY_DURATION = Duration.ofSeconds(1);
 
   private final Multiset<String> bindAttempts = HashMultiset.create();
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/system/SystemFeature.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/system/SystemFeature.kt
index c5e47a6..e9fec7f 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/system/SystemFeature.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/system/SystemFeature.kt
@@ -33,17 +33,48 @@
 import com.google.protobuf.ExtensionRegistryLite
 import com.google.protobuf.InvalidProtocolBufferException
 import java.nio.charset.StandardCharsets
+import java.util.UUID
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
 
-/** Feature responsible for system queries. */
-open class SystemFeature(
+/**
+ * Feature responsible for system queries.
+ *
+ * @param queryFeatureSupportOnConnection Feature IDs that should be queried when this SystemFeature
+ *   is notified of a connected device. The support status should be cached internally so that when
+ *   the support status is queried again later (by the actual feature), the result is immediately
+ *   available.
+ */
+open class SystemFeature
+// @VisibleForTesting
+internal constructor(
   context: Context,
   private val storage: ConnectedDeviceStorage,
-  private val connector: Connector
+  private val connector: Connector,
+  private val queryFeatureSupportOnConnection: List<UUID>,
 ) {
 
   private val bluetoothAdapter: BluetoothAdapter =
     context.getSystemService(BluetoothManager::class.java).adapter
 
+  constructor(
+    context: Context,
+    storage: ConnectedDeviceStorage,
+    connector: Connector,
+  ) : this(
+    context,
+    storage,
+    connector,
+    listOf(
+      // SecondDeviceSignInUrlFeature
+      //   This feature is started by UI (not always running in the background), so it has a limited
+      //   amount of time to initialize. We can speed up the process by preheating the system query
+      //   cache, namely querying the feature status here so when the feature checks, the response
+      //   is cached.
+      UUID.fromString("524a5d28-b208-449c-bb54-cd89498d3b1b"),
+    ),
+  )
+
   init {
     connector.featureId = SYSTEM_FEATURE_ID
     connector.callback =
@@ -73,16 +104,22 @@
   }
 
   private fun onSecureChannelEstablishedInternal(device: ConnectedDevice) {
-    logd(TAG, "Secure channel has been established. Issuing device name query.")
+    logd(TAG, "Secure channel has been established. ")
+    queryDeviceName(device)
+    queryFeatureSupportStatusToPreheatCache(device)
+  }
+
+  private fun queryDeviceName(device: ConnectedDevice) {
+    logd(TAG, "Issuing device name query.")
     val deviceNameQuery = SystemQuery.newBuilder().setType(DEVICE_NAME).build()
     connector.sendQuerySecurely(
       device,
       deviceNameQuery.toByteArray(),
-      /* parameters= */ null,
+      parameters = null,
       object : QueryCallback {
-        override fun onSuccess(response: ByteArray?) {
-          if (response?.isNotEmpty() != true) {
-            loge(TAG, "Received a null or empty device name query response. Ignoring.")
+        override fun onSuccess(response: ByteArray) {
+          if (response.isEmpty()) {
+            loge(TAG, "Received an empty device name query response. Ignoring.")
             return
           }
           val deviceName = String(response, StandardCharsets.UTF_8)
@@ -93,6 +130,14 @@
     )
   }
 
+  private fun queryFeatureSupportStatusToPreheatCache(device: ConnectedDevice) {
+    logd(TAG, "Issuing query for feature support status.")
+    // Ignore the result because we are only calling to preheat the status cache.
+    MainScope().launch {
+      val unused = connector.queryFeatureSupportStatuses(device, queryFeatureSupportOnConnection)
+    }
+  }
+
   private fun onQueryReceivedInternal(
     device: ConnectedDevice,
     queryId: Int,
@@ -133,16 +178,11 @@
       if (device.isAssociatedWithDriver) SystemUserRole.DRIVER else SystemUserRole.PASSENGER
     val response = SystemUserRoleResponse.newBuilder().setRole(role).build()
     logd(TAG, "Responding to query for user role with $role.")
-    connector.respondToQuerySecurely(
-      device,
-      queryId,
-      success = true,
-      response.toByteArray()
-    )
+    connector.respondToQuerySecurely(device, queryId, success = true, response.toByteArray())
   }
 
   private fun respondWithError(device: ConnectedDevice, queryId: Int) =
-    connector.respondToQuerySecurely(device, queryId, /* success= */ false, /* response= */ null)
+    connector.respondToQuerySecurely(device, queryId, success = false, response = null)
 
   companion object {
     private const val TAG = "SystemFeature"
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 f7b799c..c5e56e9 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceAgentService.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceAgentService.java
@@ -29,6 +29,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
@@ -36,6 +37,7 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.service.trust.GrantTrustResult;
 import android.service.trust.TrustAgentService;
 import androidx.annotation.VisibleForTesting;
 import com.google.android.connecteddevice.trust.api.ITrustedDeviceAgentDelegate;
@@ -157,15 +159,26 @@
       return;
     }
     logd(TAG, "Dismissing the lockscreen.");
-    grantTrust(
-        "Granting trust from escrow token for user.",
-        TRUST_DURATION_MS,
-        FLAG_GRANT_TRUST_DISMISS_KEYGUARD);
-    setManagingTrust(false);
-    if (trustedDeviceManager == null) {
-      loge(TAG, "Manager was null when device was unlocked. Ignoring.");
-      return;
-    }
+    // To avoid unreliable system lock status check impacting automated test metrics, invoking the
+    // user unlocked event before granting trust.
+    TrustedDeviceEventLog.onUserUnlocked();
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+      logd(TAG, "Grant trust with result callback.");
+      grantTrust(
+          "Granting trust from escrow token for user.",
+          TRUST_DURATION_MS,
+          FLAG_GRANT_TRUST_DISMISS_KEYGUARD,
+          (result) -> {
+            logd(TAG, "GrantTrust return with Result: " + result.getStatus() + ".");
+            if (result.getStatus() == GrantTrustResult.STATUS_UNLOCKED_BY_GRANT) {
+              notifyLockScreenDismissed();
+            }
+          });
+    } else {
+      grantTrust(
+          "Granting trust from escrow token for user.",
+          TRUST_DURATION_MS,
+          FLAG_GRANT_TRUST_DISMISS_KEYGUARD);
     // Other locking schemas, e.g. primary authentication, might keep the device locked even after
     // granting trust.
     if (keyguardManager == null || keyguardManager.isDeviceLocked()) {
@@ -175,7 +188,16 @@
               + "Skip the ACK message to the phone.");
       return;
     }
-    TrustedDeviceEventLog.onUserUnlocked();
+      notifyLockScreenDismissed();
+    }
+    setManagingTrust(false);
+  }
+
+  private void notifyLockScreenDismissed() {
+    if (trustedDeviceManager == null) {
+      loge(TAG, "Manager was null when device was unlocked. Ignoring.");
+      return;
+    }
     try {
       trustedDeviceManager.onUserUnlocked();
     } catch (RemoteException e) {
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceUiDelegateService.java b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceUiDelegateService.java
index 2d50787..3625605 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceUiDelegateService.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceUiDelegateService.java
@@ -31,7 +31,6 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
 import com.google.android.connecteddevice.service.MetaDataService;
 import com.google.android.connecteddevice.trust.api.IOnTrustedDeviceEnrollmentNotificationRequestListener;
 import com.google.android.connecteddevice.trust.api.ITrustedDeviceManager;
@@ -54,9 +53,6 @@
   /** {@code String} Content for the enrollment notification. */
   private static final String META_ENROLLMENT_NOTIFICATION_CONTENT =
       "com.google.android.connecteddevice.trust.enrollment_notification_content";
-  /** {@code Color} Color for the enrollment notification. */
-  private static final String META_ENROLLMENT_NOTIFICATION_COLOR =
-      "com.google.android.connecteddevice.trust.enrollment_notification_color";
 
   private NotificationManager notificationManager;
   private ITrustedDeviceManager trustedDeviceManager;
@@ -102,9 +98,6 @@
     Notification notification =
         new Notification.Builder(this, CHANNEL_ID)
             .setSmallIcon(requireMetaResourceId(META_ENROLLMENT_NOTIFICATION_ICON))
-            .setColor(
-                ContextCompat.getColor(
-                    getBaseContext(), requireMetaResourceId(META_ENROLLMENT_NOTIFICATION_COLOR)))
             .setContentTitle(requireMetaString(META_ENROLLMENT_NOTIFICATION_TITLE))
             .setContentText(requireMetaString(META_ENROLLMENT_NOTIFICATION_CONTENT))
             .setContentIntent(pendingIntent)
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceViewModel.java b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceViewModel.java
index 6ebabf5..4a25e53 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceViewModel.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceViewModel.java
@@ -241,6 +241,7 @@
    * @param device The associated device to be enrolled.
    */
   public void enrollTrustedDevice(AssociatedDevice device) {
+    updateTrustedDevicesFromServer();
     attemptInitiatingEnrollment(device);
   }
 
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 9742d70..482bffc 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
@@ -11,6 +11,8 @@
 import android.os.ParcelUuid
 import android.os.RemoteException
 import androidx.test.core.app.ApplicationProvider
+import com.google.android.companionprotos.FeatureSupportResponse
+import com.google.android.companionprotos.FeatureSupportStatus
 import com.google.android.companionprotos.Query
 import com.google.android.companionprotos.QueryResponse
 import com.google.android.companionprotos.SystemQuery
@@ -39,6 +41,11 @@
 import java.nio.charset.StandardCharsets
 import java.util.UUID
 import kotlin.test.fail
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -58,15 +65,17 @@
 
   private val context = FakeContext(mockPackageManager)
 
+  private lateinit var featureId: UUID
   private lateinit var defaultConnector: CompanionConnector
 
   @Before
   fun setUp() {
+    featureId = UUID.randomUUID()
     defaultConnector =
       CompanionConnector(context).apply {
         featureCoordinator = mockFeatureCoordinator
         callback = mockCallback
-        featureId = ParcelUuid(UUID.randomUUID())
+        featureId = ParcelUuid(this@CompanionConnectorTest.featureId)
       }
   }
 
@@ -346,13 +355,7 @@
         featureCoordinator = mockFeatureCoordinator
         this.featureId = featureId
       }
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
+    val device = createConnectedDevice()
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
 
     connector.connect()
@@ -402,13 +405,7 @@
         featureCoordinator = mockFeatureCoordinator
         this.featureId = featureId
       }
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
+    val device = createConnectedDevice()
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
 
     connector.connect()
@@ -424,13 +421,7 @@
         featureCoordinator = mockFeatureCoordinator
         this.featureId = featureId
       }
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ false,
-        /* hasSecureChannel= */ false
-      )
+    val device = createConnectedDevice(belongsToDriver = false)
     whenever(mockFeatureCoordinator.connectedDevicesForPassengers).thenReturn(listOf(device))
 
     connector.connect()
@@ -446,13 +437,7 @@
         featureCoordinator = mockFeatureCoordinator
         this.featureId = featureId
       }
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
+    val device = createConnectedDevice()
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
 
     connector.connect()
@@ -463,13 +448,7 @@
   @Test
   fun connect_deviceCallbacksNotRegisteredWhenMissingFeatureId() {
     defaultConnector.featureId = null
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
+    val device = createConnectedDevice()
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
 
     defaultConnector.connect()
@@ -479,13 +458,7 @@
 
   @Test
   fun disconnect_unregistersFeatureCoordinatorCallbacks() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
+    val device = createConnectedDevice()
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
 
     defaultConnector.disconnect()
@@ -500,13 +473,7 @@
   @Test
   fun disconnect_featureCoordinatorWithoutFeatureIdDoesNotThrow() {
     defaultConnector.featureId = null
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
+    val device = createConnectedDevice()
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
 
     defaultConnector.disconnect()
@@ -514,13 +481,7 @@
 
   @Test
   fun disconnect_featureCoordinatorRemoteExceptionIsCaught() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
+    val device = createConnectedDevice()
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
     whenever(mockFeatureCoordinator.unregisterConnectionCallback(any()))
       .thenThrow(RemoteException())
@@ -530,13 +491,7 @@
 
   @Test
   fun onDeviceConnected_registersDeviceCallbackWithFeatureId() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
+    val device = createConnectedDevice()
 
     defaultConnector.connect()
 
@@ -551,13 +506,7 @@
 
   @Test
   fun onDeviceConnected_doesNotRegisterDeviceCallbackWithoutFeatureId() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
+    val device = createConnectedDevice()
     defaultConnector.featureId = null
     defaultConnector.connect()
 
@@ -571,13 +520,7 @@
 
   @Test
   fun onDeviceDisconnected_invokedWhenDeviceDisconnects() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
+    val device = createConnectedDevice()
     defaultConnector.connect()
 
     argumentCaptor<IConnectionCallback> {
@@ -590,13 +533,7 @@
 
   @Test
   fun onDeviceDisconnected_unregistersDeviceCallback() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
+    val device = createConnectedDevice()
     defaultConnector.connect()
 
     argumentCaptor<IConnectionCallback> {
@@ -610,20 +547,8 @@
 
   @Test
   fun onSecureChannelEstablished_invokedWhenChannelEstablished() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
-    val secureDevice =
-      ConnectedDevice(
-        device.deviceId,
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice()
+    val secureDevice = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     defaultConnector.connect()
 
@@ -637,13 +562,7 @@
 
   @Test
   fun onSecureChannelEstablished_invokedOnStartupIfChannelAlreadyEstablished() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     defaultConnector.connect()
 
@@ -652,13 +571,7 @@
 
   @Test
   fun onSecureChannelEstablished_invokedOnDeviceConnectedIfChannelAlreadyEstablished() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     defaultConnector.connect()
 
     argumentCaptor<IConnectionCallback> {
@@ -670,13 +583,7 @@
 
   @Test
   fun onDeviceError_invokedOnError() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
+    val device = createConnectedDevice()
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     defaultConnector.connect()
     val error = -1
@@ -748,13 +655,7 @@
 
   @Test
   fun sendMessageSecurelyWithId_sendsMessageSecurelyToDevice() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
     defaultConnector.connect()
     val message = ByteUtils.randomBytes(10)
@@ -773,13 +674,7 @@
 
   @Test
   fun sendMessageSecurely_sendsMessageSecurelyToDevice() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     defaultConnector.connect()
     val message = ByteUtils.randomBytes(10)
 
@@ -809,13 +704,7 @@
 
   @Test
   fun onMessageFailedToSend_invokedWhenRemoteExceptionThrownId() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     whenever(mockFeatureCoordinator.sendMessage(any(), any())).thenThrow(RemoteException())
     val message = ByteUtils.randomBytes(10)
@@ -828,13 +717,7 @@
 
   @Test
   fun onMessageFailedToSend_invokedWhenRemoteExceptionThrown() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     whenever(mockFeatureCoordinator.sendMessage(any(), any())).thenThrow(RemoteException())
     val message = ByteUtils.randomBytes(10)
@@ -871,13 +754,7 @@
   @Test
   fun onMessageFailedToSend_invokedWhenSendMessageCalledBeforeServiceConnection() {
     defaultConnector.featureCoordinator = null
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     val message = ByteUtils.randomBytes(10)
 
     defaultConnector.sendMessageSecurely(device, message)
@@ -887,13 +764,7 @@
 
   @Test
   fun onMessageReceived_invokedWhenGenericMessageReceived() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     val message = ByteUtils.randomBytes(10)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     defaultConnector.connect()
@@ -916,13 +787,7 @@
 
   @Test
   fun getConnectedDeviceById_returnsDeviceWhenConnected() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
+    val device = createConnectedDevice()
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
     defaultConnector.connect()
 
@@ -931,13 +796,7 @@
 
   @Test
   fun getConnectedDeviceById_returnsNullWhenNotConnected() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ false
-      )
+    val device = createConnectedDevice()
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     defaultConnector.connect()
 
@@ -961,13 +820,7 @@
 
   @Test
   fun sendQuerySecurely_sendsQueryToOwnFeatureId() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
     val request = ByteUtils.randomBytes(10)
     val parameters = ByteUtils.randomBytes(10)
@@ -986,13 +839,7 @@
 
   @Test
   fun sendQuery_queryCallbackOnSuccessInvoked() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     val request = ByteUtils.randomBytes(10)
     val response = ByteUtils.randomBytes(10)
     val parameters = ByteUtils.randomBytes(10)
@@ -1033,13 +880,7 @@
 
   @Test
   fun sendQuery_queryCallbackOnErrorInvoked() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
     defaultConnector.connect()
@@ -1080,13 +921,7 @@
 
   @Test
   fun sendQuery_queryCallbackNotInvokedOnDifferentQueryIdResponse() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
     defaultConnector.connect()
@@ -1142,13 +977,7 @@
   @Test
   fun sendQuery_queryCallbackOnQueryNotSentInvokedBeforeServiceConnectionWithDevice() {
     defaultConnector.featureCoordinator = null
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     val request = ByteUtils.randomBytes(10)
     val parameters = ByteUtils.randomBytes(10)
     val callback = mock<Connector.QueryCallback>()
@@ -1160,13 +989,7 @@
 
   @Test
   fun sendQuery_queryCallbackOnQueryNotSentInvokedIfSendMessageThrowsRemoteException() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
     whenever(mockFeatureCoordinator.sendMessage(any(), any())).thenThrow(RemoteException())
@@ -1182,13 +1005,7 @@
 
   @Test
   fun sendQuery_queryCallbackOnQueryNotSentInvokedIfDeviceNotFound() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     defaultConnector.connect()
     val request = ByteUtils.randomBytes(10)
     val parameters = ByteUtils.randomBytes(10)
@@ -1201,13 +1018,7 @@
 
   @Test
   fun onQueryReceived_invokedWithQueryFields() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
     defaultConnector.connect()
@@ -1241,38 +1052,21 @@
 
   @Test
   fun respondToQuery_doesNotSendResponseWithUnrecognizedQueryId() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
     defaultConnector.connect()
     val nonExistentQueryId = 0
     val response = ByteUtils.randomBytes(10)
 
-    defaultConnector.respondToQuerySecurely(
-      device,
-      nonExistentQueryId,
-      success = true,
-      response
-    )
+    defaultConnector.respondToQuerySecurely(device, nonExistentQueryId, success = true, response)
 
     verify(mockFeatureCoordinator, never()).sendMessage(any(), any())
   }
 
   @Test
   fun respondToQuery_sendsResponseToSenderIfSameFeatureId() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
     defaultConnector.connect()
@@ -1320,13 +1114,7 @@
 
   @Test
   fun respondToQuery_sendResponseToSenderIfDifferentFeatureId() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
     defaultConnector.connect()
@@ -1376,13 +1164,7 @@
   @Test
   fun respondToQuery_doesNotThrowBeforeServiceConnectionWithDevice() {
     defaultConnector.featureCoordinator = null
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     val queryId = 0
     val response = ByteUtils.randomBytes(10)
 
@@ -1391,13 +1173,7 @@
 
   @Test
   fun retrieveCompanionApplicationName_sendsAppNameQueryToSystemFeature() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
     defaultConnector.connect()
@@ -1420,13 +1196,7 @@
 
   @Test
   fun retrieveCompanionApplicationName_appNameCallbackOnNameReceivedInvokedWithName() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
     defaultConnector.connect()
@@ -1466,13 +1236,7 @@
 
   @Test
   fun retrieveCompanionApplicationName_appNameCallbackOnErrorInvokedWithEmptyResponse() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     defaultConnector.connect()
     val callbackCaptor =
@@ -1512,13 +1276,7 @@
 
   @Test
   fun retrieveCompanionApplicationName_appNameCallbackOnErrorInvokedWithErrorResponse() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     defaultConnector.connect()
     val callbackCaptor =
@@ -1557,13 +1315,7 @@
 
   @Test
   fun retrieveCompanionApplicationName_appNameCallbackOnErrorInvokedWhenQueryFailedToSend() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
     whenever(mockFeatureCoordinator.sendMessage(any(), any())).thenThrow(RemoteException())
@@ -1577,13 +1329,7 @@
 
   @Test
   fun createLocalConnector_createsConnectorWithFeatureCoordinator() {
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
 
     val connector =
@@ -1607,13 +1353,7 @@
       CompanionConnector(context, userType = USER_TYPE_DRIVER).apply {
         featureCoordinator = mockFeatureCoordinator
       }
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
 
     assertThat(connector.connectedDevices).containsExactly(device)
@@ -1625,13 +1365,7 @@
       CompanionConnector(context, userType = USER_TYPE_PASSENGER).apply {
         featureCoordinator = mockFeatureCoordinator
       }
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.connectedDevicesForPassengers).thenReturn(listOf(device))
 
     assertThat(connector.connectedDevices).containsExactly(device)
@@ -1643,13 +1377,7 @@
       CompanionConnector(context, userType = USER_TYPE_ALL).apply {
         featureCoordinator = mockFeatureCoordinator
       }
-    val device =
-      ConnectedDevice(
-        UUID.randomUUID().toString(),
-        /* deviceName= */ "",
-        /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
-      )
+    val device = createConnectedDevice(hasSecureChannel = true)
     whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
 
     assertThat(connector.connectedDevices).containsExactly(device)
@@ -1904,6 +1632,250 @@
     verify(mockCallback).onFailedToConnect()
   }
 
+  @Test
+  fun isFeatureSupported_noFeatureId_notSupported() = runBlocking {
+    val connector = CompanionConnector(context)
+    val device = createConnectedDevice()
+
+    assertThat(connector.isFeatureSupported(device)).isFalse()
+  }
+
+  @Test
+  fun isFeatureSupported_sendsSystemQuery() = runBlocking {
+    val device = createConnectedDevice(hasSecureChannel = true)
+    whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
+    whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
+    defaultConnector.connect()
+
+    // Immediate execution to send out the query.
+    CoroutineScope(Dispatchers.Main.immediate).launch {
+      val unused = defaultConnector.isFeatureSupported(device)
+    }
+
+    // Assertion: successfully parse the outbound message as a SystemQuery for the feature status.
+    val messageCaptor =
+      argumentCaptor<DeviceMessage> {
+        verify(mockFeatureCoordinator).sendMessage(eq(device), capture())
+      }
+    val query =
+      Query.parseFrom(messageCaptor.firstValue.message, ExtensionRegistryLite.getEmptyRegistry())
+    val systemQuery = SystemQuery.parseFrom(query.request, ExtensionRegistryLite.getEmptyRegistry())
+    assertThat(systemQuery.type).isEqualTo(SystemQueryType.IS_FEATURE_SUPPORTED)
+    assertThat(systemQuery.payloadsList.size).isEqualTo(1)
+
+    val queriedFeatureId =
+      UUID.fromString(
+        systemQuery.payloadsList.first().toByteArray().toString(StandardCharsets.UTF_8)
+      )
+    assertThat(queriedFeatureId).isEqualTo(featureId)
+  }
+
+  @Test
+  fun isFeatureSupported_emptyResponse_notSupported() {
+    // Arrange.
+    val device = createConnectedDevice(hasSecureChannel = true)
+    whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
+    whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
+    defaultConnector.connect()
+
+    // Action - immediate execution to send out the query.
+    val deferred =
+      CoroutineScope(Dispatchers.Main.immediate).async {
+        defaultConnector.isFeatureSupported(device)
+      }
+
+    // Generate an empty response.
+    val response = ByteArray(0)
+
+    val messageCaptor =
+      argumentCaptor<DeviceMessage> {
+        verify(mockFeatureCoordinator).sendMessage(eq(device), capture())
+      }
+    val query =
+      Query.parseFrom(messageCaptor.firstValue.message, ExtensionRegistryLite.getEmptyRegistry())
+    val queryResponse =
+      QueryResponse.newBuilder()
+        .setQueryId(query.id)
+        .setSuccess(true)
+        .setResponse(ByteString.copyFrom(response))
+        .build()
+    val deviceMessage =
+      DeviceMessage.createOutgoingMessage(
+        defaultConnector.featureId?.uuid,
+        /* isMessageEncrypted= */ true,
+        DeviceMessage.OperationType.QUERY_RESPONSE,
+        queryResponse.toByteArray()
+      )
+    val callbackCaptor =
+      argumentCaptor<IDeviceCallback> {
+        verify(mockFeatureCoordinator)
+          .registerDeviceCallback(eq(device), eq(defaultConnector.featureId), capture())
+      }
+    callbackCaptor.firstValue.onMessageReceived(device, deviceMessage)
+
+    // Assert
+    runBlocking { assertThat(deferred.await()).isFalse() }
+  }
+
+  @Test
+  fun isFeatureSupported_nonProtoResponse_notSupported() {
+    // Arrange.
+    val device = createConnectedDevice(hasSecureChannel = true)
+    whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
+    whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
+    defaultConnector.connect()
+
+    // Action - immediate execution to send out the query.
+    val deferred =
+      CoroutineScope(Dispatchers.Main.immediate).async {
+        defaultConnector.isFeatureSupported(device)
+      }
+
+    // Generate a response that cannot be parsed (should at least contain UUID, which is 16 bytes).
+    val response = ByteUtils.randomBytes(10)
+
+    val messageCaptor =
+      argumentCaptor<DeviceMessage> {
+        verify(mockFeatureCoordinator).sendMessage(eq(device), capture())
+      }
+    val query =
+      Query.parseFrom(messageCaptor.firstValue.message, ExtensionRegistryLite.getEmptyRegistry())
+    val queryResponse =
+      QueryResponse.newBuilder()
+        .setQueryId(query.id)
+        .setSuccess(true)
+        .setResponse(ByteString.copyFrom(response))
+        .build()
+    val deviceMessage =
+      DeviceMessage.createOutgoingMessage(
+        defaultConnector.featureId?.uuid,
+        /* isMessageEncrypted= */ true,
+        DeviceMessage.OperationType.QUERY_RESPONSE,
+        queryResponse.toByteArray()
+      )
+    val callbackCaptor =
+      argumentCaptor<IDeviceCallback> {
+        verify(mockFeatureCoordinator)
+          .registerDeviceCallback(eq(device), eq(defaultConnector.featureId), capture())
+      }
+    callbackCaptor.firstValue.onMessageReceived(device, deviceMessage)
+
+    // Assert
+    runBlocking { assertThat(deferred.await()).isFalse() }
+  }
+
+  @Test
+  fun isFeatureSupported_notSupportedResponse_notSupported() {
+    // Arrange.
+    val device = createConnectedDevice(hasSecureChannel = true)
+    whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
+    whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
+    defaultConnector.connect()
+
+    // Action - immediate execution to send out the query.
+    val deferred =
+      CoroutineScope(Dispatchers.Main.immediate).async {
+        defaultConnector.isFeatureSupported(device)
+      }
+
+    val status =
+      FeatureSupportStatus.newBuilder().run {
+        featureId = defaultConnector.featureId!!.uuid.toString()
+        isSupported = false
+        build()
+      }
+    val response =
+      FeatureSupportResponse.newBuilder().run {
+        addStatuses(status)
+        build()
+      }
+
+    val messageCaptor =
+      argumentCaptor<DeviceMessage> {
+        verify(mockFeatureCoordinator).sendMessage(eq(device), capture())
+      }
+    val query =
+      Query.parseFrom(messageCaptor.firstValue.message, ExtensionRegistryLite.getEmptyRegistry())
+    val queryResponse =
+      QueryResponse.newBuilder()
+        .setQueryId(query.id)
+        .setSuccess(true)
+        .setResponse(ByteString.copyFrom(response.toByteArray()))
+        .build()
+    val deviceMessage =
+      DeviceMessage.createOutgoingMessage(
+        defaultConnector.featureId?.uuid,
+        /* isMessageEncrypted= */ true,
+        DeviceMessage.OperationType.QUERY_RESPONSE,
+        queryResponse.toByteArray()
+      )
+    val callbackCaptor =
+      argumentCaptor<IDeviceCallback> {
+        verify(mockFeatureCoordinator)
+          .registerDeviceCallback(eq(device), eq(defaultConnector.featureId), capture())
+      }
+    callbackCaptor.firstValue.onMessageReceived(device, deviceMessage)
+
+    // Assert
+    runBlocking { assertThat(deferred.await()).isFalse() }
+  }
+
+  @Test
+  fun isFeatureSupported_supportedResponse_featureSupported() {
+    // Arrange.
+    val device = createConnectedDevice(hasSecureChannel = true)
+    whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
+    whenever(mockFeatureCoordinator.allConnectedDevices).thenReturn(listOf(device))
+    defaultConnector.connect()
+
+    // Action - immediate execution to send out the query.
+    val deferred =
+      CoroutineScope(Dispatchers.Main.immediate).async {
+        defaultConnector.isFeatureSupported(device)
+      }
+
+    val status =
+      FeatureSupportStatus.newBuilder().run {
+        featureId = defaultConnector.featureId!!.uuid.toString()
+        isSupported = true
+        build()
+      }
+    val response =
+      FeatureSupportResponse.newBuilder().run {
+        addStatuses(status)
+        build()
+      }
+
+    val messageCaptor =
+      argumentCaptor<DeviceMessage> {
+        verify(mockFeatureCoordinator).sendMessage(eq(device), capture())
+      }
+    val query =
+      Query.parseFrom(messageCaptor.firstValue.message, ExtensionRegistryLite.getEmptyRegistry())
+    val queryResponse =
+      QueryResponse.newBuilder()
+        .setQueryId(query.id)
+        .setSuccess(true)
+        .setResponse(ByteString.copyFrom(response.toByteArray()))
+        .build()
+    val deviceMessage =
+      DeviceMessage.createOutgoingMessage(
+        defaultConnector.featureId?.uuid,
+        /* isMessageEncrypted= */ true,
+        DeviceMessage.OperationType.QUERY_RESPONSE,
+        queryResponse.toByteArray()
+      )
+    val callbackCaptor =
+      argumentCaptor<IDeviceCallback> {
+        verify(mockFeatureCoordinator)
+          .registerDeviceCallback(eq(device), eq(defaultConnector.featureId), capture())
+      }
+    callbackCaptor.firstValue.onMessageReceived(device, deviceMessage)
+
+    // Assert
+    runBlocking { assertThat(deferred.await()).isTrue() }
+  }
+
   private fun setQueryIntentServicesAnswer(answer: Answer<List<ResolveInfo>>) {
     whenever(mockPackageManager.queryIntentServices(any(), any<Int>())).thenAnswer(answer)
   }
@@ -1968,5 +1940,16 @@
     private const val PACKAGE_NAME = "com.test.package"
     private const val BG_NAME = "background"
     private const val FG_NAME = "foreground"
+
+    private fun createConnectedDevice(
+      belongsToDriver: Boolean = true,
+      hasSecureChannel: Boolean = false
+    ) =
+      ConnectedDevice(
+        UUID.randomUUID().toString(),
+        /* deviceName= */ "",
+        /* belongsToDriver= */ belongsToDriver,
+        hasSecureChannel,
+      )
   }
 }
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 538743a..36802e4 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
@@ -4,6 +4,11 @@
 import android.os.ParcelUuid
 import com.google.android.connecteddevice.model.AssociatedDevice
 import com.google.android.connecteddevice.model.ConnectedDevice
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.UUID
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.guava.future
 
 /** Fake implementation of a [Connector] to be used in tests. */
 open class FakeConnector : Connector {
@@ -64,6 +69,17 @@
     response: ByteArray?
   ) {}
 
+  override suspend fun isFeatureSupported(device: ConnectedDevice): Boolean = featureId != null
+
+  override suspend fun queryFeatureSupportStatuses(
+    device: ConnectedDevice,
+    queriedFeatures: List<UUID>
+  ): List<Pair<UUID, Boolean>> = emptyList()
+
+  override fun isFeatureSupportedFuture(device: ConnectedDevice): ListenableFuture<Boolean> {
+    return CoroutineScope(Dispatchers.Main).future { isFeatureSupported(device) }
+  }
+
   override fun getConnectedDeviceById(deviceId: String): ConnectedDevice? {
     return connectedDevices.find { it.deviceId == deviceId }
   }
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/FeatureConnectorTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/FeatureConnectorTest.kt
new file mode 100644
index 0000000..1d547b9
--- /dev/null
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/FeatureConnectorTest.kt
@@ -0,0 +1,856 @@
+package com.google.android.connecteddevice.api
+
+import android.content.ComponentName
+import android.content.ContextWrapper
+import android.content.Intent
+import android.content.ServiceConnection
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.content.pm.ServiceInfo
+import android.os.IInterface
+import android.os.Looper
+import android.os.ParcelUuid
+import androidx.test.core.app.ApplicationProvider
+import com.google.android.companionprotos.Query
+import com.google.android.companionprotos.QueryResponse
+import com.google.android.companionprotos.SystemQuery
+import com.google.android.companionprotos.SystemQueryType
+import com.google.android.connecteddevice.api.SafeConnector.Companion.ACTION_BIND_FEATURE_COORDINATOR
+import com.google.android.connecteddevice.api.SafeConnector.Companion.ACTION_QUERY_API_VERSION
+import com.google.android.connecteddevice.api.SafeConnector.QueryCallback
+import com.google.android.connecteddevice.api.external.ISafeBinderVersion
+import com.google.android.connecteddevice.api.external.ISafeFeatureCoordinator
+import com.google.android.connecteddevice.api.external.ISafeOnAssociatedDevicesRetrievedListener
+import com.google.android.connecteddevice.api.external.ISafeOnLogRequestedListener
+import com.google.android.connecteddevice.core.util.mockToBeAlive
+import com.google.android.connecteddevice.util.ByteUtils
+import com.google.common.truth.Truth.assertThat
+import com.google.protobuf.ExtensionRegistryLite
+import com.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.argumentCaptor
+import com.nhaarman.mockitokotlin2.eq
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.never
+import com.nhaarman.mockitokotlin2.spy
+import com.nhaarman.mockitokotlin2.verify
+import com.nhaarman.mockitokotlin2.whenever
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.stubbing.Answer
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+
+@RunWith(RobolectricTestRunner::class)
+class FeatureConnectorTest {
+  private val mockPackageManager = mock<PackageManager>()
+
+  private val context = FakeContext(mockPackageManager)
+
+  private val mockCallback = mock<SafeConnector.Callback>()
+
+  private val mockSafeFeatureCoordinator = mockToBeAlive<ISafeFeatureCoordinator>()
+
+  private val mockFeatureCoordinator = mockToBeAlive<IFeatureCoordinator>()
+
+  protected var mockPlatformVersion = 1
+
+  private val testFeatureId = ParcelUuid(UUID.randomUUID())
+
+  private val testDeviceId = UUID.randomUUID().toString()
+
+  private val testMessage = ByteUtils.randomBytes(10)
+
+  private val testCoordinatorProxy = spy(TestCompanionApiProxy(true, listOf(testDeviceId)))
+
+  @Test
+  fun onInit_negativeVersions_aborts() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = -1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, -1)
+
+    assertThat(context.bindingActions)
+      .containsExactly(ACTION_QUERY_API_VERSION, ACTION_BIND_FEATURE_COORDINATOR)
+    assertThat(connector.coordinatorProxy).isNull()
+  }
+
+  @Test
+  fun onInit_clientV1_platformV0_bindToFeatureCoordinator() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 0
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    assertThat(connector.coordinatorProxy is LegacyApiProxy).isTrue()
+    assertThat(context.bindingActions)
+      .containsExactly(ACTION_QUERY_API_VERSION, ACTION_BIND_FEATURE_COORDINATOR)
+  }
+
+  @Test
+  fun onInit_clientV1_platformV1_bindToFeatureCoordinator() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    assertThat(connector.coordinatorProxy is SafeApiProxy).isTrue()
+    assertThat(context.bindingActions)
+      .containsExactly(ACTION_QUERY_API_VERSION, ACTION_BIND_FEATURE_COORDINATOR)
+  }
+
+  @Test
+  fun onInit_clientV1_platformV2_bindToFeatureCoordinator() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 2
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    assertThat(connector.coordinatorProxy is SafeApiProxy).isTrue()
+    assertThat(context.bindingActions)
+      .containsExactly(ACTION_QUERY_API_VERSION, ACTION_BIND_FEATURE_COORDINATOR)
+  }
+
+  @Test
+  fun onInit_clientV2_platformV0_bindToFeatureCoordinator() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 0
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 2)
+
+    assertThat(connector.coordinatorProxy is LegacyApiProxy).isTrue()
+    assertThat(context.bindingActions)
+      .containsExactly(ACTION_QUERY_API_VERSION, ACTION_BIND_FEATURE_COORDINATOR)
+  }
+
+  @Test
+  fun onInit_clientV1_platformV1_incorrectServiceIssuesApiNotSupportedCallback() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val incorrectServiceContext = IncorrectServiceContext(mockPackageManager)
+    val connector = FeatureConnector(incorrectServiceContext, testFeatureId, mockCallback, 1)
+
+    assertThat(connector.coordinatorProxy).isNull()
+    assertThat(connector.platformVersion).isNull()
+    assertThat(incorrectServiceContext.bindingActions).containsExactly(ACTION_QUERY_API_VERSION)
+    verify(mockCallback).onFailedToConnect()
+  }
+
+  @Test
+  fun onInit_clientV2_platformV1_issuesApiNotSupportedCallback() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 2)
+
+    assertThat(connector.coordinatorProxy).isNull()
+    assertThat(connector.platformVersion).isNull()
+    assertThat(context.bindingActions).containsExactly(ACTION_QUERY_API_VERSION)
+    verify(mockCallback).onApiNotSupported()
+  }
+
+  @Test
+  fun onInit_clientV2_platformV2_bindToFeatureCoordinator() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 2
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 2)
+
+    assertThat(connector.coordinatorProxy is SafeApiProxy).isTrue()
+    assertThat(context.bindingActions)
+      .containsExactly(ACTION_QUERY_API_VERSION, ACTION_BIND_FEATURE_COORDINATOR)
+  }
+
+  @Test
+  fun onInit_clientV2_platformV3_bindToFeatureCoordinator() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 3
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 2)
+
+    assertThat(connector.coordinatorProxy is SafeApiProxy).isTrue()
+    assertThat(context.bindingActions)
+      .containsExactly(ACTION_QUERY_API_VERSION, ACTION_BIND_FEATURE_COORDINATOR)
+  }
+
+  @Test
+  fun onFailedToConnect_invokedIfNullIntent() {
+    val nullIntentAnswer = 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 -> {
+          resolveInfo.serviceInfo.name = FC_NAME
+          listOf(resolveInfo)
+        }
+        else -> listOf()
+      }
+    }
+    setQueryIntentServicesAnswer(nullIntentAnswer)
+    mockPlatformVersion = 0
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    assertThat(connector.coordinatorProxy).isNull()
+    verify(mockCallback).onFailedToConnect()
+  }
+
+  @Test
+  fun onSuccessfulInit_doesNotRetryBind() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    assertThat(connector.bindAttempts == 0).isTrue()
+    assertThat(context.bindingActions)
+      .containsExactly(ACTION_QUERY_API_VERSION, ACTION_BIND_FEATURE_COORDINATOR)
+  }
+
+  @Test
+  fun onFailedToConnect_invokedAfterBindingRetryLimitExceeded() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val failingContext = FailingContext(mockPackageManager)
+    val shadowLooper = Shadows.shadowOf(Looper.getMainLooper())
+    val connector = FeatureConnector(failingContext, testFeatureId, mockCallback, 1)
+
+    repeat(FeatureConnector.MAX_BIND_ATTEMPTS) { shadowLooper.runToEndOfTasks() }
+
+    assertThat(connector.coordinatorProxy).isNull()
+    assertThat(failingContext.serviceConnection).contains(connector.versionCheckConnection)
+    verify(mockCallback).onFailedToConnect()
+  }
+
+  @Test
+  fun onFailedToConnect_invokedAfterBindingRetryLimitExceeded_afterSuccessfulVersionQueryBind() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 0
+    val flakyContext = FailingLegacyContext(mockPackageManager)
+    val shadowLooper = Shadows.shadowOf(Looper.getMainLooper())
+    val connector = FeatureConnector(flakyContext, testFeatureId, mockCallback, 1)
+
+    repeat(FeatureConnector.MAX_BIND_ATTEMPTS) { shadowLooper.runToEndOfTasks() }
+
+    assertThat(connector.coordinatorProxy).isNull()
+    assertThat(connector.bindAttempts).isEqualTo(FeatureConnector.MAX_BIND_ATTEMPTS + 1)
+    assertThat(flakyContext.serviceConnection).contains(connector.versionCheckConnection)
+    assertThat(flakyContext.serviceConnection).contains(connector.featureCoordinatorConnection)
+    verify(mockCallback).onFailedToConnect()
+  }
+
+  @Test
+  fun bindAttemptsRetainsCorrectValue_afterVersionQueryBindAndFeatureCoordinatorBindExperienceFailures() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 0
+    val flakyContext = FlakyLegacyContext(mockPackageManager)
+    val shadowLooper = Shadows.shadowOf(Looper.getMainLooper())
+    val connector = FeatureConnector(flakyContext, testFeatureId, mockCallback, 1)
+
+    // Should repeat once for version query bind attempt, then reset, then MAX_BIND_ATTEMPTS times
+    // for feature coordinator bind attempt.
+    repeat(FeatureConnector.MAX_BIND_ATTEMPTS + 1) { shadowLooper.runToEndOfTasks() }
+
+    assertThat(connector.coordinatorProxy).isNull()
+    assertThat(connector.bindAttempts).isEqualTo(FeatureConnector.MAX_BIND_ATTEMPTS + 1)
+    assertThat(flakyContext.serviceConnection)
+      .containsExactly(
+        connector.versionCheckConnection,
+        connector.versionCheckConnection,
+        connector.featureCoordinatorConnection,
+        connector.featureCoordinatorConnection,
+        connector.featureCoordinatorConnection,
+        connector.featureCoordinatorConnection
+      )
+    verify(mockCallback).onFailedToConnect()
+  }
+
+  @Test
+  fun onDisconnected_invokedOnVersionCheckServiceDisconnected() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    connector.versionCheckConnection.onServiceDisconnected(
+      ComponentName(PACKAGE_NAME, VERSION_NAME)
+    )
+    verify(mockCallback).onDisconnected()
+  }
+
+  @Test
+  fun onDisconnected_invokedOnVersionCheckDeadBinding() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    connector.versionCheckConnection.onBindingDied(ComponentName(PACKAGE_NAME, VERSION_NAME))
+    verify(mockCallback).onDisconnected()
+  }
+
+  @Test
+  fun onDisconnected_invokedOnFeatureCoordinatorServiceDisconnected() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    connector.featureCoordinatorConnection.onServiceDisconnected(
+      ComponentName(PACKAGE_NAME, FC_NAME)
+    )
+    verify(mockCallback).onDisconnected()
+  }
+
+  @Test
+  fun onFailedToConnect_invokedOnFeatureCoordinatorNullBinding() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    connector.featureCoordinatorConnection.onNullBinding(ComponentName(PACKAGE_NAME, FC_NAME))
+    verify(mockCallback).onFailedToConnect()
+  }
+
+  @Test
+  fun onDisconnected_invokedOnFeatureCoordinatorDeadBinding() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    connector.featureCoordinatorConnection.onBindingDied(ComponentName(PACKAGE_NAME, FC_NAME))
+    verify(mockCallback).onDisconnected()
+  }
+
+  @Test
+  fun onConnected_invokedAfterSuccessfulBind() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    assertThat(connector.coordinatorProxy is SafeApiProxy).isTrue()
+    verify(mockCallback).onConnected()
+  }
+
+  @Test
+  fun onConnected_invokedAfterSuccessfulLegacyBind() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 0
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    assertThat(connector.coordinatorProxy is LegacyApiProxy).isTrue()
+    verify(mockCallback).onConnected()
+  }
+
+  @Test
+  fun sendMessage_sendsMessage() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    connector.coordinatorProxy = testCoordinatorProxy
+    connector.sendMessage(testDeviceId, testMessage)
+
+    verify(testCoordinatorProxy).sendMessage(testDeviceId, testMessage)
+  }
+
+  @Test
+  fun onMessageFailedToSend_invokedOnNullProxy() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    connector.coordinatorProxy = null
+    connector.sendMessage(testDeviceId, testMessage)
+
+    verify(mockCallback).onMessageFailedToSend(testDeviceId, testMessage, isTransient = true)
+  }
+
+  @Test
+  fun onMessageFailedToSend_invokedWhenDeviceNotFound() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    connector.coordinatorProxy = testCoordinatorProxy
+    val unexpectedDevice = UUID.randomUUID().toString()
+    connector.sendMessage(unexpectedDevice, testMessage)
+
+    assertThat(connector.connectedDevices.contains(unexpectedDevice)).isFalse()
+    verify(mockCallback).onMessageFailedToSend(unexpectedDevice, testMessage, isTransient = false)
+    verify(testCoordinatorProxy, never()).sendMessage(unexpectedDevice, testMessage)
+  }
+
+  @Test
+  fun onMessageFailedToSend_invokedWhenSendMessageFails() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val coordinatorProxy = TestCompanionApiProxy(false, listOf(testDeviceId))
+    connector.coordinatorProxy = coordinatorProxy
+    connector.sendMessage(testDeviceId, testMessage)
+
+    verify(mockCallback).onMessageFailedToSend(testDeviceId, testMessage, isTransient = false)
+  }
+
+  @Test
+  fun sendMessage_onMessageFailedToSend_invokedOnVersionMismatch() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 0
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    connector.sendMessage(testDeviceId, testMessage)
+
+    verify(mockCallback).onMessageFailedToSend(testDeviceId, testMessage, isTransient = false)
+  }
+
+  @Test
+  fun sendQuery_sendsQueryToOwnFeatureId() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    connector.coordinatorProxy = testCoordinatorProxy
+    val request = ByteUtils.randomBytes(10)
+    val parameters = ByteUtils.randomBytes(10)
+    val queryCallback = mock<SafeConnector.QueryCallback>()
+    connector.sendQuery(testDeviceId, request, parameters, queryCallback)
+
+    argumentCaptor<ByteArray> {
+      verify(testCoordinatorProxy).sendMessage(eq(testDeviceId), capture())
+      val query = Query.parseFrom(firstValue, ExtensionRegistryLite.getEmptyRegistry())
+      assertThat(query.request.toByteArray()).isEqualTo(request)
+      assertThat(query.parameters.toByteArray()).isEqualTo(parameters)
+    }
+  }
+
+  @Test
+  fun sendQuery_addsCallbackToCallbacksMap() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    connector.coordinatorProxy = testCoordinatorProxy
+    val request = ByteUtils.randomBytes(10)
+    val parameters = ByteUtils.randomBytes(10)
+    val queryCallback = mock<SafeConnector.QueryCallback>()
+    connector.sendQuery(testDeviceId, request, parameters, queryCallback)
+
+    assertThat(testCoordinatorProxy.queryCallbacks.containsValue(queryCallback)).isTrue()
+  }
+
+  @Test
+  fun sendQuery_onQueryFailedToSend_invokedOnNullProxy() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    connector.coordinatorProxy = null
+    val request = ByteUtils.randomBytes(10)
+    val parameters = ByteUtils.randomBytes(10)
+    val queryCallback = mock<SafeConnector.QueryCallback>()
+    connector.sendQuery(testDeviceId, request, parameters, queryCallback)
+
+    verify(queryCallback).onQueryFailedToSend(isTransient = false)
+  }
+
+  @Test
+  fun sendQuery_onQueryFailedToSend_invokedOnDeviceIdNotFound() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    connector.coordinatorProxy = testCoordinatorProxy
+    val request = ByteUtils.randomBytes(10)
+    val parameters = ByteUtils.randomBytes(10)
+    val queryCallback = mock<SafeConnector.QueryCallback>()
+    connector.sendQuery(UUID.randomUUID().toString(), request, parameters, queryCallback)
+
+    verify(queryCallback).onQueryFailedToSend(isTransient = false)
+  }
+
+  @Test
+  fun respondToQuery_doesNotSendResponseWithNullProxy() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    connector.coordinatorProxy = null
+    val response = ByteUtils.randomBytes(10)
+    connector.respondToQuery(testDeviceId, 1, success = true, response)
+
+    verify(testCoordinatorProxy, never()).sendMessage(any(), any())
+  }
+
+  @Test
+  fun respondToQuery_doesNotSendResponseWithUnrecognizedQueryId() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    connector.coordinatorProxy = testCoordinatorProxy
+    val nonExistentQueryId = 0
+    val response = ByteUtils.randomBytes(10)
+    connector.respondToQuery(testDeviceId, nonExistentQueryId, success = true, response)
+
+    verify(testCoordinatorProxy, never()).sendMessage(any(), any())
+  }
+
+  @Test
+  fun respondToQuery_sendsMessageWithNonNullResponse() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val queryId = 1
+    val response = ByteUtils.randomBytes(10)
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    connector.coordinatorProxy = testCoordinatorProxy
+    testCoordinatorProxy.queryResponseRecipients.put(queryId, testFeatureId)
+    connector.respondToQuery(testDeviceId, queryId, success = true, response)
+
+    argumentCaptor<ByteArray> {
+      verify(testCoordinatorProxy).sendMessage(any(), capture())
+      val queryResponse =
+        QueryResponse.parseFrom(firstValue, ExtensionRegistryLite.getEmptyRegistry())
+      assertThat(queryResponse.response.toByteArray()).isEqualTo(response)
+    }
+  }
+
+  @Test
+  fun respondToQuery_sendsMessageWithNullResponse() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val queryId = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    connector.coordinatorProxy = testCoordinatorProxy
+    testCoordinatorProxy.queryResponseRecipients.put(queryId, testFeatureId)
+    connector.respondToQuery(testDeviceId, queryId, success = true, null)
+
+    argumentCaptor<ByteArray> {
+      verify(testCoordinatorProxy).sendMessage(any(), capture())
+      val queryResponse =
+        QueryResponse.parseFrom(firstValue, ExtensionRegistryLite.getEmptyRegistry())
+      assertThat(queryResponse.response).isEmpty()
+    }
+  }
+
+  @Test
+  fun respondToQuery_onMessageFailedToSend_invokedWhenSendMessageFailed() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val proxy = spy(TestCompanionApiProxy(false, listOf(testDeviceId)))
+    connector.coordinatorProxy = proxy
+    val queryId = 1
+    proxy.queryResponseRecipients.put(queryId, testFeatureId)
+    val response = ByteUtils.randomBytes(10)
+    connector.respondToQuery(testDeviceId, queryId, success = true, response)
+
+    verify(mockCallback).onMessageFailedToSend(eq(testDeviceId), any(), eq(false))
+  }
+
+  @Test
+  fun retrieveCompanionApplicationName_sendsAppNameQueryToSystemFeature() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    connector.coordinatorProxy = testCoordinatorProxy
+    val callback = mock<SafeConnector.AppNameCallback>()
+
+    connector.retrieveCompanionApplicationName(testDeviceId, callback)
+    argumentCaptor<ByteArray> {
+      verify(testCoordinatorProxy).sendMessage(eq(testDeviceId), capture())
+      val query = Query.parseFrom(firstValue, ExtensionRegistryLite.getEmptyRegistry())
+      assertThat(query.sender.toByteArray())
+        .isEqualTo(ByteUtils.uuidToBytes(connector.featureId.uuid))
+      val systemQuery =
+        SystemQuery.parseFrom(query.request, ExtensionRegistryLite.getEmptyRegistry())
+      assertThat(systemQuery.type).isEqualTo(SystemQueryType.APP_NAME)
+    }
+  }
+
+  @Test
+  fun retrieveCompanionApplicationName_onMessageFailedToSend_invokedWhenSendMessageFailed() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val proxy = spy(TestCompanionApiProxy(false, listOf(testDeviceId)))
+    connector.coordinatorProxy = proxy
+    val appNameCallback = mock<SafeConnector.AppNameCallback>()
+    connector.retrieveCompanionApplicationName(testDeviceId, appNameCallback)
+
+    verify(appNameCallback).onError()
+  }
+
+  @Test
+  fun retrieveAssociatedDevices_worksWithSafeListener() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val mockListener = mock<ISafeOnAssociatedDevicesRetrievedListener>()
+    connector.coordinatorProxy = testCoordinatorProxy
+
+    connector.retrieveAssociatedDevices(mockListener)
+
+    verify(testCoordinatorProxy).retrieveAssociatedDevices(mockListener)
+  }
+
+  @Test
+  fun retrieveAssociatedDevices_worksWithLegacyListener() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 0
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val mockListener = mock<IOnAssociatedDevicesRetrievedListener>()
+    connector.coordinatorProxy = testCoordinatorProxy
+
+    connector.retrieveAssociatedDevices(mockListener)
+
+    verify(testCoordinatorProxy).retrieveAssociatedDevices(mockListener)
+  }
+
+  @Test
+  fun retrieveAssociatedDevices_returnsSilentlyOnNullCoordinator() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+    val mockListener = mock<ISafeOnAssociatedDevicesRetrievedListener>()
+    connector.coordinatorProxy = null
+
+    connector.retrieveAssociatedDevices(mockListener)
+
+    verify(testCoordinatorProxy, never()).retrieveAssociatedDevices(mockListener)
+  }
+
+  @Test
+  fun onCleanUp_cleansUpFeatureCoordinator() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    assertThat(connector.coordinatorProxy).isNotNull()
+    connector.cleanUp()
+    assertThat(connector.coordinatorProxy).isNull()
+  }
+
+  @Test
+  fun onCleanUp_cleansUpFeatureCoordinator_legacyPlatform() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 0
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    assertThat(connector.coordinatorProxy).isNotNull()
+    connector.cleanUp()
+    assertThat(connector.coordinatorProxy).isNull()
+  }
+
+  @Test
+  fun onCleanUp_callsCallbackDisconnected() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    connector.cleanUp()
+    verify(mockCallback).onDisconnected()
+  }
+
+  @Test
+  fun onCleanUp_callsCallbackDisconnected_legacyPlatform() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 0
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    connector.cleanUp()
+    verify(mockCallback).onDisconnected()
+  }
+
+  @Test
+  fun onCleanUp_unbindsFromContext() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 1
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    connector.cleanUp()
+    assertThat(context.unbindServiceConnection)
+      .containsExactly(connector.featureCoordinatorConnection)
+  }
+
+  @Test
+  fun onCleanUp_unbindsFromContext_legacyPlatform() {
+    setQueryIntentServicesAnswer(defaultServiceAnswer)
+    mockPlatformVersion = 0
+    val connector = FeatureConnector(context, testFeatureId, mockCallback, 1)
+
+    connector.cleanUp()
+    assertThat(context.unbindServiceConnection)
+      .containsExactly(connector.featureCoordinatorConnection)
+  }
+
+  private fun setQueryIntentServicesAnswer(answer: Answer<List<ResolveInfo>>) {
+    whenever(mockPackageManager.queryIntentServices(any(), any<Int>())).thenAnswer(answer)
+  }
+
+  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_QUERY_API_VERSION -> {
+        resolveInfo.serviceInfo.name = VERSION_NAME
+        listOf(resolveInfo)
+      }
+      ACTION_BIND_FEATURE_COORDINATOR -> {
+        resolveInfo.serviceInfo.name = FC_NAME
+        listOf(resolveInfo)
+      }
+      else -> listOf()
+    }
+  }
+
+  private open inner class FakeContext(val mockPackageManager: PackageManager) :
+    ContextWrapper(ApplicationProvider.getApplicationContext()) {
+
+    val bindingActions = mutableListOf<String>()
+
+    var serviceConnection = mutableListOf<ServiceConnection>()
+
+    var unbindServiceConnection = mutableListOf<ServiceConnection>()
+
+    override fun getPackageManager(): PackageManager = mockPackageManager
+
+    override fun bindService(service: Intent, conn: ServiceConnection, flags: Int): Boolean {
+      service.action?.let { bindingActions.add(it) }
+      serviceConnection.add(conn)
+
+      if (service.action == ACTION_QUERY_API_VERSION) {
+        bindVersionService(conn)
+      } else if (service.action == ACTION_BIND_FEATURE_COORDINATOR) {
+        bindFeatureCoordinatorService(conn)
+      }
+
+      return super.bindService(service, conn, flags)
+    }
+
+    protected fun bindVersionService(conn: ServiceConnection) {
+      val mockPlatformVersion = this@FeatureConnectorTest.mockPlatformVersion
+      val binder =
+        object : ISafeBinderVersion.Stub() {
+          override fun getVersion(): Int {
+            return mockPlatformVersion
+          }
+        }
+
+      val componentName = ComponentName(PACKAGE_NAME, VERSION_NAME)
+      if (mockPlatformVersion == 0) {
+        conn.onNullBinding(componentName)
+      } else {
+        conn.onServiceConnected(componentName, binder.asBinder())
+      }
+    }
+
+    private fun bindFeatureCoordinatorService(conn: ServiceConnection) {
+      val mockPlatformVersion = this@FeatureConnectorTest.mockPlatformVersion
+      val binder =
+        if (mockPlatformVersion == 0) {
+          mockFeatureCoordinator
+        } else {
+          mockSafeFeatureCoordinator
+        }
+
+      val componentName = ComponentName(PACKAGE_NAME, VERSION_NAME)
+      conn.onServiceConnected(componentName, binder.asBinder())
+    }
+
+    override fun unbindService(conn: ServiceConnection) {
+      unbindServiceConnection.add(conn)
+      super.unbindService(conn)
+    }
+  }
+
+  private inner class FailingContext(mockPackageManager: PackageManager) :
+    FakeContext(mockPackageManager) {
+
+    override fun bindService(service: Intent, conn: ServiceConnection, flags: Int): Boolean {
+      serviceConnection.add(conn)
+      return false
+    }
+
+    override fun unbindService(conn: ServiceConnection) {
+      throw IllegalArgumentException()
+    }
+  }
+
+  /** For testing a successful connection that returns an incorrect service */
+  private inner class IncorrectServiceContext(mockPackageManager: PackageManager) :
+    FakeContext(mockPackageManager) {
+
+    override fun bindService(service: Intent, conn: ServiceConnection, flags: Int): Boolean {
+      service.action?.let { bindingActions.add(it) }
+      serviceConnection.add(conn)
+      val binder = mockSafeFeatureCoordinator
+      val componentName = ComponentName(PACKAGE_NAME, VERSION_NAME)
+
+      conn.onServiceConnected(componentName, binder.asBinder())
+      return true
+    }
+  }
+
+  /**
+   * For testing successful connections during binds with ACTION_QUERY_API_VERSION but failures
+   * during binds using ACTION_BIND_FEATURE_COORDINATOR
+   */
+  private inner class FailingLegacyContext(mockPackageManager: PackageManager) :
+    FakeContext(mockPackageManager) {
+
+    override fun bindService(service: Intent, conn: ServiceConnection, flags: Int): Boolean {
+      service.action?.let { bindingActions.add(it) }
+      serviceConnection.add(conn)
+      if (service.action == ACTION_QUERY_API_VERSION) {
+        bindVersionService(conn)
+        return true
+      } else if (service.action == ACTION_BIND_FEATURE_COORDINATOR) {
+        return false
+      }
+      return super.bindService(service, conn, flags)
+    }
+
+    override fun unbindService(conn: ServiceConnection) {
+      throw IllegalArgumentException()
+    }
+  }
+
+  /**
+   * For testing binding with ACTION_QUERY_API_VERSION failing on the first attempt but succeeeding
+   * thereafter, as well as failing binds using ACTION_BIND_FEATURE_COORDINATOR
+   */
+  private inner class FlakyLegacyContext(mockPackageManager: PackageManager) :
+    FakeContext(mockPackageManager) {
+
+    private var bindAttempts = 0
+
+    override fun bindService(service: Intent, conn: ServiceConnection, flags: Int): Boolean {
+      service.action?.let { bindingActions.add(it) }
+      serviceConnection.add(conn)
+      if (service.action == ACTION_QUERY_API_VERSION) {
+        if (bindAttempts++ == 0) {
+          return false
+        } else {
+          bindVersionService(conn)
+          return true
+        }
+      } else if (service.action == ACTION_BIND_FEATURE_COORDINATOR) {
+        return false
+      }
+      return super.bindService(service, conn, flags)
+    }
+
+    override fun unbindService(conn: ServiceConnection) {
+      throw IllegalArgumentException()
+    }
+  }
+
+  private open class TestCompanionApiProxy(
+    val defaultReturnValue: Boolean = true,
+    val deviceIdList: List<String> = emptyList()
+  ) : CompanionApiProxy {
+
+    override val queryCallbacks = ConcurrentHashMap<Int, QueryCallback>()
+
+    override val queryResponseRecipients = ConcurrentHashMap<Int, ParcelUuid>()
+
+    override val listener: ISafeOnLogRequestedListener = mock()
+
+    override fun getConnectedDevices() = deviceIdList
+
+    override fun sendMessage(deviceId: String, message: ByteArray) = defaultReturnValue
+
+    override fun processLogRecords(loggerId: Int, logRecords: ByteArray) = defaultReturnValue
+
+    override fun retrieveAssociatedDevices(listener: IInterface) = defaultReturnValue
+
+    override fun cleanUp() {}
+  }
+
+  companion object {
+    private const val PACKAGE_NAME = "com.test.package"
+    private const val FC_NAME = "feature_coordinator"
+    private const val VERSION_NAME = "version"
+  }
+}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/LegacyApiProxyTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/LegacyApiProxyTest.kt
new file mode 100644
index 0000000..c8d8bb9
--- /dev/null
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/LegacyApiProxyTest.kt
@@ -0,0 +1,450 @@
+package com.google.android.connecteddevice.api
+
+import android.os.ParcelUuid
+import android.os.RemoteException
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.companionprotos.OperationProto.OperationType
+import com.google.android.companionprotos.Query
+import com.google.android.companionprotos.QueryResponse
+import com.google.android.connecteddevice.api.SafeConnector.QueryCallback
+import com.google.android.connecteddevice.api.external.ISafeOnAssociatedDevicesRetrievedListener
+import com.google.android.connecteddevice.api.external.ISafeOnLogRequestedListener
+import com.google.android.connecteddevice.core.util.mockToBeAlive
+import com.google.android.connecteddevice.model.ConnectedDevice
+import com.google.android.connecteddevice.model.DeviceMessage
+import com.google.android.connecteddevice.model.DeviceMessage.OperationType.CLIENT_MESSAGE
+import com.google.android.connecteddevice.util.ByteUtils
+import com.google.common.truth.Truth.assertThat
+import com.google.protobuf.ByteString
+import com.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.eq
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.never
+import com.nhaarman.mockitokotlin2.verify
+import com.nhaarman.mockitokotlin2.whenever
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class LegacyApiProxyTest {
+
+  private val negativeMockCoordinator: IFeatureCoordinator = mock()
+
+  private val versionZeroMockCoordinator: IFeatureCoordinator = mock()
+
+  private val versionOneMockCoordinator: IFeatureCoordinator = mock()
+
+  private val recipientId = ParcelUuid(UUID.randomUUID())
+
+  private val mockConnectorCallback: SafeConnector.Callback = mock()
+
+  private val testLoggerId = 0
+
+  private val mockListener: ISafeOnLogRequestedListener = mock()
+
+  private lateinit var defaultProxyVersionNegative: LegacyApiProxy
+
+  private lateinit var defaultProxyVersion0: LegacyApiProxy
+
+  private lateinit var defaultProxyVersion1: LegacyApiProxy
+
+  @Before
+  fun setUp() {
+    val connectedDevice =
+      ConnectedDevice(
+        UUID.randomUUID().toString(),
+        "driverDeviceName1",
+        /* belongsToDriver= */ true,
+        /* hasSecureChannel= */ true
+      )
+    whenever(versionZeroMockCoordinator.getConnectedDevicesForDriver())
+      .thenReturn(listOf(connectedDevice))
+    whenever(versionOneMockCoordinator.getConnectedDevicesForDriver())
+      .thenReturn(listOf(connectedDevice))
+    defaultProxyVersionNegative =
+      LegacyApiProxy(
+        negativeMockCoordinator,
+        recipientId,
+        mockConnectorCallback,
+        testLoggerId,
+        -1
+      )
+    defaultProxyVersion0 =
+      LegacyApiProxy(
+        versionZeroMockCoordinator,
+        recipientId,
+        mockConnectorCallback,
+        testLoggerId,
+        0
+      )
+    defaultProxyVersion1 =
+      LegacyApiProxy(
+        versionOneMockCoordinator,
+        recipientId,
+        mockConnectorCallback,
+        testLoggerId,
+        1
+      )
+  }
+
+  @Test
+  fun onInit_doesNotInvokeCoordinator_onPlatformVersionLessThanZero() {
+    verify(negativeMockCoordinator, never()).registerAllConnectionCallback(any())
+    verify(negativeMockCoordinator, never()).registerDeviceCallback(any(), any(), any())
+    verify(negativeMockCoordinator, never()).registerOnLogRequestedListener(any(), any())
+  }
+
+  @Test
+  fun onInit_correctlyInvokesCoordinator_onPlatformVersionZero() {
+    verify(versionZeroMockCoordinator).registerAllConnectionCallback(any())
+    verify(versionZeroMockCoordinator).registerDeviceCallback(any(), any(), any())
+    verify(versionZeroMockCoordinator).registerOnLogRequestedListener(any(), any())
+  }
+
+  @Test
+  fun onInit_correctlyInvokesCoordinator_onPlatformVersionOne() {
+    verify(versionOneMockCoordinator).registerAllConnectionCallback(any())
+    verify(versionOneMockCoordinator).registerDeviceCallback(any(), any(), any())
+    verify(versionOneMockCoordinator).registerOnLogRequestedListener(any(), any())
+  }
+
+  @Test
+  fun getConnectedDevices_returnsNullOnVersionLessThanZero() {
+    val device1 =
+      ConnectedDevice(
+        UUID.randomUUID().toString(),
+        "driverDeviceName1",
+        /* belongsToDriver= */ true,
+        /* hasSecureChannel= */ true
+      )
+    val device2 =
+      ConnectedDevice(
+        UUID.randomUUID().toString(),
+        "driverDeviceName2",
+        /* belongsToDriver= */ true,
+        /* hasSecureChannel= */ true
+      )
+    whenever(negativeMockCoordinator.getConnectedDevicesForDriver())
+      .thenReturn(listOf(device1, device2))
+    val devices = defaultProxyVersionNegative.getConnectedDevices()
+
+    assertThat(devices).isNull()
+  }
+
+  @Test
+  fun getConnectedDevices_returnsListOnVersionZero() {
+    val device1 =
+      ConnectedDevice(
+        UUID.randomUUID().toString(),
+        "driverDeviceName1",
+        /* belongsToDriver= */ true,
+        /* hasSecureChannel= */ true
+      )
+    val device2 =
+      ConnectedDevice(
+        UUID.randomUUID().toString(),
+        "driverDeviceName2",
+        /* belongsToDriver= */ true,
+        /* hasSecureChannel= */ true
+      )
+    whenever(versionZeroMockCoordinator.getConnectedDevicesForDriver())
+      .thenReturn(listOf(device1, device2))
+    val devices = defaultProxyVersion0.getConnectedDevices()
+
+    assertThat(devices).containsExactly(device1.deviceId, device2.deviceId)
+  }
+
+  @Test
+  fun getConnectedDevices_returnsListOnVersionGreaterThanZero() {
+    val device1 =
+      ConnectedDevice(
+        UUID.randomUUID().toString(),
+        "driverDeviceName1",
+        /* belongsToDriver= */ true,
+        /* hasSecureChannel= */ true
+      )
+    val device2 =
+      ConnectedDevice(
+        UUID.randomUUID().toString(),
+        "driverDeviceName2",
+        /* belongsToDriver= */ true,
+        /* hasSecureChannel= */ true
+      )
+    whenever(versionOneMockCoordinator.getConnectedDevicesForDriver())
+      .thenReturn(listOf(device1, device2))
+    val devices = defaultProxyVersion1.getConnectedDevices()
+
+    assertThat(devices).containsExactly(device1.deviceId, device2.deviceId)
+  }
+
+  @Test
+  fun sendMessage_returnsFalseOnVersionLessThanZero() {
+    val connectedDevice: ConnectedDevice = mock()
+    val deviceId = UUID.randomUUID().toString()
+    val message = ByteUtils.randomBytes(10)
+    val deviceMessage =
+      DeviceMessage.createOutgoingMessage(
+        UUID.fromString(deviceId),
+        /* isMessageEncrypted= */ true,
+        DeviceMessage.OperationType.CLIENT_MESSAGE,
+        message
+      )
+
+    whenever(connectedDevice.deviceId).thenReturn(deviceId)
+    whenever(negativeMockCoordinator.getConnectedDevicesForDriver())
+      .thenReturn(listOf(connectedDevice))
+    whenever(negativeMockCoordinator.sendMessage(connectedDevice, deviceMessage)).thenReturn(true)
+    val messageSent = defaultProxyVersionNegative.sendMessage(deviceId, message)
+
+    assertThat(messageSent).isFalse()
+  }
+
+  @Test
+  fun sendMessage_sendsMessageToControllerOnVersionZero() {
+    val connectedDevice: ConnectedDevice = mock()
+    val deviceId = UUID.randomUUID().toString()
+    val message = ByteUtils.randomBytes(10)
+    val deviceMessage =
+      DeviceMessage.createOutgoingMessage(
+        UUID.fromString(deviceId),
+        /* isMessageEncrypted= */ true,
+        DeviceMessage.OperationType.CLIENT_MESSAGE,
+        message
+      )
+
+    whenever(connectedDevice.deviceId).thenReturn(deviceId)
+    whenever(versionZeroMockCoordinator.getConnectedDevicesForDriver())
+      .thenReturn(listOf(connectedDevice))
+    whenever(versionZeroMockCoordinator.sendMessage(connectedDevice, deviceMessage))
+      .thenReturn(true)
+    val messageSent = defaultProxyVersion0.sendMessage(deviceId, message)
+
+    assertThat(messageSent).isTrue()
+  }
+
+  @Test
+  fun sendMessage_sendsMessageToControllerOnVersionGreaterThanZero() {
+    val connectedDevice: ConnectedDevice = mock()
+    val deviceId = UUID.randomUUID().toString()
+    val message = ByteUtils.randomBytes(10)
+    val deviceMessage =
+      DeviceMessage.createOutgoingMessage(
+        UUID.fromString(deviceId),
+        /* isMessageEncrypted= */ true,
+        DeviceMessage.OperationType.CLIENT_MESSAGE,
+        message
+      )
+
+    whenever(connectedDevice.deviceId).thenReturn(deviceId)
+    whenever(versionOneMockCoordinator.getConnectedDevicesForDriver())
+      .thenReturn(listOf(connectedDevice))
+    whenever(versionOneMockCoordinator.sendMessage(connectedDevice, deviceMessage)).thenReturn(true)
+    val messageSent = defaultProxyVersion1.sendMessage(deviceId, message)
+
+    assertThat(messageSent).isTrue()
+  }
+
+  @Test
+  fun sendMessage_failsToSendMessageUsingInvalidDeviceIdOnVersionZero() {
+    val connectedDevice: ConnectedDevice = mock()
+    val deviceId1 = "connectedDevice"
+    val deviceId2 = "disconnectedDevice"
+    val message = ByteUtils.randomBytes(10)
+    whenever(connectedDevice.deviceId).thenReturn(deviceId1)
+    whenever(versionZeroMockCoordinator.getConnectedDevicesForDriver())
+      .thenReturn(listOf(connectedDevice))
+    whenever(versionZeroMockCoordinator.sendMessage(any(), any())).thenReturn(true)
+    val messageSent = defaultProxyVersion0.sendMessage(deviceId2, message)
+
+    assertThat(messageSent).isFalse()
+  }
+
+  @Test
+  fun sendMessage_failsToSendMessageUsingInvalidDeviceIdOnVersionGreaterThanZero() {
+    val connectedDevice: ConnectedDevice = mock()
+    val deviceId1 = "connectedDevice"
+    val deviceId2 = "disconnectedDevice"
+    val message = ByteUtils.randomBytes(10)
+    whenever(connectedDevice.deviceId).thenReturn(deviceId1)
+    whenever(versionOneMockCoordinator.getConnectedDevicesForDriver())
+      .thenReturn(listOf(connectedDevice))
+    whenever(versionOneMockCoordinator.sendMessage(any(), any())).thenReturn(true)
+    val messageSent = defaultProxyVersion1.sendMessage(deviceId2, message)
+
+    assertThat(messageSent).isFalse()
+  }
+
+  @Test
+  fun sendMessage_failsIfCoordinatorThrowsException() {
+    val connectedDevice: ConnectedDevice = mock()
+    val deviceId = UUID.randomUUID().toString()
+    val message = ByteUtils.randomBytes(10)
+    val deviceMessage =
+      DeviceMessage.createOutgoingMessage(
+        UUID.fromString(deviceId),
+        /* isMessageEncrypted= */ true,
+        DeviceMessage.OperationType.CLIENT_MESSAGE,
+        message
+      )
+    whenever(connectedDevice.deviceId).thenReturn(deviceId)
+    whenever(versionOneMockCoordinator.getConnectedDevicesForDriver())
+      .thenReturn(listOf(connectedDevice))
+    whenever(versionZeroMockCoordinator.sendMessage(connectedDevice, deviceMessage))
+      .thenThrow(RemoteException())
+    val messageSent = defaultProxyVersion0.sendMessage(UUID.randomUUID().toString(), message)
+
+    assertThat(messageSent).isFalse()
+  }
+
+  @Test
+  fun processLogRecords_returnsFalseOnVersionLessThanZero() {
+    val testLogs = "test logs".toByteArray()
+    val success = defaultProxyVersionNegative.processLogRecords(testLoggerId, testLogs)
+
+    assertThat(success).isFalse()
+  }
+
+  @Test
+  fun processLogRecords_returnsTrueOnVersionZero() {
+    val testLogs = "test logs".toByteArray()
+    val success = defaultProxyVersion0.processLogRecords(testLoggerId, testLogs)
+
+    assertThat(success).isTrue()
+  }
+
+  @Test
+  fun processLogRecords_returnsTrueOnVersionGreaterThanZero() {
+    val testLogs = "test logs".toByteArray()
+    val success = defaultProxyVersion1.processLogRecords(testLoggerId, testLogs)
+
+    assertThat(success).isTrue()
+  }
+
+  @Test
+  fun retrieveAssociatedDevices_returnsFalseOnVersionLessThanZero() {
+    val mockListener: IOnAssociatedDevicesRetrievedListener = mockToBeAlive()
+    val success = defaultProxyVersionNegative.retrieveAssociatedDevices(mockListener)
+
+    assertThat(success).isFalse()
+  }
+
+  @Test
+  fun retrieveAssociatedDevices_returnsTrueOnVersionZero() {
+    val mockListener: IOnAssociatedDevicesRetrievedListener = mockToBeAlive()
+    val success = defaultProxyVersion0.retrieveAssociatedDevices(mockListener)
+
+    assertThat(success).isTrue()
+  }
+
+  @Test
+  fun retrieveAssociatedDevices_returnsTrueOnVersionGreaterThanZero() {
+    val mockListener: IOnAssociatedDevicesRetrievedListener = mockToBeAlive()
+    val success = defaultProxyVersion1.retrieveAssociatedDevices(mockListener)
+
+    assertThat(success).isTrue()
+  }
+
+  @Test
+  fun retrieveAssociatedDevices_returnsFalseIfIncorrectParam() {
+    val mockListener: ISafeOnAssociatedDevicesRetrievedListener = mockToBeAlive()
+    val success = defaultProxyVersion0.retrieveAssociatedDevices(mockListener)
+
+    assertThat(success).isFalse()
+  }
+
+  @Test
+  fun processIncomingMessage_worksWithClientMessage() {
+    val deviceId = UUID.randomUUID().toString()
+    val mockConnectedDevice: ConnectedDevice = mock()
+    whenever(mockConnectedDevice.deviceId).thenReturn(deviceId)
+    val payload = ByteString.copyFrom(ByteUtils.randomBytes(10)).toByteArray()
+    val message =
+      DeviceMessage.createOutgoingMessage(
+        UUID.randomUUID(),
+        /* isMessageEncrypted= */ true,
+        DeviceMessage.OperationType.CLIENT_MESSAGE,
+        payload
+      )
+    defaultProxyVersion0.deviceCallback.onMessageReceived(mockConnectedDevice, message)
+
+    verify(mockConnectorCallback).onMessageReceived(deviceId, payload)
+  }
+
+  @Test
+  fun processIncomingMessage_worksWithQuery() {
+    val deviceId = UUID.randomUUID().toString()
+    val mockConnectedDevice: ConnectedDevice = mock()
+    whenever(mockConnectedDevice.deviceId).thenReturn(deviceId)
+    val payload =
+      Query.newBuilder()
+        .setId(1)
+        .setSender(ByteString.copyFrom(ByteUtils.uuidToBytes(recipientId.uuid)))
+        .setRequest(ByteString.copyFrom(ByteUtils.randomBytes(10)))
+        .setParameters(ByteString.copyFrom(ByteUtils.randomBytes(10)))
+        .build()
+        .toByteArray()
+    val message =
+      DeviceMessage.createOutgoingMessage(
+        UUID.randomUUID(),
+        /* isMessageEncrypted= */ true,
+        DeviceMessage.OperationType.QUERY,
+        payload
+      )
+    defaultProxyVersion0.deviceCallback.onMessageReceived(mockConnectedDevice, message)
+
+    verify(mockConnectorCallback).onQueryReceived(eq(deviceId), any(), any(), any())
+  }
+
+  @Test
+  fun processIncomingMessage_worksWithQueryResponse() {
+    val deviceId = UUID.randomUUID().toString()
+    val mockConnectedDevice: ConnectedDevice = mock()
+    whenever(mockConnectedDevice.deviceId).thenReturn(deviceId)
+    val queryId = 1
+    val mockQueryCallback: QueryCallback = mock()
+    defaultProxyVersion0.queryCallbacks[queryId] = mockQueryCallback
+    val payload =
+      QueryResponse.newBuilder().setQueryId(queryId).setSuccess(true).build().toByteArray()
+    val message =
+      DeviceMessage.createOutgoingMessage(
+        UUID.randomUUID(),
+        /* isMessageEncrypted= */ true,
+        DeviceMessage.OperationType.QUERY_RESPONSE,
+        payload
+      )
+    defaultProxyVersion0.deviceCallback.onMessageReceived(mockConnectedDevice, message)
+
+    assertThat(defaultProxyVersion0.queryCallbacks.contains(queryId)).isFalse()
+    verify(mockQueryCallback).onSuccess(any())
+  }
+
+  @Test
+  fun onCleanUp_doesNotInvokeCoordinator_onPlatformVersionLessThanZero() {
+    defaultProxyVersionNegative.cleanUp()
+
+    verify(negativeMockCoordinator, never()).unregisterConnectionCallback(any())
+    verify(negativeMockCoordinator, never()).unregisterDeviceCallback(any(), any(), any())
+    verify(negativeMockCoordinator, never()).unregisterOnLogRequestedListener(any(), any())
+  }
+
+  @Test
+  fun onCleanUp_correctlyInvokesCoordinator_onPlatformVersionZero() {
+    defaultProxyVersion0.cleanUp()
+
+    verify(versionZeroMockCoordinator).unregisterConnectionCallback(any())
+    verify(versionZeroMockCoordinator).unregisterDeviceCallback(any(), any(), any())
+    verify(versionZeroMockCoordinator).unregisterOnLogRequestedListener(any(), any())
+  }
+
+  @Test
+  fun onCleanUp_correctlyInvokesCoordinator_onPlatformVersionGreaterThanZero() {
+    defaultProxyVersion1.cleanUp()
+
+    verify(versionOneMockCoordinator).unregisterConnectionCallback(any())
+    verify(versionOneMockCoordinator).unregisterDeviceCallback(any(), any(), any())
+    verify(versionOneMockCoordinator).unregisterOnLogRequestedListener(any(), any())
+  }
+}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/SafeApiProxyTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/SafeApiProxyTest.kt
new file mode 100644
index 0000000..9a1d9a3
--- /dev/null
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/SafeApiProxyTest.kt
@@ -0,0 +1,355 @@
+package com.google.android.connecteddevice.api
+
+import android.os.ParcelUuid
+import android.os.RemoteException
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.companionprotos.DeviceMessageProto
+import com.google.android.companionprotos.OperationProto.OperationType
+import com.google.android.companionprotos.Query
+import com.google.android.companionprotos.QueryResponse
+import com.google.android.connecteddevice.api.SafeConnector.QueryCallback
+import com.google.android.connecteddevice.api.external.ISafeFeatureCoordinator
+import com.google.android.connecteddevice.api.external.ISafeOnAssociatedDevicesRetrievedListener
+import com.google.android.connecteddevice.api.external.ISafeOnLogRequestedListener
+import com.google.android.connecteddevice.core.util.mockToBeAlive
+import com.google.android.connecteddevice.util.ByteUtils
+import com.google.common.truth.Truth.assertThat
+import com.google.protobuf.ByteString
+import com.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.eq
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.never
+import com.nhaarman.mockitokotlin2.verify
+import com.nhaarman.mockitokotlin2.whenever
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SafeApiProxyTest {
+
+  private val versionZeroMockCoordinator: ISafeFeatureCoordinator = mock()
+
+  private val versionOneMockCoordinator: ISafeFeatureCoordinator = mock()
+
+  private val versionTwoMockCoordinator: ISafeFeatureCoordinator = mock()
+
+  private val recipientId = ParcelUuid(UUID.randomUUID())
+
+  private val mockConnectorCallback: SafeConnector.Callback = mock()
+
+  private val testLoggerId = 0
+
+  private val mockListener: ISafeOnLogRequestedListener = mock()
+
+  private lateinit var defaultProxyVersion0: SafeApiProxy
+
+  private lateinit var defaultProxyVersion1: SafeApiProxy
+
+  private lateinit var defaultProxyVersion2: SafeApiProxy
+
+  @Before
+  fun setUp() {
+    val device = "driverDeviceName1"
+    whenever(versionOneMockCoordinator.getConnectedDevices()).thenReturn(listOf(device))
+    whenever(versionTwoMockCoordinator.getConnectedDevices()).thenReturn(listOf(device))
+    defaultProxyVersion0 =
+      SafeApiProxy(
+        versionZeroMockCoordinator,
+        recipientId,
+        mockConnectorCallback,
+        testLoggerId,
+        0
+      )
+    defaultProxyVersion1 =
+      SafeApiProxy(
+        versionOneMockCoordinator,
+        recipientId,
+        mockConnectorCallback,
+        testLoggerId,
+        1
+      )
+    defaultProxyVersion2 =
+      SafeApiProxy(
+        versionTwoMockCoordinator,
+        recipientId,
+        mockConnectorCallback,
+        testLoggerId,
+        2
+      )
+  }
+
+  @Test
+  fun onInit_doesNotInvokeCoordinator_onPlatformVersionZero() {
+    verify(versionZeroMockCoordinator, never()).registerConnectionCallback(any())
+    verify(versionZeroMockCoordinator, never()).registerDeviceCallback(any(), any(), any())
+    verify(versionZeroMockCoordinator, never()).registerOnLogRequestedListener(any(), any())
+  }
+
+  @Test
+  fun onInit_correctlyInvokesCoordinator_onPlatformVersionOne() {
+    verify(versionOneMockCoordinator).registerConnectionCallback(any())
+    verify(versionOneMockCoordinator).registerDeviceCallback(any(), any(), any())
+    verify(versionOneMockCoordinator).registerOnLogRequestedListener(any(), any())
+  }
+
+  @Test
+  fun onInit_correctlyInvokesCoordinator_onPlatformVersionTwo() {
+    verify(versionTwoMockCoordinator).registerConnectionCallback(any())
+    verify(versionTwoMockCoordinator).registerDeviceCallback(any(), any(), any())
+    verify(versionTwoMockCoordinator).registerOnLogRequestedListener(any(), any())
+  }
+
+  @Test
+  fun getConnectedDevices_returnsNullOnVersionZero() {
+    whenever(versionZeroMockCoordinator.getConnectedDevices())
+      .thenReturn(listOf("driverDeviceName1", "driverDeviceName2"))
+    val devices = defaultProxyVersion0.getConnectedDevices()
+
+    assertThat(devices).isNull()
+  }
+
+  @Test
+  fun getConnectedDevices_returnsListOnVersionOne() {
+    whenever(versionOneMockCoordinator.getConnectedDevices())
+      .thenReturn(listOf("driverDeviceName1", "driverDeviceName2"))
+    val devices = defaultProxyVersion1.getConnectedDevices()
+
+    assertThat(devices).containsExactly("driverDeviceName1", "driverDeviceName2")
+  }
+
+  @Test
+  fun getConnectedDevices_returnsListOnVersionTwo() {
+    whenever(versionTwoMockCoordinator.getConnectedDevices())
+      .thenReturn(listOf("driverDeviceName1", "driverDeviceName2"))
+    val devices = defaultProxyVersion2.getConnectedDevices()
+
+    assertThat(devices).containsExactly("driverDeviceName1", "driverDeviceName2")
+  }
+
+  @Test
+  fun sendMessage_returnsFalseOnVersionZero() {
+    val message =
+      DeviceMessageProto.Message.newBuilder()
+        .setRecipient(ByteString.copyFrom(ByteUtils.uuidToBytes(UUID.randomUUID())))
+        .setIsPayloadEncrypted(true)
+        .setOperation(
+          OperationType.forNumber(/* CLIENT_MESSAGE */ 4) ?: OperationType.OPERATION_TYPE_UNKNOWN
+        )
+        .setPayload(ByteString.copyFrom(ByteUtils.randomBytes(10)))
+        .build()
+    val rawBytes = message.toByteArray()
+    whenever(versionZeroMockCoordinator.sendMessage(any(), any())).thenReturn(true)
+    val messageSent = defaultProxyVersion0.sendMessage(UUID.randomUUID().toString(), rawBytes)
+
+    assertThat(messageSent).isFalse()
+  }
+
+  @Test
+  fun sendMessage_sendsMessageToControllerOnVersionOne() {
+    val message =
+      DeviceMessageProto.Message.newBuilder()
+        .setRecipient(ByteString.copyFrom(ByteUtils.uuidToBytes(UUID.randomUUID())))
+        .setIsPayloadEncrypted(true)
+        .setOperation(
+          OperationType.forNumber(/* CLIENT_MESSAGE */ 4) ?: OperationType.OPERATION_TYPE_UNKNOWN
+        )
+        .setPayload(ByteString.copyFrom(ByteUtils.randomBytes(10)))
+        .build()
+    val rawBytes = message.toByteArray()
+    whenever(versionOneMockCoordinator.sendMessage(any(), any())).thenReturn(true)
+    val messageSent = defaultProxyVersion1.sendMessage(UUID.randomUUID().toString(), rawBytes)
+
+    assertThat(messageSent).isTrue()
+  }
+
+  @Test
+  fun sendMessage_sendsMessageToControllerOnVersionTwo() {
+    val message =
+      DeviceMessageProto.Message.newBuilder()
+        .setRecipient(ByteString.copyFrom(ByteUtils.uuidToBytes(UUID.randomUUID())))
+        .setIsPayloadEncrypted(true)
+        .setOperation(
+          OperationType.forNumber(/* CLIENT_MESSAGE */ 4) ?: OperationType.OPERATION_TYPE_UNKNOWN
+        )
+        .setPayload(ByteString.copyFrom(ByteUtils.randomBytes(10)))
+        .build()
+    val rawBytes = message.toByteArray()
+    whenever(versionTwoMockCoordinator.sendMessage(any(), any())).thenReturn(true)
+    val messageSent = defaultProxyVersion2.sendMessage(UUID.randomUUID().toString(), rawBytes)
+
+    assertThat(messageSent).isTrue()
+  }
+
+  @Test
+  fun sendMessage_failsIfCoordinatorThrowsException() {
+    val message =
+      DeviceMessageProto.Message.newBuilder()
+        .setRecipient(ByteString.copyFrom(ByteUtils.uuidToBytes(UUID.randomUUID())))
+        .setIsPayloadEncrypted(true)
+        .setOperation(
+          OperationType.forNumber(/* CLIENT_MESSAGE */ 4) ?: OperationType.OPERATION_TYPE_UNKNOWN
+        )
+        .setPayload(ByteString.copyFrom(ByteUtils.randomBytes(10)))
+        .build()
+    val rawBytes = message.toByteArray()
+    whenever(versionOneMockCoordinator.sendMessage(any(), any())).thenThrow(RemoteException())
+    val messageSent = defaultProxyVersion1.sendMessage(UUID.randomUUID().toString(), rawBytes)
+
+    assertThat(messageSent).isFalse()
+  }
+
+  @Test
+  fun processLogRecords_returnsFalseOnVersionZero() {
+    val testLogs = "test logs".toByteArray()
+    val success = defaultProxyVersion0.processLogRecords(testLoggerId, testLogs)
+
+    assertThat(success).isFalse()
+  }
+
+  @Test
+  fun processLogRecords_returnsTrueOnVersionOne() {
+    val testLogs = "test logs".toByteArray()
+    val success = defaultProxyVersion1.processLogRecords(testLoggerId, testLogs)
+
+    assertThat(success).isTrue()
+  }
+
+  @Test
+  fun processLogRecords_returnsTrueOnVersionTwo() {
+    val testLogs = "test logs".toByteArray()
+    val success = defaultProxyVersion2.processLogRecords(testLoggerId, testLogs)
+
+    assertThat(success).isTrue()
+  }
+
+  @Test
+  fun retrieveAssociatedDevices_returnsFalseOnVersionZero() {
+    val mockListener: ISafeOnAssociatedDevicesRetrievedListener = mockToBeAlive()
+    val success = defaultProxyVersion0.retrieveAssociatedDevices(mockListener)
+
+    assertThat(success).isFalse()
+  }
+
+  @Test
+  fun retrieveAssociatedDevices_returnsTrueOnVersionOne() {
+    val mockListener: ISafeOnAssociatedDevicesRetrievedListener = mockToBeAlive()
+    val success = defaultProxyVersion1.retrieveAssociatedDevices(mockListener)
+
+    assertThat(success).isTrue()
+  }
+
+  @Test
+  fun retrieveAssociatedDevices_returnsTrueOnVersionTwo() {
+    val mockListener: ISafeOnAssociatedDevicesRetrievedListener = mockToBeAlive()
+    val success = defaultProxyVersion2.retrieveAssociatedDevices(mockListener)
+
+    assertThat(success).isTrue()
+  }
+
+  @Test
+  fun retrieveAssociatedDevices_returnsFalseIfIncorrectParam() {
+    val mockListener: IOnAssociatedDevicesRetrievedListener = mockToBeAlive()
+    val success = defaultProxyVersion1.retrieveAssociatedDevices(mockListener)
+
+    assertThat(success).isFalse()
+  }
+
+  @Test
+  fun processIncomingMessage_worksWithClientMessage() {
+    val deviceId = UUID.randomUUID().toString()
+    val payload = ByteString.copyFrom(ByteUtils.randomBytes(10))
+    val message =
+      DeviceMessageProto.Message.newBuilder()
+        .setRecipient(ByteString.copyFrom(ByteUtils.uuidToBytes(UUID.randomUUID())))
+        .setIsPayloadEncrypted(true)
+        .setOperation(
+          OperationType.forNumber(/* CLIENT_MESSAGE */ 4) ?: OperationType.OPERATION_TYPE_UNKNOWN
+        )
+        .setPayload(payload)
+        .build()
+    val rawBytes = message.toByteArray()
+    defaultProxyVersion0.deviceCallback.onMessageReceived(deviceId, rawBytes)
+
+    verify(mockConnectorCallback).onMessageReceived(deviceId, payload.toByteArray())
+  }
+
+  @Test
+  fun processIncomingMessage_worksWithQuery() {
+    val deviceId = UUID.randomUUID().toString()
+    val payload =
+      Query.newBuilder()
+        .setId(1)
+        .setSender(ByteString.copyFrom(ByteUtils.uuidToBytes(recipientId.uuid)))
+        .setRequest(ByteString.copyFrom(ByteUtils.randomBytes(10)))
+        .setParameters(ByteString.copyFrom(ByteUtils.randomBytes(10)))
+        .build()
+        .toByteString()
+    val message =
+      DeviceMessageProto.Message.newBuilder()
+        .setRecipient(ByteString.copyFrom(ByteUtils.uuidToBytes(UUID.randomUUID())))
+        .setIsPayloadEncrypted(true)
+        .setOperation(
+          OperationType.forNumber(/* QUERY */ 5) ?: OperationType.OPERATION_TYPE_UNKNOWN
+        )
+        .setPayload(payload)
+        .build()
+    val rawBytes = message.toByteArray()
+    defaultProxyVersion0.deviceCallback.onMessageReceived(deviceId, rawBytes)
+
+    verify(mockConnectorCallback).onQueryReceived(eq(deviceId), any(), any(), any())
+  }
+
+  @Test
+  fun processIncomingMessage_worksWithQueryResponse() {
+    val deviceId = UUID.randomUUID().toString()
+    val queryId = 1
+    val mockQueryCallback: QueryCallback = mock()
+    defaultProxyVersion0.queryCallbacks[queryId] = mockQueryCallback
+    val payload =
+      QueryResponse.newBuilder().setQueryId(queryId).setSuccess(true).build().toByteString()
+    val message =
+      DeviceMessageProto.Message.newBuilder()
+        .setRecipient(ByteString.copyFrom(ByteUtils.uuidToBytes(UUID.randomUUID())))
+        .setIsPayloadEncrypted(true)
+        .setOperation(
+          OperationType.forNumber(/* QUERY_RESPONSE */ 6) ?: OperationType.OPERATION_TYPE_UNKNOWN
+        )
+        .setPayload(payload)
+        .build()
+    val rawBytes = message.toByteArray()
+    defaultProxyVersion0.deviceCallback.onMessageReceived(deviceId, rawBytes)
+
+    assertThat(defaultProxyVersion0.queryCallbacks.contains(queryId)).isFalse()
+    verify(mockQueryCallback).onSuccess(any())
+  }
+
+  @Test
+  fun onCleanUp_doesNotInvokeCoordinator_onPlatformVersionZero() {
+    defaultProxyVersion0.cleanUp()
+
+    verify(versionZeroMockCoordinator, never()).unregisterConnectionCallback(any())
+    verify(versionZeroMockCoordinator, never()).unregisterDeviceCallback(any(), any(), any())
+    verify(versionZeroMockCoordinator, never()).unregisterOnLogRequestedListener(any(), any())
+  }
+
+  @Test
+  fun onCleanUp_correctlyInvokesCoordinator_onPlatformVersionOne() {
+    defaultProxyVersion1.cleanUp()
+
+    verify(versionOneMockCoordinator).unregisterConnectionCallback(any())
+    verify(versionOneMockCoordinator).unregisterDeviceCallback(any(), any(), any())
+    verify(versionOneMockCoordinator).unregisterOnLogRequestedListener(any(), any())
+  }
+
+  @Test
+  fun onCleanUp_correctlyInvokesCoordinator_onPlatformVersionTwo() {
+    defaultProxyVersion2.cleanUp()
+
+    verify(versionTwoMockCoordinator).unregisterConnectionCallback(any())
+    verify(versionTwoMockCoordinator).unregisterDeviceCallback(any(), any(), any())
+    verify(versionTwoMockCoordinator).unregisterOnLogRequestedListener(any(), any())
+  }
+}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/DeviceSystemQueryCacheTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/DeviceSystemQueryCacheTest.kt
new file mode 100644
index 0000000..3fd2d9e
--- /dev/null
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/DeviceSystemQueryCacheTest.kt
@@ -0,0 +1,381 @@
+package com.google.android.connecteddevice.core
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.companionprotos.FeatureSupportResponse
+import com.google.android.companionprotos.FeatureSupportStatus
+import com.google.android.companionprotos.Query
+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.SYSTEM_FEATURE_ID
+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.common.truth.Truth.assertThat
+import com.google.protobuf.ByteString
+import java.nio.charset.StandardCharsets
+import java.util.UUID
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DeviceSystemQueryCacheTest {
+
+  private lateinit var cache: DeviceSystemQueryCache
+
+  @Before
+  fun setUp() {
+    cache = DeviceSystemQueryCache()
+  }
+
+  @Test
+  fun getCached_notSystemFeatureRecipient_noCachedMessage() {
+    val message =
+      DeviceMessage.createOutgoingMessage(
+        /* recipient= */ UUID.randomUUID(),
+        /* isMessageEncrypted= */ true,
+        /* operationType= */ OperationType.QUERY,
+        /* message= */ ByteArray(0),
+      )
+
+    assertThat(cache.getCached(message)).isNull()
+  }
+
+  @Test
+  fun getCached_notQueryType_noCachedMessage() {
+    val message =
+      DeviceMessage.createOutgoingMessage(
+        /* recipient= */ SYSTEM_FEATURE,
+        /* isMessageEncrypted= */ true,
+        /* operationType= */ OperationType.CLIENT_MESSAGE,
+        /* message= */ ByteArray(0),
+      )
+
+    assertThat(cache.getCached(message)).isNull()
+  }
+
+  @Test
+  fun getCached_nonParseableQuery_noCachedMessage() {
+    val message =
+      DeviceMessage.createOutgoingMessage(
+        /* recipient= */ SYSTEM_FEATURE,
+        /* isMessageEncrypted= */ true,
+        /* operationType= */ OperationType.QUERY,
+        // Query proto has non-optional fields so its serialization cannot be empty.
+        /* message= */ ByteArray(0),
+      )
+
+    assertThat(cache.getCached(message)).isNull()
+  }
+
+  @Test
+  fun getCached_noCachedResponse_queryTypeIsTracked() {
+    val queryId = 4
+    val sender = UUID.randomUUID()
+    val message =
+      createSystemQueryMessage(
+        queryId = queryId,
+        querySender = sender,
+        systemQueryType = SystemQueryType.APP_NAME,
+      )
+
+    assertThat(cache.getCached(message)).isNull()
+
+    val key = cache.trackedQueryTypes.keys.first()
+    assertThat(key.first).isEqualTo(sender)
+    assertThat(key.second).isEqualTo(queryId)
+    assertThat(cache.trackedQueryTypes[key]!!).isEqualTo(SystemQueryType.APP_NAME)
+  }
+
+  @Test
+  fun getCached_queryAppName_returnsResponse() {
+    cache.appName = "appName"
+    val queryMessage =
+      createSystemQueryMessage(
+        queryId = 4,
+        querySender = UUID.randomUUID(),
+        systemQueryType = SystemQueryType.APP_NAME,
+      )
+
+    val cached = checkNotNull(cache.getCached(queryMessage))
+    val queryResponse = QueryResponse.parseFrom(cached.message)
+    val cachedAppName = String(queryResponse.response.toByteArray(), StandardCharsets.UTF_8)
+    assertThat(cachedAppName).isEqualTo("appName")
+  }
+
+  @Test
+  fun getCached_queryDeviceName_returnsResponse() {
+    cache.deviceName = "deviceName"
+    val queryMessage =
+      createSystemQueryMessage(
+        queryId = 4,
+        querySender = UUID.randomUUID(),
+        systemQueryType = SystemQueryType.DEVICE_NAME,
+      )
+
+    val cached = checkNotNull(cache.getCached(queryMessage))
+    val queryResponse = QueryResponse.parseFrom(cached.message)
+    val cachedDeviceName = String(queryResponse.response.toByteArray(), StandardCharsets.UTF_8)
+    assertThat(cachedDeviceName).isEqualTo("deviceName")
+  }
+
+  @Test
+  fun getCached_querySupportedFeature_returnsResponse() {
+    val supported = UUID.randomUUID()
+    cache.supportedFeatures.add(supported)
+    val queryMessage =
+      createSystemQueryMessage(
+        queryId = 4,
+        querySender = UUID.randomUUID(),
+        systemQueryType = SystemQueryType.IS_FEATURE_SUPPORTED,
+        systemQueryPayloads = listOf(supported.toString().toByteArray()),
+      )
+
+    val cached = checkNotNull(cache.getCached(queryMessage))
+
+    val queryResponse = QueryResponse.parseFrom(cached.message)
+    val featureSupportResponse = FeatureSupportResponse.parseFrom(queryResponse.response)
+    assertThat(featureSupportResponse.statusesList.size).isEqualTo(1)
+    val status = featureSupportResponse.statusesList.first()
+    assertThat(status.featureId).isEqualTo(supported.toString())
+    assertThat(status.isSupported).isTrue()
+  }
+
+  @Test
+  fun getCached_queryUnsupportedFeature_returnsNull() {
+    // This feature ID is not cached.
+    val unsupported = UUID.randomUUID()
+    val queryMessage =
+      createSystemQueryMessage(
+        queryId = 4,
+        querySender = UUID.randomUUID(),
+        systemQueryType = SystemQueryType.IS_FEATURE_SUPPORTED,
+        systemQueryPayloads = listOf(unsupported.toString().toByteArray()),
+      )
+
+    assertThat(cache.getCached(queryMessage)).isNull()
+  }
+
+  @Test
+  fun getCached_queryMixedFeatures_returnsNull() {
+    val supported = UUID.randomUUID()
+    cache.supportedFeatures.add(supported)
+    // This feature ID is not cached.
+    val unsupported = UUID.randomUUID()
+    val queryMessage =
+      createSystemQueryMessage(
+        queryId = 4,
+        querySender = UUID.randomUUID(),
+        systemQueryType = SystemQueryType.IS_FEATURE_SUPPORTED,
+        systemQueryPayloads =
+          listOf(unsupported.toString().toByteArray(), supported.toString().toByteArray()),
+      )
+
+    assertThat(cache.getCached(queryMessage)).isNull()
+  }
+
+  @Test
+  fun cache_nonSystemQueryResponse_notCached() {
+    val message =
+      DeviceMessage.createIncomingMessage(
+        /* recipient= */ UUID.randomUUID(),
+        /* isMessageEncrypted= */ true,
+        /* operationType= */ OperationType.CLIENT_MESSAGE,
+        /* message= */ ByteArray(0),
+        /* originalMessageSize= */ 0,
+      )
+    cache.cache(message)
+
+    assertNothingIsCached()
+  }
+
+  @Test
+  fun cache_nonParseableQueryResponse_notCached() {
+    val message =
+      DeviceMessage.createIncomingMessage(
+        /* recipient= */ UUID.randomUUID(),
+        /* isMessageEncrypted= */ true,
+        /* operationType= */ OperationType.QUERY_RESPONSE,
+        // Proto has non-optional fields so its serialization cannot be empty.
+        /* message= */ ByteArray(0),
+        /* originalMessageSize= */ 0,
+      )
+    cache.cache(message)
+
+    assertNothingIsCached()
+  }
+
+  @Test
+  fun cache_cachedAppName() {
+    val queryId = 4
+    val sender = UUID.randomUUID()
+    val queryMessage =
+      createSystemQueryMessage(
+        queryId = queryId,
+        querySender = sender,
+        systemQueryType = SystemQueryType.APP_NAME,
+      )
+
+    // Try retrieving the cached response - allows the cache to track the query type.
+    assertThat(cache.getCached(queryMessage)).isNull()
+    // Then send the query response - the response should be cached.
+    val responseMessage =
+      createSystemQueryResponseMessage(
+        queryId = queryId,
+        querySender = sender,
+        systemQueryPayload = "appName".toByteArray(),
+      )
+    cache.cache(responseMessage)
+
+    assertThat(cache.appName).isEqualTo("appName")
+  }
+
+  @Test
+  fun cache_cachedDeviceName() {
+    val queryId = 4
+    val sender = UUID.randomUUID()
+    val queryMessage =
+      createSystemQueryMessage(
+        queryId = queryId,
+        querySender = sender,
+        systemQueryType = SystemQueryType.DEVICE_NAME,
+      )
+
+    // Try retrieving the cached response - allows the cache to track the query type.
+    assertThat(cache.getCached(queryMessage)).isNull()
+    // Then send the query response - the response should be cached.
+    val responseMessage =
+      createSystemQueryResponseMessage(
+        queryId = queryId,
+        querySender = sender,
+        systemQueryPayload = "deviceName".toByteArray(),
+      )
+    cache.cache(responseMessage)
+
+    assertThat(cache.deviceName).isEqualTo("deviceName")
+  }
+
+  @Test
+  fun cache_cachedFeatureSupportStatus() {
+    val queryId = 4
+    val sender = UUID.randomUUID()
+    val queriedFeature = UUID.randomUUID()
+    val queryMessage =
+      createSystemQueryMessage(
+        queryId = queryId,
+        querySender = sender,
+        systemQueryType = SystemQueryType.IS_FEATURE_SUPPORTED,
+        systemQueryPayloads = listOf(queriedFeature.toString().toByteArray()),
+      )
+
+    // Try retrieving the cached response - allows the cache to track the query type.
+    assertThat(cache.getCached(queryMessage)).isNull()
+    // Then send the query response - the response should be cached.
+    val status =
+      FeatureSupportStatus.newBuilder().run {
+        featureId = queriedFeature.toString()
+        isSupported = true
+        build()
+      }
+    val featureSupportResponse =
+      FeatureSupportResponse.newBuilder().run {
+        addStatuses(status)
+        build()
+      }
+    val responseMessage =
+      createSystemQueryResponseMessage(
+        queryId = queryId,
+        querySender = sender,
+        systemQueryPayload = featureSupportResponse.toByteArray(),
+      )
+    cache.cache(responseMessage)
+
+    assertThat(queriedFeature in cache.supportedFeatures).isTrue()
+  }
+
+  @Test
+  fun cache_unrecognizedSenderAndId_notCached() {
+    val message =
+      createSystemQueryResponseMessage(
+        queryId = 4,
+        querySender = UUID.randomUUID(),
+        systemQueryPayload = "appName".toByteArray(),
+      )
+
+    cache.cache(message)
+
+    assertNothingIsCached()
+  }
+
+  @Test
+  fun clear_allCleared() {
+    cache.appName = "appName"
+
+    cache.clear()
+
+    assertNothingIsCached()
+  }
+
+  private fun assertNothingIsCached() {
+    assertThat(cache.appName).isNull()
+    assertThat(cache.deviceName).isNull()
+    assertThat(cache.supportedFeatures).isEmpty()
+  }
+
+  private fun createSystemQueryMessage(
+    queryId: Int,
+    querySender: UUID,
+    systemQueryType: SystemQueryType,
+    systemQueryPayloads: List<ByteArray> = emptyList(),
+  ): DeviceMessage {
+    val systemQuery =
+      SystemQuery.newBuilder().run {
+        type = systemQueryType
+        addAllPayloads(systemQueryPayloads.map { ByteString.copyFrom(it) })
+        build()
+      }
+    val query =
+      Query.newBuilder().run {
+        id = queryId
+        sender = ByteString.copyFrom(ByteUtils.uuidToBytes(querySender))
+        request = ByteString.copyFrom(systemQuery.toByteArray())
+        build()
+      }
+    val message =
+      DeviceMessage.createOutgoingMessage(
+        /* recipient= */ SYSTEM_FEATURE,
+        /* isMessageEncrypted= */ true,
+        /* operationType= */ OperationType.QUERY,
+        /* message= */ query.toByteArray(),
+      )
+    return message
+  }
+
+  private fun createSystemQueryResponseMessage(
+    queryId: Int,
+    querySender: UUID,
+    systemQueryPayload: ByteArray,
+  ): DeviceMessage {
+    val queryResponse =
+      QueryResponse.newBuilder().run {
+        this.queryId = queryId
+        success = true
+        response = ByteString.copyFrom(systemQueryPayload)
+        build()
+      }
+    val message =
+      DeviceMessage.createIncomingMessage(
+        /* recipient= */ querySender,
+        /* isMessageEncrypted= */ true,
+        /* operationType= */ OperationType.QUERY_RESPONSE,
+        /* message= */ queryResponse.toByteArray(),
+        /* originalMessageSize= */ 0,
+      )
+    return message
+  }
+
+  companion object {
+    private val SYSTEM_FEATURE: UUID = SYSTEM_FEATURE_ID.uuid
+  }
+}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/FeatureCoordinatorTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/FeatureCoordinatorTest.kt
index de2b21c..0be386d 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/FeatureCoordinatorTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/FeatureCoordinatorTest.kt
@@ -2,8 +2,9 @@
 
 import android.os.ParcelUuid
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.android.companionprotos.DeviceMessageProto
 import com.google.android.companionprotos.OperationProto.OperationType
+import com.google.android.companionprotos.message
+import com.google.android.connecteddevice.api.Connector.Companion.SYSTEM_FEATURE_ID
 import com.google.android.connecteddevice.api.IAssociationCallback
 import com.google.android.connecteddevice.api.IConnectionCallback
 import com.google.android.connecteddevice.api.IDeviceAssociationCallback
@@ -21,6 +22,8 @@
 import com.google.android.connecteddevice.model.ConnectedDevice
 import com.google.android.connecteddevice.model.DeviceMessage
 import com.google.android.connecteddevice.model.DeviceMessage.OperationType.CLIENT_MESSAGE
+import com.google.android.connecteddevice.model.DeviceMessage.OperationType.QUERY
+import com.google.android.connecteddevice.model.DeviceMessage.OperationType.QUERY_RESPONSE
 import com.google.android.connecteddevice.model.Errors.DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED
 import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
 import com.google.android.connecteddevice.util.ByteUtils
@@ -42,13 +45,18 @@
 @RunWith(AndroidJUnit4::class)
 class FeatureCoordinatorTest {
   private val mockController: DeviceController = mock()
-
   private val mockStorage: ConnectedDeviceStorage = mock()
-
   private val mockLoggingManager: LoggingManager = mock()
+  private val mockSystemQueryCache: SystemQueryCache = mock()
 
   private val coordinator =
-    FeatureCoordinator(mockController, mockStorage, mockLoggingManager, directExecutor())
+    FeatureCoordinator(
+      mockController,
+      mockStorage,
+      mockSystemQueryCache,
+      mockLoggingManager,
+      directExecutor()
+    )
 
   private val safeCoordinator = coordinator.safeFeatureCoordinator
 
@@ -59,21 +67,21 @@
         UUID.randomUUID().toString(),
         "driverDeviceName1",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val driverDevice2 =
       ConnectedDevice(
         UUID.randomUUID().toString(),
         "driverDeviceName2",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val passengerDevice =
       ConnectedDevice(
         UUID.randomUUID().toString(),
         "passengerDeviceName",
         /* belongsToDriver= */ false,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     whenever(mockController.connectedDevices)
       .thenReturn(listOf(driverDevice1, passengerDevice, driverDevice2))
@@ -90,21 +98,21 @@
         UUID.randomUUID().toString(),
         "passengerDevice1",
         /* belongsToDriver= */ false,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val passengerDevice2 =
       ConnectedDevice(
         UUID.randomUUID().toString(),
         "passengerDevice2",
         /* belongsToDriver= */ false,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val driverDevice =
       ConnectedDevice(
         UUID.randomUUID().toString(),
         "driverDevice",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     whenever(mockController.connectedDevices)
       .thenReturn(listOf(passengerDevice1, driverDevice, passengerDevice2))
@@ -121,21 +129,21 @@
         UUID.randomUUID().toString(),
         "driverDeviceName1",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val driverDevice2 =
       ConnectedDevice(
         UUID.randomUUID().toString(),
         "driverDeviceName2",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val passengerDevice =
       ConnectedDevice(
         UUID.randomUUID().toString(),
         "passengerDeviceName",
         /* belongsToDriver= */ false,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     whenever(mockController.connectedDevices)
       .thenReturn(listOf(driverDevice1, passengerDevice, driverDevice2))
@@ -154,7 +162,7 @@
         UUID.randomUUID().toString(),
         "driverDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.onDeviceConnectedInternal(driverDevice)
@@ -170,9 +178,9 @@
     val passengerDevice =
       ConnectedDevice(
         UUID.randomUUID().toString(),
-        "driverDeviceName",
+        "passengerDeviceName",
         /* belongsToDriver= */ false,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.onDeviceConnectedInternal(passengerDevice)
@@ -189,7 +197,7 @@
         UUID.randomUUID().toString(),
         "driverDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.onDeviceConnectedInternal(driverDevice)
@@ -204,9 +212,9 @@
     val passengerDevice =
       ConnectedDevice(
         UUID.randomUUID().toString(),
-        "driverDeviceName",
+        "passengerDeviceName",
         /* belongsToDriver= */ false,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.onDeviceConnectedInternal(passengerDevice)
@@ -223,7 +231,7 @@
         UUID.randomUUID().toString(),
         "driverDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.onDeviceConnectedInternal(driverDevice)
@@ -238,9 +246,9 @@
     val passengerDevice =
       ConnectedDevice(
         UUID.randomUUID().toString(),
-        "driverDeviceName",
+        "passengerDeviceName",
         /* belongsToDriver= */ false,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.onDeviceConnectedInternal(passengerDevice)
@@ -257,7 +265,7 @@
         UUID.randomUUID().toString(),
         "driverDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.onDeviceDisconnectedInternal(driverDevice)
@@ -272,9 +280,9 @@
     val passengerDevice =
       ConnectedDevice(
         UUID.randomUUID().toString(),
-        "driverDeviceName",
+        "passengerDeviceName",
         /* belongsToDriver= */ false,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.onDeviceDisconnectedInternal(passengerDevice)
@@ -291,7 +299,7 @@
         UUID.randomUUID().toString(),
         "driverDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.onDeviceDisconnectedInternal(driverDevice)
@@ -306,9 +314,9 @@
     val passengerDevice =
       ConnectedDevice(
         UUID.randomUUID().toString(),
-        "driverDeviceName",
+        "passengerDeviceName",
         /* belongsToDriver= */ false,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.onDeviceDisconnectedInternal(passengerDevice)
@@ -325,7 +333,7 @@
         UUID.randomUUID().toString(),
         "driverDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.onDeviceDisconnectedInternal(driverDevice)
@@ -340,9 +348,9 @@
     val passengerDevice =
       ConnectedDevice(
         UUID.randomUUID().toString(),
-        "driverDeviceName",
+        "passengerDeviceName",
         /* belongsToDriver= */ false,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.onDeviceDisconnectedInternal(passengerDevice)
@@ -359,7 +367,7 @@
         UUID.randomUUID().toString(),
         "driverDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.unregisterConnectionCallback(mockConnectionCallback)
@@ -375,9 +383,9 @@
     val passengerDevice =
       ConnectedDevice(
         UUID.randomUUID().toString(),
-        "driverDeviceName",
+        "passengerDeviceName",
         /* belongsToDriver= */ false,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.unregisterConnectionCallback(mockConnectionCallback)
@@ -395,7 +403,7 @@
         UUID.randomUUID().toString(),
         "driverDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.unregisterConnectionCallback(mockConnectionCallback)
@@ -413,14 +421,14 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val missedMessage =
       DeviceMessage.createOutgoingMessage(
         recipientId.uuid,
         /* isMessageEncrypted= */ true,
         CLIENT_MESSAGE,
-        ByteUtils.randomBytes(10)
+        ByteUtils.randomBytes(10),
       )
 
     coordinator.onMessageReceivedInternal(connectedDevice, missedMessage)
@@ -439,7 +447,7 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.registerDeviceCallback(connectedDevice, recipientId, deviceCallback)
@@ -461,7 +469,7 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     coordinator.registerDeviceCallback(connectedDevice, recipientId, deadDeviceCallback)
@@ -482,7 +490,7 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     coordinator.registerDeviceCallback(connectedDevice, recipientId, deviceCallback)
     coordinator.registerDeviceCallback(connectedDevice, recipientId, duplicateDeviceCallback)
@@ -494,6 +502,36 @@
   }
 
   @Test
+  fun registerDuplicateDeviceCallback_onDeviceDisconnected_clearBlockedRecipients() {
+    val mockConnectionCallback: IConnectionCallback = mockToBeAlive()
+    coordinator.registerDriverConnectionCallback(mockConnectionCallback)
+    val deviceCallback: IDeviceCallback = mockToBeAlive()
+    val duplicateDeviceCallback: IDeviceCallback = mockToBeAlive()
+    val callbackAfterReconnection: IDeviceCallback = mockToBeAlive()
+    val recipientId = ParcelUuid(UUID.randomUUID())
+    val connectedDevice =
+      ConnectedDevice(
+        UUID.randomUUID().toString(),
+        "testDeviceName",
+        /* belongsToDriver= */ true,
+        /* hasSecureChannel= */ true,
+      )
+
+    coordinator.registerDeviceCallback(connectedDevice, recipientId, deviceCallback)
+    coordinator.registerDeviceCallback(connectedDevice, recipientId, duplicateDeviceCallback)
+
+    verify(deviceCallback)
+      .onDeviceError(connectedDevice, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED)
+    verify(duplicateDeviceCallback)
+      .onDeviceError(connectedDevice, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED)
+
+    coordinator.onDeviceDisconnectedInternal(connectedDevice)
+    coordinator.registerDeviceCallback(connectedDevice, recipientId, callbackAfterReconnection)
+    verify(callbackAfterReconnection, never())
+      .onDeviceError(connectedDevice, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED)
+  }
+
+  @Test
   fun unregisterDeviceCallback_callbackNotInvokedAfterUnregistering() {
     val deviceCallback: IDeviceCallback = mockToBeAlive()
     val recipientId = ParcelUuid(UUID.randomUUID())
@@ -502,14 +540,14 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val message =
       DeviceMessage.createOutgoingMessage(
         recipientId.uuid,
         /* isMessageEncrypted= */ true,
         CLIENT_MESSAGE,
-        ByteUtils.randomBytes(10)
+        ByteUtils.randomBytes(10),
       )
 
     coordinator.registerDeviceCallback(connectedDevice, recipientId, deviceCallback)
@@ -528,14 +566,14 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val message =
       DeviceMessage.createOutgoingMessage(
         recipientId.uuid,
         /* isMessageEncrypted= */ true,
         CLIENT_MESSAGE,
-        ByteUtils.randomBytes(10)
+        ByteUtils.randomBytes(10),
       )
 
     coordinator.registerDeviceCallback(connectedDevice, recipientId, deviceCallback)
@@ -554,14 +592,14 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val message =
       DeviceMessage.createOutgoingMessage(
         otherRecipientId.uuid,
         /* isMessageEncrypted= */ true,
         CLIENT_MESSAGE,
-        ByteUtils.randomBytes(10)
+        ByteUtils.randomBytes(10),
       )
 
     coordinator.registerDeviceCallback(connectedDevice, recipientId, deviceCallback)
@@ -580,14 +618,14 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val message =
       DeviceMessage.createOutgoingMessage(
         recipientId.uuid,
         /* isMessageEncrypted= */ true,
         CLIENT_MESSAGE,
-        ByteUtils.randomBytes(10)
+        ByteUtils.randomBytes(10),
       )
     coordinator.registerDeviceCallback(connectedDevice, recipientId, deviceCallback)
     coordinator.registerDeviceCallback(connectedDevice, recipientId, duplicateDeviceCallback)
@@ -608,7 +646,7 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     coordinator.registerDeviceCallback(connectedDevice, recipientId, deviceCallback)
     coordinator.registerDeviceCallback(connectedDevice, recipientId, duplicateDeviceCallback)
@@ -621,7 +659,7 @@
         recipientId.uuid,
         /* isMessageEncrypted= */ true,
         CLIENT_MESSAGE,
-        ByteUtils.randomBytes(10)
+        ByteUtils.randomBytes(10),
       )
     coordinator.onMessageReceivedInternal(connectedDevice, message)
 
@@ -636,33 +674,77 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val nullRecipientMessage =
       DeviceMessage.createOutgoingMessage(
         /* recipient= */ null,
         /* isMessageEncrypted= */ true,
         CLIENT_MESSAGE,
-        ByteUtils.randomBytes(10)
+        ByteUtils.randomBytes(10),
       )
     coordinator.onMessageReceivedInternal(connectedDevice, nullRecipientMessage)
   }
 
   @Test
+  fun onMessageReceived_shouldCacheMessage_cachesMessage() {
+    val connectedDevice =
+      ConnectedDevice(
+        UUID.randomUUID().toString(),
+        "testDeviceName",
+        /* belongsToDriver= */ true,
+        /* hasSecureChannel= */ true,
+      )
+    val message =
+      DeviceMessage.createIncomingMessage(
+        /* recipient= */ UUID.randomUUID(),
+        /* isMessageEncrypted= */ false,
+        /* operationType= */ QUERY_RESPONSE,
+        /* message= */ ByteArray(0),
+        /* originalMessageSize= */ 0,
+      )
+    coordinator.onMessageReceivedInternal(connectedDevice, message, shouldCacheMessage = true)
+
+    verify(mockSystemQueryCache).maybeCacheResponse(connectedDevice, message)
+  }
+
+  @Test
+  fun onMessageReceived_shouldNotCacheMessage_skipsCache() {
+    val connectedDevice =
+      ConnectedDevice(
+        UUID.randomUUID().toString(),
+        "testDeviceName",
+        /* belongsToDriver= */ true,
+        /* hasSecureChannel= */ true,
+      )
+    val message =
+      DeviceMessage.createIncomingMessage(
+        /* recipient= */ UUID.randomUUID(),
+        /* isMessageEncrypted= */ false,
+        /* operationType= */ QUERY_RESPONSE,
+        /* message= */ ByteArray(0),
+        /* originalMessageSize= */ 0,
+      )
+    coordinator.onMessageReceivedInternal(connectedDevice, message, shouldCacheMessage = false)
+
+    verify(mockSystemQueryCache, never()).maybeCacheResponse(any(), any())
+  }
+
+  @Test
   fun sendMessage_sendsMessageToController() {
     val connectedDevice =
       ConnectedDevice(
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val message =
       DeviceMessage.createOutgoingMessage(
         UUID.randomUUID(),
         /* isMessageEncrypted= */ true,
         CLIENT_MESSAGE,
-        ByteUtils.randomBytes(10)
+        ByteUtils.randomBytes(10),
       )
 
     coordinator.sendMessage(connectedDevice, message)
@@ -671,6 +753,42 @@
   }
 
   @Test
+  fun sendMessage_cachedQueryResponse_skipsController() {
+    val connectedDevice =
+      ConnectedDevice(
+        UUID.randomUUID().toString(),
+        "testDeviceName",
+        /* belongsToDriver= */ true,
+        /* hasSecureChannel= */ true,
+      )
+    val sender = UUID.randomUUID()
+    val deviceCallback: IDeviceCallback = mockToBeAlive()
+    coordinator.registerDeviceCallback(connectedDevice, ParcelUuid(sender), deviceCallback)
+
+    val cachedResponse =
+      DeviceMessage.createIncomingMessage(
+        /* recipient= */ sender,
+        /* isMessageEncrypted= */ false,
+        /* operationType= */ QUERY_RESPONSE,
+        /* message= */ ByteArray(0),
+        /* originalMessageSize= */ 0,
+      )
+    whenever(mockSystemQueryCache.getCachedResponse(any(), any())).thenReturn(cachedResponse)
+
+    val message =
+      DeviceMessage.createOutgoingMessage(
+        SYSTEM_FEATURE_ID.uuid,
+        /* isMessageEncrypted= */ true,
+        QUERY,
+        ByteUtils.randomBytes(10),
+      )
+    coordinator.sendMessage(connectedDevice, message)
+
+    verify(mockController, never()).sendMessage(any(), any())
+    verify(deviceCallback).onMessageReceived(connectedDevice, cachedResponse)
+  }
+
+  @Test
   fun startAssociation_startsAssociationWithCorrectlySizedName() {
     val associationCallback: IAssociationCallback = mockToBeAlive()
 
@@ -707,7 +825,7 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     coordinator.registerDeviceCallback(connectedDevice, recipientId, deviceCallback)
     coordinator.registerDeviceCallback(connectedDevice, secondRecipientId, secondDeviceCallback)
@@ -729,14 +847,14 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val otherConnectedDevice =
       ConnectedDevice(
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     coordinator.registerDeviceCallback(connectedDevice, recipientId, deviceCallback)
     coordinator.registerDeviceCallback(otherConnectedDevice, otherRecipientId, otherDeviceCallback)
@@ -756,7 +874,7 @@
         UUID.randomUUID().toString(),
         "deviceAddress",
         "deviceName",
-        /* isConnectionEnabled= */ true
+        /* isConnectionEnabled= */ true,
       )
 
     coordinator.onAssociatedDeviceAddedInternal(associatedDevice)
@@ -773,7 +891,7 @@
         UUID.randomUUID().toString(),
         "deviceAddress",
         "deviceName",
-        /* isConnectionEnabled= */ true
+        /* isConnectionEnabled= */ true,
       )
 
     coordinator.onAssociatedDeviceRemovedInternal(associatedDevice)
@@ -790,7 +908,7 @@
         UUID.randomUUID().toString(),
         "deviceAddress",
         "deviceName",
-        /* isConnectionEnabled= */ true
+        /* isConnectionEnabled= */ true,
       )
 
     coordinator.onAssociatedDeviceUpdatedInternal(associatedDevice)
@@ -807,7 +925,7 @@
         UUID.randomUUID().toString(),
         "deviceAddress",
         "deviceName",
-        /* isConnectionEnabled= */ true
+        /* isConnectionEnabled= */ true,
       )
 
     coordinator.unregisterDeviceAssociationCallback(callback)
@@ -866,13 +984,13 @@
           UUID.randomUUID().toString(),
           /* deviceAddress= */ "",
           /* deviceName= */ null,
-          /* isConnectionEnabled= */ true
+          /* isConnectionEnabled= */ true,
         ),
         AssociatedDevice(
           UUID.randomUUID().toString(),
           /* deviceAddress= */ "",
           /* deviceName= */ null,
-          /* isConnectionEnabled= */ false
+          /* isConnectionEnabled= */ false,
         )
       )
     val listener: IOnAssociatedDevicesRetrievedListener = mockToBeAlive()
@@ -891,13 +1009,13 @@
           UUID.randomUUID().toString(),
           /* deviceAddress= */ "",
           /* deviceName= */ null,
-          /* isConnectionEnabled= */ true
+          /* isConnectionEnabled= */ true,
         ),
         AssociatedDevice(
           UUID.randomUUID().toString(),
           /* deviceAddress= */ "",
           /* deviceName= */ null,
-          /* isConnectionEnabled= */ false
+          /* isConnectionEnabled= */ false,
         )
       )
     val listener: IOnAssociatedDevicesRetrievedListener = mockToBeAlive()
@@ -916,13 +1034,13 @@
           UUID.randomUUID().toString(),
           /* deviceAddress= */ "",
           /* deviceName= */ null,
-          /* isConnectionEnabled= */ true
+          /* isConnectionEnabled= */ true,
         ),
         AssociatedDevice(
           UUID.randomUUID().toString(),
           /* deviceAddress= */ "",
           /* deviceName= */ null,
-          /* isConnectionEnabled= */ false
+          /* isConnectionEnabled= */ false,
         )
       )
     val listener: IOnAssociatedDevicesRetrievedListener = mockToBeAlive()
@@ -1015,21 +1133,21 @@
         UUID.randomUUID().toString(),
         "driverDeviceName1",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val driverDevice2 =
       ConnectedDevice(
         UUID.randomUUID().toString(),
         "driverDeviceName2",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val passengerDevice =
       ConnectedDevice(
         UUID.randomUUID().toString(),
         "passengerDeviceName",
         /* belongsToDriver= */ false,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     whenever(mockController.connectedDevices)
       .thenReturn(listOf(driverDevice1, passengerDevice, driverDevice2))
@@ -1082,17 +1200,15 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
-    val message =
-      DeviceMessageProto.Message.newBuilder()
-        .setRecipient(ByteString.copyFrom(ByteUtils.uuidToBytes(recipientId.uuid)))
-        .setIsPayloadEncrypted(true)
-        .setOperation(
-          OperationType.forNumber(/* CLIENT_MESSAGE */ 4) ?: OperationType.OPERATION_TYPE_UNKNOWN
-        )
-        .setPayload(ByteString.copyFrom(ByteUtils.randomBytes(10)))
-        .build()
+    val message = message {
+      operation =
+        OperationType.forNumber(/* CLIENT_MESSAGE */ 4) ?: OperationType.OPERATION_TYPE_UNKNOWN
+      isPayloadEncrypted = true
+      recipient = ByteString.copyFrom(ByteUtils.uuidToBytes(recipientId.uuid))
+      payload = ByteString.copyFrom(ByteUtils.randomBytes(10))
+    }
 
     val rawBytes = message.toByteArray()
 
@@ -1120,7 +1236,7 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     safeCoordinator.registerDeviceCallback(connectedDevice.deviceId, recipientId, deviceCallback)
@@ -1137,6 +1253,32 @@
   }
 
   @Test
+  fun safeFC_registerDeviceCallback_blocksRecipientIfRegisteredWithNonSafeDeviceCallback() {
+    val deviceCallback: IDeviceCallback = mockToBeAlive()
+    val duplicateDeviceCallback: ISafeDeviceCallback = mockToBeAlive()
+    val recipientId = ParcelUuid(UUID.randomUUID())
+    val connectedDevice =
+      ConnectedDevice(
+        UUID.randomUUID().toString(),
+        "testDeviceName",
+        /* belongsToDriver= */ true,
+        /* hasSecureChannel= */ true,
+      )
+
+    coordinator.registerDeviceCallback(connectedDevice, recipientId, deviceCallback)
+    safeCoordinator.registerDeviceCallback(
+      connectedDevice.deviceId,
+      recipientId,
+      duplicateDeviceCallback
+    )
+
+    verify(deviceCallback)
+      .onDeviceError(connectedDevice, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED)
+    verify(duplicateDeviceCallback)
+      .onDeviceError(connectedDevice.deviceId, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED)
+  }
+
+  @Test
   fun safeFC_registerDeviceCallback_ignoreDeadPreviousRegistererIfAlreadyRegistered() {
     val deadDeviceCallback: ISafeDeviceCallback = mockToBeDead()
     val duplicateDeviceCallback: ISafeDeviceCallback = mockToBeAlive()
@@ -1146,7 +1288,7 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
 
     safeCoordinator.registerDeviceCallback(
@@ -1175,7 +1317,7 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     safeCoordinator.registerDeviceCallback(connectedDevice.deviceId, recipientId, deviceCallback)
     safeCoordinator.registerDeviceCallback(
@@ -1203,14 +1345,14 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val message =
       DeviceMessage.createOutgoingMessage(
         recipientId.uuid,
         /* isMessageEncrypted= */ true,
         CLIENT_MESSAGE,
-        ByteUtils.randomBytes(10)
+        ByteUtils.randomBytes(10),
       )
 
     safeCoordinator.registerDeviceCallback(connectedDevice.deviceId, recipientId, deviceCallback)
@@ -1229,14 +1371,14 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val message =
       DeviceMessage.createOutgoingMessage(
         recipientId.uuid,
         /* isMessageEncrypted= */ true,
         CLIENT_MESSAGE,
-        ByteUtils.randomBytes(10)
+        ByteUtils.randomBytes(10),
       )
 
     safeCoordinator.registerDeviceCallback(connectedDevice.deviceId, recipientId, deviceCallback)
@@ -1255,14 +1397,14 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val message =
       DeviceMessage.createOutgoingMessage(
         otherRecipientId.uuid,
         /* isMessageEncrypted= */ true,
         CLIENT_MESSAGE,
-        ByteUtils.randomBytes(10)
+        ByteUtils.randomBytes(10),
       )
 
     safeCoordinator.registerDeviceCallback(connectedDevice.deviceId, recipientId, deviceCallback)
@@ -1281,14 +1423,14 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val message =
       DeviceMessage.createOutgoingMessage(
         recipientId.uuid,
         /* isMessageEncrypted= */ true,
         CLIENT_MESSAGE,
-        ByteUtils.randomBytes(10)
+        ByteUtils.randomBytes(10),
       )
     safeCoordinator.registerDeviceCallback(connectedDevice.deviceId, recipientId, deviceCallback)
     safeCoordinator.registerDeviceCallback(
@@ -1313,13 +1455,13 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     safeCoordinator.registerDeviceCallback(connectedDevice.deviceId, recipientId, deviceCallback)
     safeCoordinator.registerDeviceCallback(
       connectedDevice.deviceId,
       recipientId,
-      duplicateDeviceCallback
+      duplicateDeviceCallback,
     )
 
     // Clear the blocked recipient list.
@@ -1330,7 +1472,7 @@
         recipientId.uuid,
         /* isMessageEncrypted= */ true,
         CLIENT_MESSAGE,
-        ByteUtils.randomBytes(10)
+        ByteUtils.randomBytes(10),
       )
     coordinator.onMessageReceivedInternal(connectedDevice, message)
 
@@ -1345,36 +1487,70 @@
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
     val nullRecipientMessage =
       DeviceMessage.createOutgoingMessage(
         /* recipient= */ null,
         /* isMessageEncrypted= */ true,
         CLIENT_MESSAGE,
-        ByteUtils.randomBytes(10)
+        ByteUtils.randomBytes(10),
       )
     coordinator.onMessageReceivedInternal(connectedDevice, nullRecipientMessage)
   }
 
   @Test
-  fun safeFC_sendMessage_sendsMessageToController() {
+  fun safeFC_sendMessage_succeedsOnRegisteredDevice() {
+    val deviceId = UUID.randomUUID().toString()
+    val connectedDevice =
+      ConnectedDevice(
+        deviceId,
+        "testDeviceName",
+        /* belongsToDriver= */ true,
+        /* hasSecureChannel= */ true,
+      )
+    val message = message {
+      operation =
+        OperationType.forNumber(/* CLIENT_MESSAGE */ 4) ?: OperationType.OPERATION_TYPE_UNKNOWN
+      isPayloadEncrypted = true
+      recipient = ByteString.copyFrom(ByteUtils.uuidToBytes(UUID.randomUUID()))
+      payload = ByteString.copyFrom(ByteUtils.randomBytes(10))
+    }
+
+    val rawBytes = message.toByteArray()
+
+    whenever(mockController.connectedDevices).thenReturn(listOf(connectedDevice))
+    whenever(mockController.sendMessage(any(), any())).thenReturn(true)
+    val messageSent = safeCoordinator.sendMessage(deviceId, rawBytes)
+
+    val deviceMessage =
+      DeviceMessage.createOutgoingMessage(
+        ByteUtils.bytesToUUID(message.recipient.toByteArray()),
+        message.isPayloadEncrypted,
+        DeviceMessage.OperationType.fromValue(message.operation.number),
+        message.payload.toByteArray(),
+      )
+
+    assertThat(messageSent).isTrue()
+    verify(mockController).sendMessage(UUID.fromString(deviceId), deviceMessage)
+  }
+
+  @Test
+  fun safeFC_sendMessage_failsOnUnregisteredDevice() {
     val connectedDevice =
       ConnectedDevice(
         UUID.randomUUID().toString(),
         "testDeviceName",
         /* belongsToDriver= */ true,
-        /* hasSecureChannel= */ true
+        /* hasSecureChannel= */ true,
       )
-    val message =
-      DeviceMessageProto.Message.newBuilder()
-        .setRecipient(ByteString.copyFrom(ByteUtils.uuidToBytes(UUID.randomUUID())))
-        .setIsPayloadEncrypted(true)
-        .setOperation(
-          OperationType.forNumber(/* CLIENT_MESSAGE */ 4) ?: OperationType.OPERATION_TYPE_UNKNOWN
-        )
-        .setPayload(ByteString.copyFrom(ByteUtils.randomBytes(10)))
-        .build()
+    val message = message {
+      operation =
+        OperationType.forNumber(/* CLIENT_MESSAGE */ 4) ?: OperationType.OPERATION_TYPE_UNKNOWN
+      isPayloadEncrypted = true
+      recipient = ByteString.copyFrom(ByteUtils.uuidToBytes(UUID.randomUUID()))
+      payload = ByteString.copyFrom(ByteUtils.randomBytes(10))
+    }
 
     val rawBytes = message.toByteArray()
 
@@ -1385,11 +1561,12 @@
         ByteUtils.bytesToUUID(message.recipient.toByteArray()),
         message.isPayloadEncrypted,
         DeviceMessage.OperationType.fromValue(message.operation.number),
-        message.payload.toByteArray()
+        message.payload.toByteArray(),
       )
 
-    assertThat(messageSent).isFalse() // Recipient not registered so this will return false
-    verify(mockController).sendMessage(UUID.fromString(connectedDevice.deviceId), deviceMessage)
+    assertThat(messageSent).isFalse()
+    verify(mockController, never())
+      .sendMessage(UUID.fromString(connectedDevice.deviceId), deviceMessage)
   }
 
   @Test
@@ -1430,13 +1607,13 @@
           UUID.randomUUID().toString(),
           /* deviceAddress= */ "",
           /* deviceName= */ null,
-          /* isConnectionEnabled= */ true
+          /* isConnectionEnabled= */ true,
         ),
         AssociatedDevice(
           UUID.randomUUID().toString(),
           /* deviceAddress= */ "",
           /* deviceName= */ null,
-          /* isConnectionEnabled= */ false
+          /* isConnectionEnabled= */ false,
         )
       )
     val listener: ISafeOnAssociatedDevicesRetrievedListener = mockToBeAlive()
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 56df277..05a1b2a 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
@@ -101,12 +101,13 @@
     whenever(spyStorage.hashWithChallengeSecret(any(), any())).thenReturn(TEST_CHALLENGE)
     deviceController =
       MultiProtocolDeviceController(
+        context,
         protocolDelegate,
         spyStorage,
         mockOobRunner,
         testAssociationServiceUuid.uuid,
         enablePassenger = false,
-        callbackExecutor = directExecutor()
+        storageExecutor = directExecutor()
       )
     deviceController.registerCallback(mockCallback, directExecutor())
     secureChannel =
@@ -143,12 +144,13 @@
       }
 
     MultiProtocolDeviceController(
+        context,
         protocolDelegate,
         transientErrorStorage,
         mockOobRunner,
         testAssociationServiceUuid.uuid,
         enablePassenger = false,
-        callbackExecutor = directExecutor()
+        storageExecutor = directExecutor()
       )
       .start()
 
@@ -178,12 +180,13 @@
     whenever(spyStorage.allAssociatedDevices).thenReturn(listOf(driverDevice, passengerDevice))
     deviceController =
       MultiProtocolDeviceController(
+        context,
         protocolDelegate,
         spyStorage,
         mockOobRunner,
         testAssociationServiceUuid.uuid,
         enablePassenger = true,
-        callbackExecutor = directExecutor()
+        storageExecutor = directExecutor()
       )
 
     deviceController.start()
@@ -215,12 +218,13 @@
     whenever(spyStorage.allAssociatedDevices).thenReturn(listOf(driverDevice, passengerDevice))
     deviceController =
       MultiProtocolDeviceController(
+        context,
         protocolDelegate,
         spyStorage,
         mockOobRunner,
         testAssociationServiceUuid.uuid,
         enablePassenger = false,
-        callbackExecutor = directExecutor()
+        storageExecutor = directExecutor()
       )
 
     deviceController.start()
@@ -350,12 +354,13 @@
       )
     deviceController =
       MultiProtocolDeviceController(
+        context,
         protocolDelegate,
         spyStorage,
         mockOobRunner,
         testAssociationServiceUuid.uuid,
         enablePassenger = false,
-        callbackExecutor = directExecutor()
+        storageExecutor = directExecutor()
       )
     deviceController.registerCallback(mockCallback, directExecutor())
     deviceController.start()
@@ -672,9 +677,32 @@
     )
 
     verify(spyStorage).saveChallengeSecret(deviceId.toString(), secret)
-    verify(mockCallback).onDeviceConnected(any())
+    assertThat(deviceController.getConnectedDevice(deviceId)).isNotNull()
+    verify(mockAssociationCallback).onAssociationCompleted()
+  }
+
+  @Test
+  fun onAssociatedDeviceAdded_issuesDeviceConnectedCallback() {
+    val deviceId = UUID.randomUUID()
+    val deviceName = "TestDeviceName"
+    val associatedDevice =
+      AssociatedDevice(
+        deviceId.toString(),
+        "deviceAddress",
+        deviceName,
+        /* isConnectionEnabled= */ true
+      )
+    argumentCaptor<ConnectedDeviceStorage.AssociatedDeviceCallback>().apply {
+      verify(spyStorage).registerAssociatedDeviceCallback(capture())
+      firstValue.onAssociatedDeviceAdded(associatedDevice)
+    }
+    argumentCaptor<ConnectedDevice>().apply {
+      verify(mockCallback).onDeviceConnected(capture())
+      assertThat(firstValue.deviceId).isEqualTo(associatedDevice.deviceId)
+      assertThat(firstValue.hasSecureChannel()).isFalse()
+    }
     verify(mockCallback).onSecureChannelEstablished(any())
-    assertThat(deviceController.connectedDevices).isNotEmpty()
+    verify(spyStorage).getAllAssociatedDevices()
   }
 
   @Test
@@ -762,12 +790,13 @@
       )
     deviceController =
       MultiProtocolDeviceController(
+        context,
         protocolDelegate,
         spyStorage,
         mockOobRunner,
         testAssociationServiceUuid.uuid,
         enablePassenger = false,
-        callbackExecutor = directExecutor()
+        storageExecutor = directExecutor()
       )
 
     deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
@@ -806,12 +835,13 @@
       )
     deviceController =
       MultiProtocolDeviceController(
+        context,
         protocolDelegate,
         spyStorage,
         mockOobRunner,
         testAssociationServiceUuid.uuid,
         enablePassenger = true,
-        callbackExecutor = directExecutor()
+        storageExecutor = directExecutor()
       )
 
     deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
@@ -868,12 +898,13 @@
     // Recreate controller after registering mock returns since they are used in the constructor.
     deviceController =
       MultiProtocolDeviceController(
+        context,
         protocolDelegate,
         spyStorage,
         mockOobRunner,
         testAssociationServiceUuid.uuid,
         enablePassenger = false,
-        callbackExecutor = directExecutor()
+        storageExecutor = directExecutor()
       )
     deviceController.registerCallback(mockCallback, directExecutor())
     deviceController.start()
@@ -963,12 +994,13 @@
     protocolDelegate.addProtocol(protocol2)
     deviceController =
       MultiProtocolDeviceController(
+          context,
           protocolDelegate,
           spyStorage,
           mockOobRunner,
           testAssociationServiceUuid.uuid,
           enablePassenger = false,
-          callbackExecutor = directExecutor()
+          storageExecutor = directExecutor()
         )
         .apply { registerCallback(mockCallback, directExecutor()) }
     deviceController.initiateConnectionToDevice(deviceId.uuid)
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/SystemQueryCacheTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/SystemQueryCacheTest.kt
new file mode 100644
index 0000000..f242282
--- /dev/null
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/SystemQueryCacheTest.kt
@@ -0,0 +1,41 @@
+package com.google.android.connecteddevice.core
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.connecteddevice.model.ConnectedDevice
+import com.google.common.truth.Truth.assertThat
+import com.nhaarman.mockitokotlin2.mock
+import java.util.UUID
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SystemQueryCacheTest {
+
+  private lateinit var cache: SystemQueryCacheImpl
+
+  @Before
+  fun setUp() {
+    cache = SystemQueryCacheImpl()
+  }
+
+  @Test
+  fun maybeCacheResponse_createsCacheForNewDevice() {
+    val deviceId = UUID.randomUUID()
+    val device = createConnectedDevice(deviceId)
+
+    cache.maybeCacheResponse(device, mock())
+
+    assertThat(deviceId in cache.deviceCaches).isTrue()
+  }
+
+  companion object {
+    private fun createConnectedDevice(deviceId: UUID) =
+      ConnectedDevice(
+        deviceId.toString(),
+        /* deviceName= */ "",
+        /* belongsToDriver= */ true,
+        true,
+      )
+  }
+}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/logging/LoggingFeatureTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/logging/LoggingFeatureTest.java
index 2b7191a..6395f18 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/logging/LoggingFeatureTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/logging/LoggingFeatureTest.java
@@ -137,9 +137,9 @@
   @NonNull
   private static ConnectedDevice createConnectedDevice() {
     return new ConnectedDevice(
-        /* deviceId = */ "TEST_ID",
-        /* deviceName = */ "TEST_NAME",
-        /* belongsToActiveUser = */ true,
-        /* hasSecureChannel = */ true);
+        /* deviceId= */ "TEST_ID",
+        /* deviceName= */ "TEST_NAME",
+        /* belongsToDriver= */ true,
+        /* hasSecureChannel= */ true);
   }
 }
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/system/SystemFeatureTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/system/SystemFeatureTest.kt
index 825f175..00a9766 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/system/SystemFeatureTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/system/SystemFeatureTest.kt
@@ -2,6 +2,7 @@
 
 import android.bluetooth.BluetoothManager
 import android.content.Context
+import android.os.Looper
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.android.companionprotos.SystemQuery
@@ -24,12 +25,15 @@
 import com.nhaarman.mockitokotlin2.mock
 import com.nhaarman.mockitokotlin2.never
 import com.nhaarman.mockitokotlin2.spy
+import com.nhaarman.mockitokotlin2.times
 import com.nhaarman.mockitokotlin2.verify
 import java.nio.charset.StandardCharsets
 import java.util.UUID
+import kotlinx.coroutines.runBlocking
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.robolectric.Shadows.shadowOf
 
 @RunWith(AndroidJUnit4::class)
 class SystemFeatureTest {
@@ -46,13 +50,17 @@
       /* hasSecureChannel= */ true
     )
 
+  private val queriedFeature1 = UUID.randomUUID()
+  private val queriedFeature2 = UUID.randomUUID()
+  private val onConnectionQueriedFeatures = listOf(queriedFeature1, queriedFeature2)
+
   private lateinit var systemFeature: SystemFeature
 
   @Before
   fun setUp() {
     val context = ApplicationProvider.getApplicationContext<Context>()
     context.getSystemService(BluetoothManager::class.java).adapter.name = TEST_DEVICE_NAME
-    systemFeature = SystemFeature(context, mockStorage, fakeConnector)
+    systemFeature = SystemFeature(context, mockStorage, fakeConnector, onConnectionQueriedFeatures)
     assertThat(fakeConnector.callback).isNotNull()
   }
 
@@ -76,13 +84,26 @@
 
     argumentCaptor<ByteArray>() {
       verify(fakeConnector).sendQuerySecurely(eq(device), capture(), anyOrNull(), any())
-      val systemQuery =
-        SystemQuery.parseFrom(firstValue, ExtensionRegistryLite.getEmptyRegistry())
+      val systemQuery = SystemQuery.parseFrom(firstValue, ExtensionRegistryLite.getEmptyRegistry())
       assertThat(systemQuery.type).isEqualTo(DEVICE_NAME)
     }
   }
 
   @Test
+  fun onSecureChannelEstablished_sendsQueryForFeatureSupportStatus() {
+    runBlocking {
+      fakeConnector.callback?.onSecureChannelEstablished(device)
+      shadowOf(Looper.getMainLooper()).idle()
+
+      argumentCaptor<List<UUID>>() {
+        verify(fakeConnector).queryFeatureSupportStatuses(eq(device), capture())
+        val queriedFeatures = firstValue
+        assertThat(queriedFeatures).isEqualTo(onConnectionQueriedFeatures)
+      }
+    }
+  }
+
+  @Test
   fun deviceNameQueryResponse_successUpdatesDeviceName() {
     fakeConnector.callback?.onSecureChannelEstablished(device)
 
@@ -112,7 +133,7 @@
 
     argumentCaptor<Connector.QueryCallback>() {
       verify(fakeConnector).sendQuerySecurely(eq(device), any(), anyOrNull(), capture())
-      firstValue.onError(/* response= */ null)
+      firstValue.onError(ByteArray(0))
       verify(mockStorage, never()).updateAssociatedDeviceName(any(), any())
     }
   }
@@ -123,8 +144,8 @@
 
     argumentCaptor<Connector.QueryCallback>() {
       verify(fakeConnector).sendQuerySecurely(eq(device), any(), anyOrNull(), capture())
-      firstValue.onQueryFailedToSend(/* isTransient= */ false)
-      firstValue.onQueryFailedToSend(/* isTransient= */ true)
+      firstValue.onQueryFailedToSend(isTransient = false)
+      firstValue.onQueryFailedToSend(isTransient = true)
       verify(mockStorage, never()).updateAssociatedDeviceName(any(), any())
     }
   }
@@ -137,8 +158,7 @@
     fakeConnector.callback?.onQueryReceived(device, queryId, query.toByteArray(), null)
 
     argumentCaptor<ByteArray> {
-      verify(fakeConnector)
-        .respondToQuerySecurely(eq(device), eq(queryId), eq(true), capture())
+      verify(fakeConnector).respondToQuerySecurely(eq(device), eq(queryId), eq(true), capture())
       assertThat(firstValue).isEqualTo(TEST_DEVICE_NAME.toByteArray(StandardCharsets.UTF_8))
     }
   }
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
index 12791f4..30c4243 100644
--- 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
@@ -3,6 +3,9 @@
 import android.app.ActivityManager
 import android.app.KeyguardManager
 import android.os.PowerManager
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import android.os.Build;
 import android.content.Context
 import androidx.test.core.app.ApplicationProvider
 import com.google.android.connecteddevice.trust.api.ITrustedDeviceAgentDelegate
@@ -18,6 +21,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.Robolectric
+import org.robolectric.annotation.Config
 import org.robolectric.RobolectricTestRunner
 import org.robolectric.shadow.api.Shadow
 import org.robolectric.shadows.ShadowKeyguardManager
@@ -25,6 +29,7 @@
 
 
 @RunWith(RobolectricTestRunner::class)
+@Config(sdk=[Build.VERSION_CODES.S, Build.VERSION_CODES.R, Build.VERSION_CODES.Q])
 class TrustedDeviceAgentServiceTest {
 
   private val context = ApplicationProvider.getApplicationContext<Context>()
@@ -60,7 +65,7 @@
   }
 
   @Test
-  fun unlockUserWithToken_invokesCallbackAfterTokenReceived_DeviceAndUserAreLocked() {
+  fun unlockUserWithToken_invokesCallbackAfterTokenReceived_deviceAndUserAreLocked() {
     lockUser()
 
     sendToken()
@@ -70,7 +75,7 @@
   }
 
   @Test
-  fun unlockUserWithToken_invokesCallbackAfterTokenReceived_UserIsUnlocked() {
+  fun unlockUserWithTokenBeforeAndroidT_invokesCallbackAfterTokenReceived_userIsUnlocked() {
     service.isUserUnlocked = true
 
     sendToken()
@@ -78,6 +83,16 @@
     verify(mockTrustedDeviceManager).onUserUnlocked()
   }
 
+  @Config(sdk=[Build.VERSION_CODES.TIRAMISU])
+  @Test
+  fun unlockUserWithTokenAfterAndroidT_doNotInvokesCallbackAfterTokenReceivedImmediately() {
+    service.isUserUnlocked = true
+
+    sendToken()
+
+    verify(mockTrustedDeviceManager, never()).onUserUnlocked()
+  }
+
   @Test
   fun onUserUnlock_doesNotInvokeCallbackIfTokenWasNotUsed() {
     unlockUser()
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceViewModelTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceViewModelTest.java
index e129a4d..d728a03 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceViewModelTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceViewModelTest.java
@@ -26,6 +26,7 @@
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -44,6 +45,7 @@
   @Mock private ITrustedDeviceManager mockTrustedDeviceManager;
   private KeyguardManager keyguardManager;
   @Mock private Observer<EnrollmentState> mockEnrollmentStateObserver;
+  @Mock private Observer<List<TrustedDevice>> mockTrustedDevicesObserver;
 
   private TrustedDeviceViewModel viewModel;
 
@@ -79,6 +81,22 @@
   }
 
   @Test
+  public void enrollTrustedDevice_beforeProcessEscrowToken_setEmptyTrustedDevices()
+      throws RemoteException {
+    List<TrustedDevice> emptyList = new ArrayList<>();
+    ArgumentCaptor<IOnTrustedDevicesRetrievedListener> listenerCaptor =
+        ArgumentCaptor.forClass(IOnTrustedDevicesRetrievedListener.class);
+    viewModel.enrollTrustedDevice(createAssociatedDevice());
+    // This first call is when the view model is initialized.
+    verify(mockTrustedDeviceManager, times(2))
+        .retrieveTrustedDevicesForActiveUser(listenerCaptor.capture());
+    listenerCaptor.getAllValues().get(1).onTrustedDevicesRetrieved(emptyList);
+    viewModel.getTrustedDevices().observeForever(mockTrustedDevicesObserver);
+    waitForLiveDataUpdate();
+    verify(mockTrustedDevicesObserver).onChanged(emptyList);
+  }
+
+  @Test
   public void disableTrustedDevice() throws RemoteException {
     TrustedDevice testDevice = createTrustedDevice();
     viewModel.disableTrustedDevice(testDevice);
diff --git a/libs/encryptionrunner/android_build.gradle b/libs/encryptionrunner/android_build.gradle
new file mode 100644
index 0000000..bb84b61
--- /dev/null
+++ b/libs/encryptionrunner/android_build.gradle
@@ -0,0 +1,40 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion rootProject.ext.sdkVersion
+
+    defaultConfig {
+        minSdkVersion rootProject.ext.minSdkVersion
+        targetSdkVersion rootProject.ext.sdkVersion
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+    sourceSets {
+        main {
+            manifest.srcFile 'AndroidManifest.xml'
+            java.srcDirs = ['src']
+        }
+
+        androidTest {
+            java.srcDirs = ['tests/unit/src']
+        }
+    }
+}
+
+dependencies {
+    implementation files('ukey2.jar')
+
+    implementation libs.guava
+    implementation libs.androidx.annotation
+
+    androidTestImplementation libs.bundles.androidx.testing
+    androidTestImplementation libs.truth
+}