Merge "Revert "Fix the command to run CtsSeekerDiscoverProviderTest."" into tm-dev
diff --git a/nearby/framework/java/android/nearby/FastPairClient.java b/nearby/framework/java/android/nearby/FastPairClient.java
new file mode 100644
index 0000000..dc70d71
--- /dev/null
+++ b/nearby/framework/java/android/nearby/FastPairClient.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.BinderThread;
+import android.content.Context;
+import android.nearby.aidl.IFastPairClient;
+import android.nearby.aidl.IFastPairStatusCallback;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * 0p API for controlling Fast Pair. It communicates between main thread and service.
+ *
+ * @hide
+ */
+public class FastPairClient {
+
+    private static final String TAG = "FastPairClient";
+    private final IBinder mBinder;
+    private final WeakReference<Context> mWeakContext;
+    IFastPairClient mFastPairClient;
+    PairStatusCallbackIBinder mPairStatusCallbackIBinder;
+
+    /**
+     * The Ibinder instance should be from
+     * {@link com.android.server.nearby.fastpair.halfsheet.FastPairService} so that the client can
+     * talk with the service.
+     */
+    public FastPairClient(Context context, IBinder binder) {
+        mBinder = binder;
+        mFastPairClient = IFastPairClient.Stub.asInterface(mBinder);
+        mWeakContext = new WeakReference<>(context);
+    }
+
+    /**
+     * Registers a callback at service to get UI updates.
+     */
+    public void registerHalfSheet(FastPairStatusCallback fastPairStatusCallback) {
+        if (mPairStatusCallbackIBinder != null) {
+            return;
+        }
+        mPairStatusCallbackIBinder = new PairStatusCallbackIBinder(fastPairStatusCallback);
+        try {
+            mFastPairClient.registerHalfSheet(mPairStatusCallbackIBinder);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to register fastPairStatusCallback", e);
+        }
+    }
+
+    /**
+     * Pairs the device at service.
+     */
+    public void connect(FastPairDevice fastPairDevice) {
+        try {
+            mFastPairClient.connect(fastPairDevice);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to connect Fast Pair device" + fastPairDevice, e);
+        }
+    }
+
+    private class PairStatusCallbackIBinder extends IFastPairStatusCallback.Stub {
+        private final FastPairStatusCallback mStatusCallback;
+
+        private PairStatusCallbackIBinder(FastPairStatusCallback fastPairStatusCallback) {
+            mStatusCallback = fastPairStatusCallback;
+        }
+
+        @BinderThread
+        @Override
+        public synchronized void onPairUpdate(FastPairDevice fastPairDevice,
+                PairStatusMetadata pairStatusMetadata) {
+            Context context = mWeakContext.get();
+            if (context != null) {
+                Handler handler = new Handler(context.getMainLooper());
+                handler.post(() ->
+                        mStatusCallback.onPairUpdate(fastPairDevice, pairStatusMetadata));
+            }
+        }
+    }
+}
diff --git a/nearby/framework/java/android/nearby/INearbyManager.aidl b/nearby/framework/java/android/nearby/INearbyManager.aidl
index cd05106..4fff563 100644
--- a/nearby/framework/java/android/nearby/INearbyManager.aidl
+++ b/nearby/framework/java/android/nearby/INearbyManager.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2007, The Android Open Source Project
+ * Copyright (C) 2022, The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/nearby/framework/java/android/nearby/IScanListener.aidl b/nearby/framework/java/android/nearby/IScanListener.aidl
index 8747b07..54033aa 100644
--- a/nearby/framework/java/android/nearby/IScanListener.aidl
+++ b/nearby/framework/java/android/nearby/IScanListener.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2007, The Android Open Source Project
+ * Copyright (C) 2022, The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
index 3b0a776..e9a29b7 100644
--- a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
+++ b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
@@ -76,6 +76,8 @@
                     return new NearbyDeviceParcelable[size];
                 }
             };
+
+    @ScanRequest.ScanType int mScanType;
     @Nullable
     private final String mName;
     @NearbyDevice.Medium
@@ -89,9 +91,10 @@
     @Nullable
     private final byte[] mData;
 
-    private NearbyDeviceParcelable(@Nullable String name, int medium, int rssi,
-            @Nullable String fastPairModelId, @Nullable String bluetoothAddress,
-            @Nullable byte[] data) {
+    private NearbyDeviceParcelable(@ScanRequest.ScanType int scanType, @Nullable String name,
+            int medium, int rssi, @Nullable String fastPairModelId,
+            @Nullable String bluetoothAddress, @Nullable byte[] data) {
+        mScanType = scanType;
         mName = name;
         mMedium = medium;
         mRssi = rssi;
@@ -176,6 +179,16 @@
     }
 
     /**
+     * Returns the type of the scan.
+     *
+     * @hide
+     */
+    @ScanRequest.ScanType
+    public int getScanType() {
+        return mScanType;
+    }
+
+    /**
      * Gets the name of the NearbyDeviceParcelable. Returns {@code null} If there is no name.
      */
     @Nullable
@@ -235,6 +248,7 @@
         @NearbyDevice.Medium
         private int mMedium;
         private int mRssi;
+        @ScanRequest.ScanType int mScanType;
         @Nullable
         private String mFastPairModelId;
         @Nullable
@@ -243,6 +257,16 @@
         private byte[] mData;
 
         /**
+         * Sets the scan type of the NearbyDeviceParcelable.
+         *
+         * @hide
+         */
+        public Builder setScanType(@ScanRequest.ScanType int scanType) {
+            mScanType = scanType;
+            return this;
+        }
+
+        /**
          * Sets the name of the scanned device.
          *
          * @param name The local name of the scanned device.
@@ -314,7 +338,7 @@
          */
         @NonNull
         public NearbyDeviceParcelable build() {
-            return new NearbyDeviceParcelable(mName, mMedium, mRssi, mFastPairModelId,
+            return new NearbyDeviceParcelable(mScanType, mName, mMedium, mRssi, mFastPairModelId,
                     mBluetoothAddress, mData);
         }
     }
diff --git a/nearby/framework/java/android/nearby/ScanRequest.aidl b/nearby/framework/java/android/nearby/ScanRequest.aidl
index 0aaf3af..438dfed 100644
--- a/nearby/framework/java/android/nearby/ScanRequest.aidl
+++ b/nearby/framework/java/android/nearby/ScanRequest.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012, The Android Open Source Project
+ * Copyright (C) 2022, The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/nearby/halfsheet/Android.bp b/nearby/halfsheet/Android.bp
index 2b35254..486a3ff 100644
--- a/nearby/halfsheet/Android.bp
+++ b/nearby/halfsheet/Android.bp
@@ -28,7 +28,7 @@
     certificate: ":com.android.nearby.halfsheetcertificate",
     libs: [
         "framework-bluetooth",
-        "framework-connectivity-tiramisu",
+        "framework-connectivity-t",
         "nearby-service-string",
     ],
     static_libs: [
diff --git a/nearby/halfsheet/res/drawable/fast_pair_ic_info.xml b/nearby/halfsheet/res/drawable/fast_pair_ic_info.xml
index cc514e5..7d61d1c 100644
--- a/nearby/halfsheet/res/drawable/fast_pair_ic_info.xml
+++ b/nearby/halfsheet/res/drawable/fast_pair_ic_info.xml
@@ -17,7 +17,8 @@
     android:width="24dp"
     android:height="24dp"
     android:viewportWidth="24.0"
-    android:viewportHeight="24.0">
+    android:viewportHeight="24.0"
+    android:tint="@color/fast_pair_half_sheet_subtitle_color">
     <path
         android:fillColor="@color/fast_pair_half_sheet_subtitle_color"
         android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/>
diff --git a/nearby/halfsheet/res/layout/fast_pair_consent_fragment.xml b/nearby/halfsheet/res/layout/fast_pair_consent_fragment.xml
deleted file mode 100644
index aba9a32..0000000
--- a/nearby/halfsheet/res/layout/fast_pair_consent_fragment.xml
+++ /dev/null
@@ -1,77 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<android.support.v7.widget.LinearLayoutCompat
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:orientation="vertical"
-    android:layout_width="match_parent" android:layout_height="match_parent">
-
-  <android.support.constraint.ConstraintLayout
-      android:id="@+id/image_view"
-      android:layout_width="match_parent"
-      android:layout_height="340dp"
-      android:paddingStart="12dp"
-      android:paddingEnd="12dp"
-      android:paddingTop="12dp">
-    <TextView
-        android:id="@+id/header_subtitle"
-        android:textColor="@color/fast_pair_half_sheet_subtitle_color"
-        android:fontFamily="google-sans"
-        android:textSize="14sp"
-        android:gravity="center"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent" />
-
-    <ImageView
-        android:id="@+id/pairing_pic"
-        android:layout_width="@dimen/fast_pair_half_sheet_image_size"
-        android:layout_height="@dimen/fast_pair_half_sheet_image_size"
-        android:paddingTop="18dp"
-        android:paddingBottom="18dp"
-        android:importantForAccessibility="no"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toBottomOf="@+id/header_subtitle" />
-
-    <ProgressBar
-        android:id="@+id/connect_progressbar"
-        android:layout_width="@dimen/fast_pair_half_sheet_image_size"
-        android:layout_height="2dp"
-        android:indeterminate="true"
-        android:indeterminateTint="@color/fast_pair_progress_color"
-        android:indeterminateTintMode="src_in"
-        style="?android:attr/progressBarStyleHorizontal"
-        app:layout_constraintTop_toBottomOf="@+id/pairing_pic"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"/>
-
-    <Button
-        android:id="@+id/connect_btn"
-        android:text="@string/fast_pair_app_launch_button"
-        android:layout_height="@dimen/fast_pair_connect_button_height"
-        android:layout_width="@dimen/fast_pair_half_sheet_image_size"
-        android:background="@color/fast_pair_half_sheet_button_color"
-        android:paddingTop="6dp"
-        android:paddingBottom="6dp"
-        app:layout_constraintTop_toBottomOf="@+id/pairing_pic"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        style="@style/HalfSheetButton" />
-
-    <Button
-        android:id="@+id/result_action_btn"
-        android:text="@string/common_done"
-        android:layout_height="wrap_content"
-        android:layout_width="wrap_content"
-        android:paddingTop="6dp"
-        android:paddingBottom="6dp"
-        app:layout_constraintTop_toBottomOf="@+id/pairing_pic"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        style="@style/HalfSheetButtonBorderless" />
-
-  </android.support.constraint.ConstraintLayout>
-
-</android.support.v7.widget.LinearLayoutCompat>
diff --git a/nearby/halfsheet/res/layout/fast_pair_device_pairing_fragment.xml b/nearby/halfsheet/res/layout/fast_pair_device_pairing_fragment.xml
index 24fcd83..7fbe229 100644
--- a/nearby/halfsheet/res/layout/fast_pair_device_pairing_fragment.xml
+++ b/nearby/halfsheet/res/layout/fast_pair_device_pairing_fragment.xml
@@ -73,50 +73,67 @@
     <RelativeLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:orientation="horizontal"
         app:layout_constraintTop_toBottomOf="@+id/connect_progressbar"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent">
+
       <ImageView
           android:id="@+id/info_icon"
+          android:layout_width="24dp"
+          android:layout_height="24dp"
+          app:srcCompat="@drawable/fast_pair_ic_info"
           android:layout_centerInParent="true"
           android:contentDescription="@null"
           android:layout_marginEnd="10dp"
           android:layout_toStartOf="@id/connect_btn"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"/>
-      <Button
+          android:visibility="invisible" />
+
+      <com.google.android.material.button.MaterialButton
           android:id="@+id/connect_btn"
-          android:text="@string/common_connect"
-          android:layout_height="wrap_content"
           android:layout_width="@dimen/fast_pair_half_sheet_image_size"
+          android:layout_height="wrap_content"
+          android:text="@string/paring_action_connect"
           android:layout_centerInParent="true"
-          android:background="@color/fast_pair_half_sheet_button_color"
           style="@style/HalfSheetButton" />
+
     </RelativeLayout>
 
-    <Button
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/settings_btn"
+        android:text="@string/paring_action_settings"
+        android:layout_height="wrap_content"
+        android:layout_width="@dimen/fast_pair_half_sheet_image_size"
+        app:layout_constraintTop_toBottomOf="@+id/connect_progressbar"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        android:visibility="invisible"
+        style="@style/HalfSheetButton" />
+
+    <com.google.android.material.button.MaterialButton
         android:id="@+id/cancel_btn"
-        android:text="@string/common_done"
-        android:visibility="gone"
+        android:text="@string/paring_action_done"
+        android:visibility="invisible"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
         android:gravity="start|center_vertical"
         android:layout_marginTop="6dp"
-        android:layout_marginBottom="16dp"
         style="@style/HalfSheetButtonBorderless"/>
 
-    <Button
+    <com.google.android.material.button.MaterialButton
         android:id="@+id/setup_btn"
+        android:text="@string/paring_action_launch"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
         android:layout_marginTop="6dp"
         android:layout_marginBottom="16dp"
         android:background="@color/fast_pair_half_sheet_button_color"
-        android:visibility="gone"
+        android:visibility="invisible"
         android:layout_height="@dimen/fast_pair_half_sheet_bottom_button_height"
         android:layout_width="wrap_content"
         style="@style/HalfSheetButton" />
+
   </androidx.constraintlayout.widget.ConstraintLayout>
 
 </LinearLayout>
diff --git a/nearby/halfsheet/res/values/strings.xml b/nearby/halfsheet/res/values/strings.xml
index 12e3c23..01a82e4 100644
--- a/nearby/halfsheet/res/values/strings.xml
+++ b/nearby/halfsheet/res/values/strings.xml
@@ -16,9 +16,57 @@
 
 <resources>
 
-    <string name="common_done" description="After pairing process finish button text to dismiss halfsheet">Done</string>
-    <string name="common_save">Save</string>
-    <string name="common_connect" description="Button text to start connecting process">Connect</string>
-    <string name="fast_pair_app_launch_button" description="String on app launch half sheet button.">Set up</string>
+    <!--
+      ============================================================
+      PAIRING FRAGMENT
+      ============================================================
+    -->
 
+    <!--
+      A button shown to remind user setup is in progress. [CHAR LIMIT=30]
+    -->
+    <string name="fast_pair_setup_in_progress">Starting Setup&#x2026;</string>
+    <!--
+      Title text shown to remind user to setup a device through companion app. [CHAR LIMIT=40]
+    -->
+    <string name="fast_pair_title_setup">Set up device</string>
+    <!--
+      Title after we successfully pair with the audio device
+      [CHAR LIMIT=30]
+    -->
+    <string name="fast_pair_device_ready">Device connected</string>
+    <!-- Title text shown when peripheral device fail to connect to phone. [CHAR_LIMIT=30] -->
+    <string name="fast_pair_title_fail">Couldn\'t connect</string>
+
+    <!--
+      ============================================================
+      MISCELLANEOUS
+      ============================================================
+    -->
+
+    <!--
+      A button shown after paring process to dismiss the current activity.
+      [CHAR LIMIT=30]
+    -->
+    <string name="paring_action_done">Done</string>
+    <!--
+      A button shown for retroactive paring.
+      [CHAR LIMIT=30]
+     -->
+    <string name="paring_action_save">Save</string>
+    <!--
+      A button to start connecting process.
+      [CHAR LIMIT=30]
+     -->
+    <string name="paring_action_connect">Connect</string>
+    <!--
+      A button to launch a companion app.
+      [CHAR LIMIT=30]
+    -->
+    <string name="paring_action_launch">Set up</string>
+    <!--
+      A button to launch a bluetooth Settings page.
+      [CHAR LIMIT=20]
+    -->
+    <string name="paring_action_settings">Settings</string>
 </resources>
\ No newline at end of file
diff --git a/nearby/halfsheet/res/values/styles.xml b/nearby/halfsheet/res/values/styles.xml
index b48da70..917bb63 100644
--- a/nearby/halfsheet/res/values/styles.xml
+++ b/nearby/halfsheet/res/values/styles.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
-  <style name="HalfSheetStyle" parent="Theme.MaterialComponents.DayNight.NoActionBar">
+  <style name="HalfSheetStyle" parent="Theme.Material3.DayNight.NoActionBar">
     <item name="android:windowFrame">@null</item>
     <item name="android:windowBackground">@android:color/transparent</item>
     <item name="android:windowEnterAnimation">@anim/fast_pair_half_sheet_slide_in</item>
@@ -14,7 +14,7 @@
     <item name="android:windowTranslucentNavigation">true</item>
   </style>
 
-  <style name="HalfSheetButton" parent="@style/Widget.MaterialComponents.Button.TextButton">
+  <style name="HalfSheetButton" parent="@style/Widget.Material3.Button.TonalButton">
     <item name="android:textColor">@color/fast_pair_half_sheet_button_accent_text</item>
     <item name="android:backgroundTint">@color/fast_pair_half_sheet_button_color</item>
     <item name="android:textSize">@dimen/fast_pair_notification_text_size</item>
@@ -23,8 +23,7 @@
     <item name="android:textAllCaps">false</item>
   </style>
 
-  <style name="HalfSheetButtonBorderless"
-      parent="@style/Widget.MaterialComponents.Button.OutlinedButton">
+  <style name="HalfSheetButtonBorderless" parent="@style/Widget.Material3.Button.OutlinedButton">
     <item name="android:textColor">@color/fast_pair_half_sheet_button_text</item>
     <item name="android:strokeColor">@color/fast_pair_half_sheet_button_color</item>
     <item name="android:textAllCaps">false</item>
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java
index c495c35..9507b9b 100644
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java
@@ -19,22 +19,18 @@
 import static com.android.nearby.halfsheet.fragment.DevicePairingFragment.APP_LAUNCH_FRAGMENT_TYPE;
 import static com.android.server.nearby.common.bluetooth.fastpair.FastPairConstants.EXTRA_MODEL_ID;
 import static com.android.server.nearby.common.fastpair.service.UserActionHandlerBase.EXTRA_MAC_ADDRESS;
-import static com.android.server.nearby.fastpair.Constant.ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET;
 import static com.android.server.nearby.fastpair.Constant.ACTION_FAST_PAIR_HALF_SHEET_CANCEL;
 import static com.android.server.nearby.fastpair.Constant.DEVICE_PAIRING_FRAGMENT_TYPE;
 import static com.android.server.nearby.fastpair.Constant.EXTRA_HALF_SHEET_INFO;
 import static com.android.server.nearby.fastpair.Constant.EXTRA_HALF_SHEET_TYPE;
 
-import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
-import android.util.DisplayMetrics;
 import android.util.Log;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
 import androidx.fragment.app.FragmentActivity;
 
 import com.android.nearby.halfsheet.fragment.DevicePairingFragment;
@@ -48,14 +44,12 @@
 import service.proto.Cache;
 
 /**
- * Half sheet activity to show pairing ux.
+ * A class show Fast Pair related information in Half sheet format.
  */
 public class HalfSheetActivity extends FragmentActivity {
 
-    public static final String EXTRA_HALF_SHEET_PENDING_INTENT_CALL_BACK =
-            "com.android.nearby.halfsheet.EXTRA_HALF_SHEET_PENDING_INTENT_CALL_BACK";
-    public static final String EXTRA_HALF_SHEET_PACKAGE_NAME =
-            "com.android.nearby.halfsheet.EXTRA_HALF_SHEET_PACKAGE_NAME";
+    public static final String TAG = "HalfSheetActivity";
+
     public static final String EXTRA_HALF_SHEET_CONTENT =
             "com.android.nearby.halfsheet.HALF_SHEET_CONTENT";
     public static final String EXTRA_TITLE =
@@ -72,38 +66,11 @@
             "com.android.nearby.halfsheet.EXTRA_HALF_SHEET_PAIRING_RESURFACE";
     public static final String ACTION_HALF_SHEET_FOREGROUND_STATE =
             "com.android.nearby.halfsheet.ACTION_HALF_SHEET_FOREGROUND_STATE";
-    public static final String ACTION_HALF_SHEET_BAN_ALL_ITEM =
-            "com.android.nearby.halfsheet.ACTION_HALF_SHEET_BAN_ALL_ITEM";
-    public static final String ACTION_HALF_SHEET_APP_LAUNCH_CLICKED =
-            "com.android.nearby.halfsheet.ACTION_HALF_SHEET_APP_LAUNCH_CLICKED";
-    public static final String ACTION_HALF_SHEET_WEAR_OS_CLICKED =
-            "com.android.nearby.halfsheet.ACTION_HALF_SHEET_WEAR_OS_CLICKED";
-    public static final String ACTION_HALF_SHEET_USER_COMPLETE_CONFIRMATION =
-            "com.android.nearby.halfsheet.ACTION_HALF_SHEET_USER_COMPLETE_CONFIRMATION";
-    public static final String ACTION_FAST_PAIR_HANDLE_CHIP_DEVICE =
-            "com.android.nearby.halfsheet.ACTION_FAST_PAIR_HANDLE_CHIP_DEVICE";
-    // Intent extra contains another intent that will trigger DiscoveryChimeraService to upload
-    // device
-    // information to the cloud.
-    public static final String EXTRA_HALF_SHEET_CLOUD_SYNC_INTENT =
-            "com.android.nearby.halfsheet.HALF_SHEET_CLOUD_SYNC_INTENT";
     // Intent extra contains the user gmail name eg. testaccount@gmail.com.
     public static final String EXTRA_HALF_SHEET_ACCOUNT_NAME =
             "com.android.nearby.halfsheet.HALF_SHEET_ACCOUNT_NAME";
     public static final String EXTRA_HALF_SHEET_FOREGROUND =
             "com.android.nearby.halfsheet.EXTRA_HALF_SHEET_FOREGROUND";
-    public static final String EXTRA_USER_CONSENT_SYNC_CONTACTS =
-            "com.android.nearby.halfsheet.EXTRA_USER_CONSENT_SYNC_CONTACTS";
-    public static final String EXTRA_USER_CONSENT_SYNC_SMS =
-            "com.android.nearby.halfsheet.EXTRA_USER_CONSENT_SYNC_SMS";
-    public static final String EXTRA_USER_CONFIRM_PASSKEY =
-            "com.android.nearby.halfsheet.EXTRA_USER_CONFIRM_PASSKEY";
-    public static final String CLASS_NAME =
-            "com.android.nearby.halfsheet.HalfSheetActivity";
-    public static final String ACTION_HALF_SHEET_STATUS_CHANGE =
-            "com.android.nearby.halfsheet.ACTION_HALF_SHEET_STATUS_CHANGE";
-    public static final String FINISHED_STATE = "FINISHED_STATE";
-    public static final String EXTRA_CLASSIC_MAC_ADDRESS = "EXTRA_CLASSIC_MAC_ADDRESS";
     public static final String ARG_FRAGMENT_STATE = "ARG_FRAGMENT_STATE";
     @Nullable
     private HalfSheetModuleFragment mHalfSheetModuleFragment;
@@ -128,7 +95,7 @@
                 mHalfSheetModuleFragment = DevicePairingFragment.newInstance(getIntent(),
                         savedInstanceState);
                 if (mHalfSheetModuleFragment == null) {
-                    Log.d("HalfSheetActivity", "device pairing fragment has error.");
+                    Log.d(TAG, "device pairing fragment has error.");
                     finish();
                     return;
                 }
@@ -136,13 +103,13 @@
             case APP_LAUNCH_FRAGMENT_TYPE:
                 // currentFragment = AppLaunchFragment.newInstance(getIntent());
                 if (mHalfSheetModuleFragment == null) {
-                    Log.v("HalfSheetActivity", "app launch fragment has error.");
+                    Log.v(TAG, "app launch fragment has error.");
                     finish();
                     return;
                 }
                 break;
             default:
-                Log.w("HalfSheetActivity", "there is no valid type for half sheet");
+                Log.w(TAG, "there is no valid type for half sheet");
                 finish();
                 return;
         }
@@ -159,23 +126,19 @@
         findViewById(R.id.background).setOnClickListener(v -> onCancelClicked());
         findViewById(R.id.card)
                 .setOnClickListener(
-                        v -> Log.v("HalfSheetActivity", "card view is clicked noop"));
+                        v -> Log.v(TAG, "card view is clicked noop"));
         try {
             mScanFastPairStoreItem =
                     Cache.ScanFastPairStoreItem.parseFrom(infoArray);
         } catch (InvalidProtocolBufferException e) {
             Log.w(
-                    "HalfSheetActivity", "error happens when pass info to half sheet");
+                    TAG, "error happens when pass info to half sheet");
         }
     }
 
     @Override
     protected void onStart() {
         super.onStart();
-        BroadcastUtils.sendBroadcast(
-                this,
-                new Intent(ACTION_HALF_SHEET_FOREGROUND_STATE)
-                        .putExtra(EXTRA_HALF_SHEET_FOREGROUND, true));
     }
 
     @Override
@@ -217,17 +180,18 @@
                         mScanFastPairStoreItem.getAddress())
                         && testScanFastPairStoreItem.getModelId().equals(
                         mScanFastPairStoreItem.getModelId())) {
-                    Log.d("HalfSheetActivity", "possible factory reset happens");
+                    Log.d(TAG, "possible factory reset happens");
                     halfSheetStateChange();
                 }
             } catch (InvalidProtocolBufferException | NullPointerException e) {
-                Log.w("HalfSheetActivity", "error happens when pass info to half sheet");
+                Log.w(TAG, "error happens when pass info to half sheet");
             }
         }
     }
 
     /** This function should be called when user click empty area and cancel button. */
     public void onCancelClicked() {
+        Log.d(TAG, "Cancels the half sheet and paring.");
         sendHalfSheetCancelBroadcast();
         finish();
     }
@@ -241,20 +205,6 @@
         finish();
     }
 
-    /**
-     * Change the half sheet ban state to active sometimes users leave half sheet to go to fast pair
-     * info page we do not want the behavior to be counted as dismiss.
-     */
-    public void sendBanStateResetBroadcast() {
-        if (mScanFastPairStoreItem != null) {
-            BroadcastUtils.sendBroadcast(
-                    this,
-                    new Intent(ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET)
-                            .putExtra(EXTRA_MODEL_ID,
-                                    mScanFastPairStoreItem.getModelId().toLowerCase(Locale.ROOT)));
-        }
-    }
-
     private void sendHalfSheetCancelBroadcast() {
         BroadcastUtils.sendBroadcast(
                 this,
@@ -280,31 +230,10 @@
         }
     }
 
-    @Nullable
-    @VisibleForTesting
-    public HalfSheetModuleFragment getFragmentModel() {
-        return mHalfSheetModuleFragment;
-    }
-
     @Override
     public void setTitle(CharSequence title) {
         super.setTitle(title);
         TextView toolbarTitle = findViewById(R.id.toolbar_title);
         toolbarTitle.setText(title);
     }
-
-
-    /**
-     * This method converts dp unit to equivalent pixels, depending on device density.
-     *
-     * @param dp      A value in dp (density independent pixels) unit, which we need to convert into
-     *                pixels
-     * @param context Context to get resources and device specific display metrics
-     * @return A float value to represent px equivalent to dp depending on device density
-     */
-    private float convertDpToPixel(float dp, Context context) {
-        return dp
-                * ((float) context.getResources().getDisplayMetrics().densityDpi
-                / DisplayMetrics.DENSITY_DEFAULT);
-    }
 }
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/DevicePairingFragment.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/DevicePairingFragment.java
index c99c130..a62c8cc 100644
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/DevicePairingFragment.java
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/DevicePairingFragment.java
@@ -17,44 +17,35 @@
 
 import static android.text.TextUtils.isEmpty;
 
-import static com.android.nearby.halfsheet.HalfSheetActivity.ACTION_HALF_SHEET_STATUS_CHANGE;
 import static com.android.nearby.halfsheet.HalfSheetActivity.ARG_FRAGMENT_STATE;
-import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_CLASSIC_MAC_ADDRESS;
 import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_DESCRIPTION;
 import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_HALF_SHEET_ACCOUNT_NAME;
 import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_HALF_SHEET_CONTENT;
 import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_HALF_SHEET_ID;
-import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_HALF_SHEET_IS_RETROACTIVE;
-import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR;
-import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_HALF_SHEET_PAIRING_RESURFACE;
 import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_TITLE;
-import static com.android.nearby.halfsheet.HalfSheetActivity.FINISHED_STATE;
+import static com.android.nearby.halfsheet.HalfSheetActivity.TAG;
+import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.FAILED;
+import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.FOUND_DEVICE;
 import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.NOT_STARTED;
-import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.RESULT_FAILURE;
-import static com.android.server.nearby.common.bluetooth.fastpair.FastPairConstants.EXTRA_MODEL_ID;
-import static com.android.server.nearby.fastpair.Constant.DISMISS;
+import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.PAIRED_LAUNCHABLE;
+import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.PAIRED_UNLAUNCHABLE;
+import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.PAIRING;
 import static com.android.server.nearby.fastpair.Constant.EXTRA_BINDER;
 import static com.android.server.nearby.fastpair.Constant.EXTRA_BUNDLE;
 import static com.android.server.nearby.fastpair.Constant.EXTRA_HALF_SHEET_INFO;
-import static com.android.server.nearby.fastpair.Constant.FAIL_STATE;
-import static com.android.server.nearby.fastpair.Constant.SUCCESS_STATE;
-import static com.android.server.nearby.fastpair.UserActionHandler.ACTION_FAST_PAIR;
-import static com.android.server.nearby.fastpair.UserActionHandler.EXTRA_PRIVATE_BLE_ADDRESS;
 
-import android.animation.AnimatorSet;
-import android.app.Activity;
-import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.res.Configuration;
-import android.nearby.IFastPairHalfSheetCallback;
+import android.graphics.Bitmap;
+import android.nearby.FastPairClient;
+import android.nearby.FastPairDevice;
+import android.nearby.FastPairStatusCallback;
+import android.nearby.NearbyDevice;
+import android.nearby.PairStatusMetadata;
 import android.os.Bundle;
-import android.os.RemoteException;
 import android.provider.Settings;
+import android.text.TextUtils;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -69,12 +60,13 @@
 
 import com.android.nearby.halfsheet.HalfSheetActivity;
 import com.android.nearby.halfsheet.R;
-import com.android.nearby.halfsheet.utils.BroadcastUtils;
 import com.android.nearby.halfsheet.utils.FastPairUtils;
-import com.android.server.nearby.fastpair.UserActionHandler;
+import com.android.nearby.halfsheet.utils.IconUtils;
 
 import com.google.protobuf.InvalidProtocolBufferException;
 
+import java.util.Objects;
+
 import service.proto.Cache.ScanFastPairStoreItem;
 
 /**
@@ -83,36 +75,38 @@
  * <p>This fragment will handle initial pairing subsequent pairing and retroactive pairing.
  */
 @SuppressWarnings("nullness")
-public class DevicePairingFragment extends HalfSheetModuleFragment {
+public class DevicePairingFragment extends HalfSheetModuleFragment implements
+        FastPairStatusCallback {
+    private TextView mTitleView;
+    private TextView mSubTitleView;
+    private ImageView mImage;
+
     private Button mConnectButton;
     private Button mSetupButton;
     private Button mCancelButton;
+    // Opens Bluetooth Settings.
+    private Button mSettingsButton;
     private ImageView mInfoIconButton;
     private ProgressBar mConnectProgressBar;
-    private View mRootView;
-    private TextView mSubTitle;
-    private TextView mTitle;
-    private ImageView mImage;
-    private ScanFastPairStoreItem mScanFastPairStoreItem;
-    // This open companion app intent will be triggered after user finish Fast Pair.
-    private Intent mOpenCompanionAppIntent;
-    // Indicates that the setup button is clicked before.
-    private boolean mSetupButtonClicked = false;
-    private boolean mIsSubsequentPair = false;
-    private boolean mIsPairingResurface = false;
-    private String mBluetoothMacAddress = "";
-    private HalfSheetFragmentState mFragmentState = NOT_STARTED;
-    private AnimatorSet mAnimatorSet = new AnimatorSet();
-    // True means pairing was successful and false means failed.
-    private Boolean mPairingResult = false;
+
     private Bundle mBundle;
 
-    public static final String APP_LAUNCH_FRAGMENT_TYPE = "APP_LAUNCH";
-    public static final String FAST_PAIR_CONSENT_FRAGMENT_TYPE = "FAST_PAIR_CONSENT";
-    private static final String ARG_SETUP_BUTTON_CLICKED = "SETUP_BUTTON_CLICKED";
-    public static final String RESULT_FAIL = "RESULT_FAIL";
-    private static final String ARG_PAIRING_RESULT = "PAIRING_RESULT";
+    private ScanFastPairStoreItem mScanFastPairStoreItem;
+    private FastPairClient mFastPairClient;
 
+    private @PairStatusMetadata.Status int mPairStatus = PairStatusMetadata.Status.UNKNOWN;
+    // True when there is a companion app to open.
+    private boolean mIsLaunchable;
+    private boolean mIsConnecting;
+    // Indicates that the setup button is clicked before.
+    private boolean mSetupButtonClicked = false;
+
+    // Holds the new text while we transition between the two.
+    private static final int TAG_PENDING_TEXT = R.id.toolbar_title;
+    public static final String APP_LAUNCH_FRAGMENT_TYPE = "APP_LAUNCH";
+
+    private static final String ARG_SETUP_BUTTON_CLICKED = "SETUP_BUTTON_CLICKED";
+    private static final String ARG_PAIRING_RESULT = "PAIRING_RESULT";
 
     /**
      * Create certain fragment according to the intent.
@@ -122,23 +116,15 @@
             Intent intent, @Nullable Bundle saveInstanceStates) {
         Bundle args = new Bundle();
         byte[] infoArray = intent.getByteArrayExtra(EXTRA_HALF_SHEET_INFO);
-        boolean isRetroactive = intent.getBooleanExtra(EXTRA_HALF_SHEET_IS_RETROACTIVE, false);
-        boolean isSubsequentPair = intent.getBooleanExtra(EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR,
-                false);
-        boolean isPairingResurface = intent.getBooleanExtra(EXTRA_HALF_SHEET_PAIRING_RESURFACE,
-                false);
+
         Bundle bundle = intent.getBundleExtra(EXTRA_BUNDLE);
         String title = intent.getStringExtra(EXTRA_TITLE);
         String description = intent.getStringExtra(EXTRA_DESCRIPTION);
         String accountName = intent.getStringExtra(EXTRA_HALF_SHEET_ACCOUNT_NAME);
         String result = intent.getStringExtra(EXTRA_HALF_SHEET_CONTENT);
-        String publicAddress = intent.getStringExtra(EXTRA_CLASSIC_MAC_ADDRESS);
         int halfSheetId = intent.getIntExtra(EXTRA_HALF_SHEET_ID, 0);
 
         args.putByteArray(EXTRA_HALF_SHEET_INFO, infoArray);
-        args.putBoolean(EXTRA_HALF_SHEET_IS_RETROACTIVE, isRetroactive);
-        args.putBoolean(EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR, isSubsequentPair);
-        args.putBoolean(EXTRA_HALF_SHEET_PAIRING_RESURFACE, isPairingResurface);
         args.putString(EXTRA_HALF_SHEET_ACCOUNT_NAME, accountName);
         args.putString(EXTRA_TITLE, title);
         args.putString(EXTRA_DESCRIPTION, description);
@@ -180,21 +166,22 @@
     public View onCreateView(
             LayoutInflater inflater, @Nullable ViewGroup container,
             @Nullable Bundle savedInstanceState) {
-        mRootView =
-                inflater.inflate(
-                        R.layout.fast_pair_device_pairing_fragment, container, /* attachToRoot= */
-                        false);
+        /* attachToRoot= */
+        View rootView = inflater.inflate(
+                R.layout.fast_pair_device_pairing_fragment, container, /* attachToRoot= */
+                false);
         if (getContext() == null) {
-            Log.d("DevicePairingFragment", "can't find the attached activity");
-            return mRootView;
+            Log.d(TAG, "can't find the attached activity");
+            return rootView;
         }
+
         Bundle args = getArguments();
         byte[] storeFastPairItemBytesArray = args.getByteArray(EXTRA_HALF_SHEET_INFO);
-        boolean isRetroactive = args.getBoolean(EXTRA_HALF_SHEET_IS_RETROACTIVE);
-        mIsSubsequentPair = args.getBoolean(EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR);
-        mIsPairingResurface = args.getBoolean(EXTRA_HALF_SHEET_PAIRING_RESURFACE);
-        String accountName = args.getString(EXTRA_HALF_SHEET_ACCOUNT_NAME);
         mBundle = args.getBundle(EXTRA_BUNDLE);
+        if (mBundle != null) {
+            mFastPairClient = new FastPairClient(getContext(), mBundle.getBinder(EXTRA_BINDER));
+            mFastPairClient.registerHalfSheet(this);
+        }
         if (args.containsKey(ARG_FRAGMENT_STATE)) {
             mFragmentState = (HalfSheetFragmentState) args.getSerializable(ARG_FRAGMENT_STATE);
         }
@@ -202,86 +189,53 @@
             mSetupButtonClicked = args.getBoolean(ARG_SETUP_BUTTON_CLICKED);
         }
         if (args.containsKey(ARG_PAIRING_RESULT)) {
-            mPairingResult = args.getBoolean(ARG_PAIRING_RESULT);
-        } else {
-            mPairingResult = false;
+            mPairStatus = args.getInt(ARG_PAIRING_RESULT);
         }
 
-        // title = ((FragmentActivity) getContext()).findViewById(R.id.toolbar_title);
-        mConnectButton = mRootView.findViewById(R.id.connect_btn);
-        mImage = mRootView.findViewById(R.id.pairing_pic);
+        // Initiate views.
+        mTitleView = Objects.requireNonNull(getActivity()).findViewById(R.id.toolbar_title);
+        mSubTitleView = rootView.findViewById(R.id.header_subtitle);
+        mImage = rootView.findViewById(R.id.pairing_pic);
+        mConnectProgressBar = rootView.findViewById(R.id.connect_progressbar);
+        mConnectButton = rootView.findViewById(R.id.connect_btn);
+        mCancelButton = rootView.findViewById(R.id.cancel_btn);
+        mSettingsButton = rootView.findViewById(R.id.settings_btn);
+        mSetupButton = rootView.findViewById(R.id.setup_btn);
+        mInfoIconButton = rootView.findViewById(R.id.info_icon);
+        mInfoIconButton.setImageResource(R.drawable.fast_pair_ic_info);
 
-        mConnectProgressBar = mRootView.findViewById(R.id.connect_progressbar);
-        mConnectProgressBar.setVisibility(View.INVISIBLE);
+        try {
+            setScanFastPairStoreItem(ScanFastPairStoreItem.parseFrom(storeFastPairItemBytesArray));
+        } catch (InvalidProtocolBufferException e) {
+            Log.w(TAG,
+                    "DevicePairingFragment: error happens when pass info to half sheet");
+            return rootView;
+        }
 
+        // Config for landscape mode
         DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
         if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
-            mRootView.getLayoutParams().height = displayMetrics.heightPixels * 4 / 5;
-            mRootView.getLayoutParams().width = displayMetrics.heightPixels * 4 / 5;
+            rootView.getLayoutParams().height = displayMetrics.heightPixels * 4 / 5;
+            rootView.getLayoutParams().width = displayMetrics.heightPixels * 4 / 5;
             mImage.getLayoutParams().height = displayMetrics.heightPixels / 2;
             mImage.getLayoutParams().width = displayMetrics.heightPixels / 2;
             mConnectProgressBar.getLayoutParams().width = displayMetrics.heightPixels / 2;
             mConnectButton.getLayoutParams().width = displayMetrics.heightPixels / 2;
+            //TODO(b/213373051): Add cancel button
         }
 
-        mCancelButton = mRootView.findViewById(R.id.cancel_btn);
-        mSetupButton = mRootView.findViewById(R.id.setup_btn);
-        mInfoIconButton = mRootView.findViewById(R.id.info_icon);
-        mSubTitle = mRootView.findViewById(R.id.header_subtitle);
-        mSetupButton.setVisibility(View.GONE);
-        mInfoIconButton.setVisibility(View.GONE);
-
-        try {
-            if (storeFastPairItemBytesArray != null) {
-                mScanFastPairStoreItem =
-                        ScanFastPairStoreItem.parseFrom(storeFastPairItemBytesArray);
-            }
-
-            // If the fragmentState is not NOT_STARTED, it is because the fragment was just
-            // resumed from
-            // configuration change (e.g. rotating the screen or half-sheet resurface). Let's
-            // recover the
-            // UI directly.
-            if (mFragmentState != NOT_STARTED) {
-                switch (mFragmentState) {
-                    case PAIRING:
-                        Log.d("DevicePairingFragment", "redraw for PAIRING state.");
-                        return mRootView;
-                    case RESULT_SUCCESS:
-                        Log.d("DevicePairingFragment", "redraw for RESULT_SUCCESS state.");
-                        return mRootView;
-                    case RESULT_FAILURE:
-                        Log.d("DevicePairingFragment", "redraw for RESULT_FAILURE state.");
-                        return mRootView;
-                    default:
-                        // fall-out
-                        Log.d("DevicePairingFragment",
-                                "DevicePairingFragment: not supported state");
-                }
-            }
-            if (mIsPairingResurface) {
-                // Since the Settings contextual card has sent the pairing intent, we don't send the
-                // pairing intent here.
-                onConnectClick(/* sendPairingIntent= */ false);
-            } else {
-                mSubTitle.setText(this.getArguments().getString(EXTRA_DESCRIPTION));
-                mSubTitle.setText("");
-                mConnectButton.setOnClickListener(
-                        v -> onConnectClick(/* sendPairingIntent= */ true));
-                // Pairing fail half sheet resurface
-                if (this.getArguments().getString(EXTRA_HALF_SHEET_CONTENT).equals(RESULT_FAIL)) {
-                    mFragmentState = RESULT_FAILURE;
-                    showFailInfo();
-                } else {
-                    mConnectButton.setOnClickListener(
-                            v -> onConnectClick(/* sendPairingIntent= */ true));
-                }
-            }
-        } catch (InvalidProtocolBufferException e) {
-            Log.w("DevicePairingFragment",
-                    "DevicePairingFragment: error happens when pass info to half sheet");
+        Bitmap icon = IconUtils.getIcon(mScanFastPairStoreItem.getIconPng().toByteArray(),
+                mScanFastPairStoreItem.getIconPng().size());
+        if (icon != null) {
+            mImage.setImageBitmap(icon);
         }
-        return mRootView;
+        mConnectButton.setOnClickListener(v -> onConnectClick());
+        mCancelButton.setOnClickListener(v ->
+                ((HalfSheetActivity) getActivity()).onCancelClicked());
+        mSettingsButton.setOnClickListener(v -> onSettingsClicked());
+        mSetupButton.setOnClickListener(v -> onSetupClick());
+
+        return rootView;
     }
 
     @Override
@@ -294,19 +248,8 @@
     @Override
     public void onStart() {
         super.onStart();
-        if (getContext() != null) {
-            IntentFilter intentFilter = new IntentFilter();
-            intentFilter.addAction(ACTION_HALF_SHEET_STATUS_CHANGE);
-            BroadcastUtils.registerReceiver(getContext(), mHalfSheetChangeReceiver, intentFilter);
-        }
-    }
-
-    @Override
-    public void onStop() {
-        super.onStop();
-        if (getContext() != null) {
-            BroadcastUtils.unregisterReceiver(getContext(), mHalfSheetChangeReceiver);
-        }
+        Log.v(TAG, "onStart: invalidate states");
+        invalidateState();
     }
 
     @Override
@@ -315,118 +258,229 @@
 
         savedInstanceState.putSerializable(ARG_FRAGMENT_STATE, mFragmentState);
         savedInstanceState.putBoolean(ARG_SETUP_BUTTON_CLICKED, mSetupButtonClicked);
-        savedInstanceState.putBoolean(ARG_PAIRING_RESULT, mPairingResult);
-
-
+        savedInstanceState.putInt(ARG_PAIRING_RESULT, mPairStatus);
     }
 
-    @Nullable
-    private Intent createCompletionIntent(@Nullable String companionApp, @Nullable String address) {
-        if (isEmpty(companionApp)) {
-            return null;
-        } else if (FastPairUtils.isAppInstalled(companionApp, getContext())
-                && isLaunchable(companionApp)) {
-            mOpenCompanionAppIntent = createCompanionAppIntent(companionApp, address);
-            return mOpenCompanionAppIntent;
-        } else {
-            return null;
-        }
-    }
-
-    @Nullable
-    private Intent createCompanionAppIntent(String packageName, @Nullable String address) {
-        return createCompanionAppIntent(getContext(), packageName, address);
-    }
-
-    @Nullable
-    private static Intent createCompanionAppIntent(
-            Context context, String packageName, @Nullable String address) {
-        Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName);
-        BluetoothManager manager = context.getSystemService(BluetoothManager.class);
-        if (address != null && manager != null) {
-            BluetoothAdapter adapter = manager.getAdapter();
-            if (intent != null && adapter != null) {
-                intent.putExtra(BluetoothDevice.EXTRA_DEVICE, adapter.getRemoteDevice(address));
-            }
-        }
-        return intent;
-    }
-
-    private void onConnectClick(boolean sendPairingIntent) {
-        if (mScanFastPairStoreItem == null) {
-            Log.w("DevicePairingFragment", "No pairing related information in half sheet");
-            return;
-        }
-
-        Log.d("FastPairHalfSheet", "on connect click");
-        // Allow user to setup device before connection setup.
-        // showPairingLastPhase();
-        ((Activity) getContext())
-                .findViewById(R.id.background)
-                .setOnClickListener(
-                        v ->
-                                Log.d("DevicePairingFragment",
-                                        "DevicePairingFragment: tap empty area do not dismiss "
-                                                + "half sheet when pairing."));
-        if (sendPairingIntent) {
-            try {
-                Log.d("FastPairHalfSheet", "on connect click");
-                Intent intent =
-                        new Intent(ACTION_FAST_PAIR)
-                                // Using the DiscoveryChimeraService notification id for
-                                // backwards compat
-                                .putExtra(
-                                        UserActionHandler.EXTRA_DISCOVERY_ITEM,
-                                        FastPairUtils.convertFrom(
-                                                mScanFastPairStoreItem).toByteArray())
-                                .putExtra(EXTRA_MODEL_ID, mScanFastPairStoreItem.getModelId())
-                                .putExtra(EXTRA_PRIVATE_BLE_ADDRESS,
-                                        mScanFastPairStoreItem.getAddress());
-                IFastPairHalfSheetCallback.Stub.asInterface(mBundle.getBinder(EXTRA_BINDER))
-                        .onHalfSheetConnectionConfirm(intent);
-            } catch (RemoteException e) {
-                Log.d("FastPairHalfSheet", "invoke callback fall");
-            }
-        }
-    }
-
-    private void onFailConnectClick() {
+    private void onSettingsClicked() {
         startActivity(new Intent(Settings.ACTION_BLUETOOTH_SETTINGS));
     }
 
-    private final BroadcastReceiver mHalfSheetChangeReceiver =
-            new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    if (!ACTION_HALF_SHEET_STATUS_CHANGE.equals(intent.getAction())) {
-                        return;
-                    }
-                    if (SUCCESS_STATE.equals(intent.getStringExtra(FINISHED_STATE))) {
-                        mBluetoothMacAddress = intent.getStringExtra(EXTRA_CLASSIC_MAC_ADDRESS);
-                        showSuccessInfo();
-                        if (mOpenCompanionAppIntent != null) {
-                            //((HalfSheetActivity) getContext()).halfSheetStateChange();
-                            String companionApp =
-                                    FastPairUtils.getCompanionAppFromActionUrl(
-                                            mScanFastPairStoreItem.getActionUrl());
-                            // Redirect user to companion app if user choose to setup the app.
-                            // Recreate the intent
-                            // since the correct mac address just populated.
-                            startActivity(
-                                    createCompletionIntent(companionApp, mBluetoothMacAddress));
-                        }
-                    } else if (FAIL_STATE.equals(intent.getStringExtra(FINISHED_STATE))) {
-                        showFailInfo();
-                    } else if (DISMISS.equals(intent.getStringExtra(FINISHED_STATE))) {
-                        if (getContext() != null) {
-                            HalfSheetActivity activity = (HalfSheetActivity) getContext();
-                            activity.finish();
-                        }
-                    }
-                }
-            };
+    private void onSetupClick() {
+        String companionApp =
+                FastPairUtils.getCompanionAppFromActionUrl(mScanFastPairStoreItem.getActionUrl());
+        Intent intent =
+                FastPairUtils.createCompanionAppIntent(
+                        Objects.requireNonNull(getContext()),
+                        companionApp,
+                        mScanFastPairStoreItem.getAddress());
+        mSetupButtonClicked = true;
+        if (mFragmentState == PAIRED_LAUNCHABLE) {
+            if (intent != null) {
+                startActivity(intent);
+            }
+        } else {
+            Log.d(TAG, "onSetupClick: State is " + mFragmentState);
+        }
+    }
 
-    private boolean isLaunchable(String companionApp) {
-        return createCompanionAppIntent(companionApp, null) != null;
+    private void onConnectClick() {
+        if (mScanFastPairStoreItem == null) {
+            Log.w(TAG, "No pairing related information in half sheet");
+            return;
+        }
+        if (getFragmentState() == PAIRING) {
+            return;
+        }
+        mIsConnecting = true;
+        invalidateState();
+        mFastPairClient.connect(
+                new FastPairDevice.Builder()
+                        .addMedium(NearbyDevice.Medium.BLE)
+                        .setBluetoothAddress(mScanFastPairStoreItem.getAddress())
+                        .setData(FastPairUtils.convertFrom(mScanFastPairStoreItem)
+                                .toByteArray())
+                        .build());
+    }
+
+    // Receives callback from service.
+    @Override
+    public void onPairUpdate(FastPairDevice fastPairDevice, PairStatusMetadata pairStatusMetadata) {
+        @PairStatusMetadata.Status int status = pairStatusMetadata.getStatus();
+        if (status == PairStatusMetadata.Status.DISMISS && getActivity() != null) {
+            getActivity().finish();
+        }
+        mIsConnecting = false;
+        mPairStatus = status;
+        invalidateState();
+    }
+
+    @Override
+    public void invalidateState() {
+        HalfSheetFragmentState newState = NOT_STARTED;
+        if (mIsConnecting) {
+            newState = PAIRING;
+        } else {
+            switch (mPairStatus) {
+                case PairStatusMetadata.Status.SUCCESS:
+                    newState = mIsLaunchable ? PAIRED_LAUNCHABLE : PAIRED_UNLAUNCHABLE;
+                    break;
+                case PairStatusMetadata.Status.FAIL:
+                    newState = FAILED;
+                    break;
+                default:
+                    if (mScanFastPairStoreItem != null) {
+                        newState = FOUND_DEVICE;
+                    }
+            }
+        }
+        if (newState == mFragmentState) {
+            return;
+        }
+        setState(newState);
+    }
+
+    @Override
+    public void setState(HalfSheetFragmentState state) {
+        super.setState(state);
+        invalidateTitles();
+        invalidateButtons();
+    }
+
+    private void setScanFastPairStoreItem(ScanFastPairStoreItem item) {
+        mScanFastPairStoreItem = item;
+        invalidateLaunchable();
+    }
+
+    private void invalidateLaunchable() {
+        String companionApp =
+                FastPairUtils.getCompanionAppFromActionUrl(mScanFastPairStoreItem.getActionUrl());
+        if (isEmpty(companionApp)) {
+            mIsLaunchable = false;
+            return;
+        }
+        mIsLaunchable =
+                FastPairUtils.isLaunchable(Objects.requireNonNull(getContext()), companionApp);
+    }
+
+    private void invalidateButtons() {
+        mConnectProgressBar.setVisibility(View.INVISIBLE);
+        mConnectButton.setVisibility(View.INVISIBLE);
+        mCancelButton.setVisibility(View.INVISIBLE);
+        mSetupButton.setVisibility(View.INVISIBLE);
+        mSettingsButton.setVisibility(View.INVISIBLE);
+        mInfoIconButton.setVisibility(View.INVISIBLE);
+
+        switch (mFragmentState) {
+            case FOUND_DEVICE:
+                mInfoIconButton.setVisibility(View.VISIBLE);
+                mConnectButton.setVisibility(View.VISIBLE);
+                break;
+            case PAIRING:
+                mConnectProgressBar.setVisibility(View.VISIBLE);
+                mCancelButton.setVisibility(View.VISIBLE);
+                setBackgroundClickable(false);
+                break;
+            case PAIRED_LAUNCHABLE:
+                mCancelButton.setVisibility(View.VISIBLE);
+                mSetupButton.setVisibility(View.VISIBLE);
+                setBackgroundClickable(true);
+                break;
+            case FAILED:
+                mSettingsButton.setVisibility(View.VISIBLE);
+                setBackgroundClickable(true);
+                break;
+            case NOT_STARTED:
+            case PAIRED_UNLAUNCHABLE:
+            default:
+                mCancelButton.setVisibility(View.VISIBLE);
+                setBackgroundClickable(true);
+        }
+    }
+
+    private void setBackgroundClickable(boolean isClickable) {
+        HalfSheetActivity activity = (HalfSheetActivity) getActivity();
+        if (activity == null) {
+            Log.w(TAG, "setBackgroundClickable: failed to set clickable to " + isClickable
+                    + " because cannot get HalfSheetActivity.");
+            return;
+        }
+        View background = activity.findViewById(R.id.background);
+        if (background == null) {
+            Log.w(TAG, "setBackgroundClickable: failed to set clickable to " + isClickable
+                    + " cannot find background at HalfSheetActivity.");
+            return;
+        }
+        Log.d(TAG, "setBackgroundClickable to " + isClickable);
+        background.setClickable(isClickable);
+    }
+
+    private void invalidateTitles() {
+        String newTitle = getTitle();
+        invalidateTextView(mTitleView, newTitle);
+        String newSubTitle = getSubTitle();
+        invalidateTextView(mSubTitleView, newSubTitle);
+    }
+
+    private void invalidateTextView(TextView textView, String newText) {
+        CharSequence oldText =
+                textView.getTag(TAG_PENDING_TEXT) != null
+                        ? (CharSequence) textView.getTag(TAG_PENDING_TEXT)
+                        : textView.getText();
+        if (TextUtils.equals(oldText, newText)) {
+            return;
+        }
+        if (TextUtils.isEmpty(oldText)) {
+            // First time run. Don't animate since there's nothing to animate from.
+            textView.setText(newText);
+        } else {
+            textView.setTag(TAG_PENDING_TEXT, newText);
+            textView
+                    .animate()
+                    .alpha(0f)
+                    .setDuration(TEXT_ANIMATION_DURATION_MILLISECONDS)
+                    .withEndAction(
+                            () -> {
+                                textView.setText(newText);
+                                textView
+                                        .animate()
+                                        .alpha(1f)
+                                        .setDuration(TEXT_ANIMATION_DURATION_MILLISECONDS);
+                            });
+        }
+    }
+
+    private String getTitle() {
+        switch (mFragmentState) {
+            case PAIRED_LAUNCHABLE:
+                return getString(R.string.fast_pair_title_setup);
+            case FAILED:
+                return getString(R.string.fast_pair_title_fail);
+            case FOUND_DEVICE:
+            case NOT_STARTED:
+            case PAIRED_UNLAUNCHABLE:
+            default:
+                return mScanFastPairStoreItem.getDeviceName();
+        }
+    }
+
+    private String getSubTitle() {
+        switch (mFragmentState) {
+            case PAIRED_LAUNCHABLE:
+                return String.format(
+                        mScanFastPairStoreItem
+                                .getFastPairStrings()
+                                .getPairingFinishedCompanionAppInstalled(),
+                        mScanFastPairStoreItem.getDeviceName());
+            case FAILED:
+                return mScanFastPairStoreItem.getFastPairStrings().getPairingFailDescription();
+            case PAIRED_UNLAUNCHABLE:
+                getString(R.string.fast_pair_device_ready);
+            // fall through
+            case FOUND_DEVICE:
+            case NOT_STARTED:
+                return mScanFastPairStoreItem.getFastPairStrings().getInitialPairingDescription();
+            default:
+                return "";
+        }
     }
 }
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/HalfSheetModuleFragment.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/HalfSheetModuleFragment.java
index 88caf95..f1db4d0 100644
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/HalfSheetModuleFragment.java
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/HalfSheetModuleFragment.java
@@ -15,17 +15,23 @@
  */
 package com.android.nearby.halfsheet.fragment;
 
+import static com.android.nearby.halfsheet.HalfSheetActivity.TAG;
+import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.NOT_STARTED;
+
 import android.os.Bundle;
+import android.util.Log;
 
 import androidx.annotation.Nullable;
 import androidx.fragment.app.Fragment;
 
 
 /** Base class for all of the half sheet fragment. */
-// TODO(b/177675274): Resolve nullness suppression.
-@SuppressWarnings("nullness")
 public abstract class HalfSheetModuleFragment extends Fragment {
 
+    static final int TEXT_ANIMATION_DURATION_MILLISECONDS = 200;
+
+    HalfSheetFragmentState mFragmentState = NOT_STARTED;
+
     @Override
     public void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -38,26 +44,15 @@
 
     /** UI states of the half-sheet fragment. */
     public enum HalfSheetFragmentState {
-        NOT_STARTED,
-        SYNC_CONTACTS,
-        SYNC_SMS,
-        PROGRESSING,
-        CONFIRM_PASSKEY,
-        WRONG_PASSKEY,
-        PAIRING,
-        ADDITIONAL_SETUP_PROGRESS,
-        ADDITIONAL_SETUP_FINAL,
-        RESULT_SUCCESS,
-        RESULT_FAILURE,
-        FINISHED
-    }
-
-    /** Only used in {@link DevicePairingFragment} show pairing success info in half sheet. */
-    public void showSuccessInfo() {
-    }
-
-    /** Only used in {@link DevicePairingFragment} show pairing fail info in half sheet. */
-    public void showFailInfo() {
+        NOT_STARTED, // Initial status
+        FOUND_DEVICE, // When a device is found found from Nearby scan service
+        PAIRING, // When user taps 'Connect' and Fast Pair stars pairing process
+        PAIRED_LAUNCHABLE, // When pair successfully
+        // and we found a launchable companion app installed
+        PAIRED_UNLAUNCHABLE, // When pair successfully
+        // but we cannot find a companion app to launch it
+        FAILED, // When paring was failed
+        FINISHED // When the activity is about to end finished.
     }
 
     /**
@@ -67,6 +62,16 @@
      * activity.
      */
     public HalfSheetFragmentState getFragmentState() {
-        return HalfSheetFragmentState.NOT_STARTED;
+        return mFragmentState;
     }
+
+    void setState(HalfSheetFragmentState state) {
+        Log.v(TAG, "Settings state from " + mFragmentState + " to " + state);
+        mFragmentState = state;
+    }
+
+    /**
+     * Populate data to UI widgets according to the latest {@link HalfSheetFragmentState}.
+     */
+    abstract void invalidateState();
 }
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/BroadcastUtils.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/BroadcastUtils.java
index cae2d8e..467997c 100644
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/BroadcastUtils.java
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/BroadcastUtils.java
@@ -16,30 +16,13 @@
 
 package com.android.nearby.halfsheet.utils;
 
-
-import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 
 /**
  * Broadcast util class
  */
 public class BroadcastUtils {
-    /**
-     * Registers a list of broadcast receiver.
-     */
-    public static void registerReceiver(
-            Context context, BroadcastReceiver receiver, IntentFilter intentFilter) {
-        context.registerReceiver(receiver, intentFilter);
-    }
-
-    /**
-     * Unregisters the already registered receiver.
-     */
-    public static void unregisterReceiver(Context context, BroadcastReceiver receiver) {
-        context.unregisterReceiver(receiver);
-    }
 
     /**
      * Helps send broadcast.
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/FastPairUtils.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/FastPairUtils.java
index ee869ba..903ea90 100644
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/FastPairUtils.java
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/FastPairUtils.java
@@ -18,11 +18,15 @@
 import static com.android.server.nearby.common.fastpair.service.UserActionHandlerBase.EXTRA_COMPANION_APP;
 import static com.android.server.nearby.fastpair.UserActionHandler.ACTION_FAST_PAIR;
 
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import java.net.URISyntaxException;
@@ -34,10 +38,8 @@
  */
 public class FastPairUtils {
 
-    public static final String TAG = "HalfSheetActivity";
-
     /** FastPair util method check certain app is install on the device or not. */
-    public static boolean isAppInstalled(String packageName, Context context) {
+    public static boolean isAppInstalled(Context context, String packageName) {
         try {
             context.getPackageManager().getPackageInfo(packageName, 0);
             return true;
@@ -71,7 +73,8 @@
     }
 
     /**
-     * Converts a {@link ScanFastPairStoreItem} to a {@link StoredDiscoveryItem}.
+     * Converts a {@link service.proto.Cache.ScanFastPairStoreItem}
+     * to a {@link service.proto.Cache.StoredDiscoveryItem}.
      *
      * <p>This is needed to make the new Fast Pair scanning stack compatible with the rest of the
      * legacy Fast Pair code.
@@ -110,7 +113,39 @@
                 .build();
     }
 
-    private FastPairUtils() {
-
+    /**
+     * Returns true the application is installed and can be opened on device.
+     */
+    public static boolean isLaunchable(@NonNull Context context, String companionApp) {
+        return isAppInstalled(context, companionApp)
+                && createCompanionAppIntent(context, companionApp, null) != null;
     }
+
+    /**
+     * Returns an intent to launch given the package name and bluetooth address (if provided).
+     * Returns null if no such an intent can be found.
+     */
+    @Nullable
+    public static Intent createCompanionAppIntent(@NonNull Context context, String packageName,
+            @Nullable String address) {
+        Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName);
+        if (intent == null) {
+            return null;
+        }
+        if (address != null) {
+            BluetoothAdapter adapter = getBluetoothAdapter(context);
+            if (adapter != null) {
+                intent.putExtra(BluetoothDevice.EXTRA_DEVICE, adapter.getRemoteDevice(address));
+            }
+        }
+        return intent;
+    }
+
+    @Nullable
+    private static BluetoothAdapter getBluetoothAdapter(@NonNull Context context) {
+        BluetoothManager bluetoothManager = context.getSystemService(BluetoothManager.class);
+        return bluetoothManager == null ? null : bluetoothManager.getAdapter();
+    }
+
+    private FastPairUtils() {}
 }
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/IconUtils.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/IconUtils.java
index 0521b7b..218c756 100644
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/IconUtils.java
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/IconUtils.java
@@ -16,7 +16,7 @@
 
 package com.android.nearby.halfsheet.utils;
 
-import static com.android.nearby.halfsheet.utils.FastPairUtils.TAG;
+import static com.android.nearby.halfsheet.HalfSheetActivity.TAG;
 
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index 8f6a227..6743c21 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -53,7 +53,7 @@
     libs: [
         "framework-bluetooth.stubs.module_lib", // TODO(b/215722418): Change to framework-bluetooth once fixed
         "error_prone_annotations",
-        "framework-connectivity-tiramisu.impl",
+        "framework-connectivity-t.impl",
         "framework-statsd.stubs.module_lib",
     ],
     static_libs: [
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConnection.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConnection.java
index a7c05d6..c963aa6 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConnection.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConnection.java
@@ -18,7 +18,6 @@
 
 import android.annotation.WorkerThread;
 import android.bluetooth.BluetoothDevice;
-import android.content.Intent;
 
 import androidx.annotation.Nullable;
 import androidx.core.util.Consumer;
@@ -135,13 +134,6 @@
     @Nullable
     public abstract String getPublicAddress();
 
-    /**
-     * Creates cloud syncing intent which saves the Fast Pair device to the account.
-     *
-     * @param accountKey account key which is written into the Fast Pair device
-     * @return cloud syncing intent
-     */
-    public abstract Intent createCloudSyncingIntent(byte[] accountKey);
 
     /** Callback for getting notifications when pairing has completed. */
     public interface OnPairedCallback {
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java
index 8b466fa..789ef59 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java
@@ -20,7 +20,6 @@
 import static android.bluetooth.BluetoothDevice.BOND_BONDING;
 import static android.bluetooth.BluetoothDevice.BOND_NONE;
 
-import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.AES_BLOCK_LENGTH;
 import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
 import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.get16BitUuid;
 import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.to128BitUuid;
@@ -28,7 +27,6 @@
 import static com.android.server.nearby.common.bluetooth.fastpair.Bytes.toShorts;
 
 import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Strings.isNullOrEmpty;
 import static com.google.common.base.Verify.verifyNotNull;
 import static com.google.common.io.BaseEncoding.base16;
 import static com.google.common.primitives.Bytes.concat;
@@ -221,6 +219,12 @@
                     },
                     REQUESTED_SERVICES_LTV);
 
+    private static boolean sTestMode = false;
+
+    static void enableTestMode() {
+        sTestMode = true;
+    }
+
     /**
      * Operation Result Code.
      */
@@ -493,6 +497,7 @@
         // Lazily initialize a new connection manager for each pairing request.
         initGattConnectionManager();
         boolean isSecretHandshakeCompleted = true;
+
         try {
             if (key != null && key.length > 0) {
                 // GATT_CONNECTION_AND_SECRET_HANDSHAKE start.
@@ -731,11 +736,6 @@
         }
     }
 
-    @VisibleForTesting
-    void setBeforeDirectlyConnectProfileFromCacheForTest(Runnable runnable) {
-        this.mBeforeDirectlyConnectProfileFromCacheForTest = runnable;
-    }
-
     /**
      * Logs for user retry, check go/fastpairquality21q3 for more details.
      */
@@ -898,6 +898,7 @@
                                     mBluetoothAdapter.getRemoteDevice(mBleAddress).unwrap(),
                                     mPreferences.getNumSdpAttempts()));
         }
+
         BluetoothDevice device =
                 mBluetoothAdapter.getRemoteDevice(brEdrHandoverInformation.mBluetoothAddress)
                         .unwrap();
@@ -910,6 +911,7 @@
                         ? null
                         : new KeyBasedPairingInfo(
                                 mPairingSecret, mGattConnectionManager, mProviderInitiatesBonding);
+
         BluetoothAudioPairer pairer =
                 new BluetoothAudioPairer(
                         mContext,
@@ -928,7 +930,9 @@
         // normally do and we can finish early. It is also more reliable than tearing down the bond
         // and recreating it.
         try {
-            attemptDirectConnectionIfBonded(device, pairer);
+            if (!sTestMode) {
+                attemptDirectConnectionIfBonded(device, pairer);
+            }
             callbackOnPaired();
             return maybeWriteAccountKey(device);
         } catch (PairingException e) {
@@ -1299,26 +1303,6 @@
     }
 
     /**
-     * Creates cloud syncing intent which saves the Fast Pair device to the account.
-     *
-     * @param accountKey account key which is written into the Fast Pair device
-     * @return cloud syncing intent
-     */
-    public Intent createCloudSyncingIntent(byte[] accountKey) {
-        Intent intent = new Intent(BroadcastConstants.ACTION_FAST_PAIR_DEVICE_ADDED);
-        intent.setClassName(BroadcastConstants.PACKAGE_NAME, BroadcastConstants.SERVICE_NAME);
-        intent.putExtra(BroadcastConstants.EXTRA_ADDRESS, mBleAddress);
-        if (mPublicAddress != null) {
-            intent.putExtra(BroadcastConstants.EXTRA_PUBLIC_ADDRESS, mPublicAddress);
-        }
-        intent.putExtra(BroadcastConstants.EXTRA_ACCOUNT_KEY, accountKey);
-        intent.putExtra(
-                BroadcastConstants.EXTRA_RETROACTIVE_PAIR, mPreferences.getIsRetroactivePairing());
-
-        return intent;
-    }
-
-    /**
      * Checks whether or not an account key should be written to the device and writes it if so.
      * This is called after handle notifying the pairedCallback that we've finished pairing, because
      * at this point the headset is ready to use.
@@ -1328,7 +1312,9 @@
             throws InterruptedException, ExecutionException, TimeoutException,
             NoSuchAlgorithmException,
             BluetoothException {
-        Locator.get(mContext, FastPairController.class).setShouldUpload(false);
+        if (!sTestMode) {
+            Locator.get(mContext, FastPairController.class).setShouldUpload(false);
+        }
         if (!shouldWriteAccountKey()) {
             // For FastPair 2.0, here should be a subsequent pairing case.
             return null;
@@ -1611,95 +1597,6 @@
         waitForBluetoothStateUsingPolling(state);
     }
 
-    /**
-     * Update device name to provider.
-     *
-     * <pre>
-     *    A) Connect GATT
-     *    B) Handshake with provider to get the pairing secret and public address
-     *    C) Write new device name into provider through name characteristic in GATT
-     *    D) Disconnect GATT
-     * </pre>
-     *
-     * Synchronous: Blocks until until the name has finished being written. Throws on any error.
-     *
-     * @param key is a 16-byte account key. See go/fast-pair-2-spec for how these keys are used.
-     * @return true if the task is done, i.e. name is written successfully or it is skipped because
-     * of unsupported Name characteristic, false if some error happens and may need to re-try.
-     */
-    @WorkerThread
-    public boolean updateProviderName(@Nullable byte[] key, @Nullable String deviceName)
-            throws BluetoothException, InterruptedException, TimeoutException, ExecutionException,
-            PairingException, GeneralSecurityException {
-        if (!mPreferences.getEnableNamingCharacteristic()) {
-            Log.i(TAG, "Disable NamingCharacteristic feature, ignoring.");
-            return false;
-        }
-        if (isNullOrEmpty(deviceName)) {
-            Log.i(TAG, "Provider name is null or empty, ignoring.");
-            return false;
-        }
-        if (key == null || key.length != AES_BLOCK_LENGTH) {
-            Log.i(TAG, "key is null or key length is not account key size.");
-            return false;
-        }
-
-        Log.i(TAG, "Start to update device name for provider.");
-        boolean result = false;
-        if (mPreferences.getExtraLoggingInformation() != null) {
-            this.mEventLogger.bind(
-                    mContext, mBleAddress, mPreferences.getExtraLoggingInformation());
-        }
-
-        // Lazily initialize a new connection manager for each renaming request.
-        mGattConnectionManager =
-                new GattConnectionManager(
-                        mContext,
-                        mPreferences,
-                        mEventLogger,
-                        mBluetoothAdapter,
-                        this::toggleBluetooth,
-                        mBleAddress,
-                        mTimingLogger,
-                        mFastPairSignalChecker,
-                        /* setMtu= */ true);
-
-        try (BluetoothGattConnection connection = mGattConnectionManager.getConnection()) {
-            UUID characteristicUuid = NameCharacteristic.getId(connection);
-            if (!validateBluetoothGattCharacteristic(connection, characteristicUuid)) {
-                Log.i(TAG, "Can't find name characteristic, skip to write name with retry times.");
-                mGattConnectionManager.closeConnection();
-                // Returns true because the task is done with no name characteristic in device.
-                return true;
-            }
-            mEventLogger.setCurrentEvent(EventCode.SECRET_HANDSHAKE);
-            // Handshake to get pairing secret for name characteristic decryption and encryption.
-            try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Handshake")) {
-                handshakeForActionOverBle(key, AdditionalDataType.PERSONALIZED_NAME);
-            }
-            mEventLogger.logCurrentEventSucceeded();
-            // After handshake to get secret, write the name back to provider.
-            try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
-                    "WriteNameToProvider")) {
-                result = writeNameToProvider(deviceName, mPublicAddress);
-            }
-        } catch (BluetoothException
-                | InterruptedException
-                | TimeoutException
-                | ExecutionException
-                | PairingException
-                | GeneralSecurityException e) {
-            if (mEventLogger.isCurrentEvent()) {
-                mEventLogger.logCurrentEventFailed(e);
-            }
-            throw e;
-        } finally {
-            mTimingLogger.dump();
-        }
-        mGattConnectionManager.closeConnection();
-        return result;
-    }
-
     private void waitForBluetoothStateUsingPolling(int state) throws TimeoutException {
         // There's a bug where we (pretty often!) never get the broadcast for STATE_ON or STATE_OFF.
         // So poll instead.
@@ -1882,9 +1779,13 @@
                             /* setMtu= */ true);
             mGattConnectionManager.closeConnection();
         }
+        if (sTestMode) {
+            return null;
+        }
         BluetoothGattConnection connection = mGattConnectionManager.getConnection();
         connection.setOperationTimeout(
                 TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
+
         try {
             String firmwareVersion =
                     new String(
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManager.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManager.java
index ff0efc7..e7ce4bf 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManager.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManager.java
@@ -70,6 +70,11 @@
     private final FastPairConnection.FastPairSignalChecker mFastPairSignalChecker;
     @Nullable
     private BluetoothGattConnection mGattConnection;
+    private static boolean sTestMode = false;
+
+    static void enableTestMode() {
+        sTestMode = true;
+    }
 
     GattConnectionManager(
             Context context,
@@ -147,6 +152,9 @@
             try (ScopedTiming scopedTiming =
                     new ScopedTiming(mTimingLogger, "Connect GATT #" + i)) {
                 Log.i(TAG, "Connecting to GATT server at " + maskBluetoothAddress(address));
+                if (sTestMode) {
+                    return null;
+                }
                 BluetoothGattConnection connection =
                         new BluetoothGattHelper(mContext, mBluetoothAdapter)
                                 .connect(
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/Constant.java b/nearby/service/java/com/android/server/nearby/fastpair/Constant.java
index 5958007..0695b5f 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/Constant.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/Constant.java
@@ -33,22 +33,11 @@
 
     public static final String EXTRA_BINDER = "com.android.server.nearby.fastpair.BINDER";
     public static final String EXTRA_BUNDLE = "com.android.server.nearby.fastpair.BUNDLE_EXTRA";
-    public static final String SUCCESS_STATE = "SUCCESS";
-    public static final String FAIL_STATE = "FAIL";
-    public static final String DISMISS = "DISMISS";
-    public static final String NEED_CONFIRM_PASSKEY = "NEED CONFIRM PASSKEY";
-    // device support assistant additional setup
-    public static final String NEED_ADDITIONAL_SETUP = "NEED ADDITIONAL SETUP";
-    public static final String SHOW_PAIRING_WITHOUT_INTERACTION =
-            "SHOW_PAIRING_WITHOUT_INTERACTION";
     public static final String ACTION_FAST_PAIR_HALF_SHEET_CANCEL =
             "com.android.nearby.ACTION_FAST_PAIR_HALF_SHEET_CANCEL";
-    public static final String ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET =
-            "com.android.nearby.ACTION_FAST_PAIR_BAN_STATE_RESET";
     public static final String EXTRA_HALF_SHEET_INFO =
             "com.android.nearby.halfsheet.HALF_SHEET";
     public static final String EXTRA_HALF_SHEET_TYPE =
             "com.android.nearby.halfsheet.HALF_SHEET_TYPE";
     public static final String DEVICE_PAIRING_FRAGMENT_TYPE = "DEVICE_PAIRING";
-
 }
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
index f8d5a62..6a296c2 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
@@ -16,6 +16,8 @@
 
 package com.android.server.nearby.fastpair;
 
+import static com.android.server.nearby.fastpair.Constant.TAG;
+
 import static com.google.common.primitives.Bytes.concat;
 
 import android.accounts.Account;
@@ -45,8 +47,6 @@
 public class FastPairAdvHandler {
     Context mContext;
     String mBleAddress;
-    private static final String TAG = "FastPairAdvHandler";
-
     /** The types about how the bloomfilter is processed. */
     public enum ProcessBloomFilterType {
         IGNORE, // The bloomfilter is not handled. e.g. distance is too far away.
@@ -69,26 +69,26 @@
     public void handleBroadcast(NearbyDevice device) {
         FastPairDevice fastPairDevice = (FastPairDevice) device;
         mBleAddress = fastPairDevice.getBluetoothAddress();
+        List<Account> accountList =
+                FastPairDataProvider.getInstance().loadFastPairEligibleAccounts();
         if (FastPairDecoder.checkModelId(fastPairDevice.getData())) {
             byte[] model = FastPairDecoder.getModelId(fastPairDevice.getData());
-            Log.d("FastPairService",
-                    "On discovery model id " + Hex.bytesToStringLowercase(model));
+            Log.d(TAG, "On discovery model id " + Hex.bytesToStringLowercase(model));
             // Use api to get anti spoofing key from model id.
             try {
                 Rpcs.GetObservedDeviceResponse response =
                         FastPairDataProvider.getInstance()
                                 .loadFastPairAntispoofkeyDeviceMetadata(model);
                 Locator.get(mContext, FastPairHalfSheetManager.class).showHalfSheet(
-                        DataUtils.toScanFastPairStoreItem(response, mBleAddress));
+                        DataUtils.toScanFastPairStoreItem(
+                                response, mBleAddress,
+                                accountList.isEmpty() ? null : accountList.get(0).name));
             } catch (IllegalStateException e) {
                 Log.e(TAG, "OEM does not construct fast pair data proxy correctly");
             }
-
         } else {
             // Start to process bloom filter
             try {
-                List<Account> accountList =
-                        FastPairDataProvider.getInstance().loadFastPairEligibleAccounts();
                 Log.d(TAG, "account list size" + accountList.size());
                 byte[] bloomFilterByteArray = FastPairDecoder
                         .getBloomFilter(fastPairDevice.getData());
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java
index 793e126..1264ade 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java
@@ -16,21 +16,15 @@
 
 package com.android.server.nearby.fastpair;
 
-import static com.android.server.nearby.common.bluetooth.fastpair.BroadcastConstants.EXTRA_RETROACTIVE_PAIR;
-import static com.android.server.nearby.common.fastpair.service.UserActionHandlerBase.EXTRA_COMPANION_APP;
-import static com.android.server.nearby.fastpair.FastPairManager.EXTRA_NOTIFICATION_ID;
-
-import static com.google.common.io.BaseEncoding.base16;
 import static com.google.common.primitives.Bytes.concat;
 
 import android.accounts.Account;
 import android.annotation.Nullable;
 import android.content.Context;
-import android.content.Intent;
+import android.nearby.FastPairDevice;
 import android.text.TextUtils;
 import android.util.Log;
 
-import androidx.annotation.UiThread;
 import androidx.annotation.WorkerThread;
 
 import com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress;
@@ -110,73 +104,60 @@
     /**
      * Pairing function.
      */
-    @UiThread
-    public void pair(Intent intent) {
-        String itemId = intent.getStringExtra(UserActionHandler.EXTRA_ITEM_ID);
-        int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1);
-        byte[] discoveryItem = intent.getByteArrayExtra(UserActionHandler.EXTRA_DISCOVERY_ITEM);
-        String accountKeyString = intent.getStringExtra(UserActionHandler.EXTRA_FAST_PAIR_SECRET);
-        String companionApp = trimCompanionApp(intent.getStringExtra(EXTRA_COMPANION_APP));
-        byte[] accountKey = accountKeyString != null ? base16().decode(accountKeyString) : null;
-        boolean isRetroactivePair = intent.getBooleanExtra(EXTRA_RETROACTIVE_PAIR, false);
+    public void pair(FastPairDevice fastPairDevice) {
+        byte[] discoveryItem = fastPairDevice.getData();
+        String modelId = fastPairDevice.getModelId();
 
+        Log.v(TAG, "pair: fastPairDevice " + fastPairDevice);
         mEventLoop.postRunnable(
-                new NamedRunnable("fastPairWith=" + itemId) {
+                new NamedRunnable("fastPairWith=" + modelId) {
                     @Override
                     public void run() {
-                        DiscoveryItem item = null;
-                        if (discoveryItem != null) {
-                            try {
-                                item = new DiscoveryItem(mContext,
-                                        Cache.StoredDiscoveryItem.parseFrom(discoveryItem));
-                            } catch (InvalidProtocolBufferException e) {
-                                Log.w(TAG,
-                                        "Error parsing serialized discovery item with size "
-                                                + discoveryItem.length);
+                        try {
+                            DiscoveryItem item = new DiscoveryItem(mContext,
+                                    Cache.StoredDiscoveryItem.parseFrom(discoveryItem));
+                            if (TextUtils.isEmpty(item.getMacAddress())) {
+                                Log.w(TAG, "There is no mac address in the DiscoveryItem,"
+                                        + " ignore pairing");
                                 return;
                             }
+                            // Check enabled state to prevent multiple pair attempts if we get the
+                            // intent more than once (this can happen due to an Android platform
+                            // bug - b/31459521).
+                            if (item.getState()
+                                    != Cache.StoredDiscoveryItem.State.STATE_ENABLED) {
+                                Log.d(TAG, "Incorrect state, ignore pairing");
+                                return;
+                            }
+                            boolean useLargeNotifications =
+                                    item.getAuthenticationPublicKeySecp256R1() != null;
+                            FastPairNotificationManager fastPairNotificationManager =
+                                    new FastPairNotificationManager(mContext, item,
+                                            useLargeNotifications);
+                            FastPairHalfSheetManager fastPairHalfSheetManager =
+                                    Locator.get(mContext, FastPairHalfSheetManager.class);
+                            mFastPairCacheManager.saveDiscoveryItem(item);
+
+                            PairingProgressHandlerBase pairingProgressHandlerBase =
+                                    PairingProgressHandlerBase.create(
+                                            mContext,
+                                            item,
+                                            /* companionApp= */ null,
+                                            /* accountKey= */ null,
+                                            mFootprintsDeviceManager,
+                                            fastPairNotificationManager,
+                                            fastPairHalfSheetManager,
+                                            /* isRetroactivePair= */ false);
+
+                            pair(item,
+                                    /* accountKey= */ null,
+                                    /* companionApp= */ null,
+                                    pairingProgressHandlerBase);
+                        } catch (InvalidProtocolBufferException e) {
+                            Log.w(TAG,
+                                    "Error parsing serialized discovery item with size "
+                                            + discoveryItem.length);
                         }
-
-
-                        if (item == null || TextUtils.isEmpty(item.getMacAddress())) {
-                            Log.w(TAG, "Invalid DiscoveryItem, ignore pairing");
-                            return;
-                        }
-
-                        // Check enabled state to prevent multiple pair attempts if we get the
-                        // intent more than once (this can happen due to an Android platform
-                        // bug - b/31459521).
-                        if (item.getState() != Cache.StoredDiscoveryItem.State.STATE_ENABLED
-                                && !isRetroactivePair) {
-                            Log.d(TAG, "Incorrect state, ignore pairing");
-                            return;
-                        }
-
-                        boolean useLargeNotifications = accountKey != null
-                                || item.getAuthenticationPublicKeySecp256R1() != null;
-                        FastPairNotificationManager fastPairNotificationManager =
-                                notificationId == -1
-                                        ? new FastPairNotificationManager(mContext, item,
-                                        useLargeNotifications)
-                                        : new FastPairNotificationManager(mContext, item,
-                                                useLargeNotifications, notificationId);
-                        FastPairHalfSheetManager fastPairHalfSheetManager =
-                                Locator.get(mContext, FastPairHalfSheetManager.class);
-
-                        mFastPairCacheManager.saveDiscoveryItem(item);
-
-                        PairingProgressHandlerBase pairingProgressHandlerBase =
-                                PairingProgressHandlerBase.create(
-                                        mContext,
-                                        item,
-                                        companionApp,
-                                        accountKey,
-                                        mFootprintsDeviceManager,
-                                        fastPairNotificationManager,
-                                        fastPairHalfSheetManager,
-                                        isRetroactivePair);
-
-                        pair(item, accountKey, companionApp, pairingProgressHandlerBase);
                     }
                 });
     }
@@ -315,4 +296,4 @@
     interface Callback {
         void fastPairUpdateDeviceItemsEnabled(boolean enabled);
     }
-}
+}
\ No newline at end of file
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
index 9e1a718..3a3c962 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
@@ -56,6 +56,7 @@
 import com.android.server.nearby.fastpair.cache.DiscoveryItem;
 import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
 import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
+import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
 import com.android.server.nearby.fastpair.pairinghandler.PairingProgressHandlerBase;
 import com.android.server.nearby.util.FastPairDecoder;
 import com.android.server.nearby.util.ForegroundThread;
@@ -194,7 +195,7 @@
             @Nullable byte[] accountKey,
             FootprintsDeviceManager footprints,
             PairingProgressHandlerBase pairingProgressHandlerBase) {
-
+        FastPairHalfSheetManager manager = Locator.get(context, FastPairHalfSheetManager.class);
         try {
             pairingProgressHandlerBase.onPairingStarted();
             if (pairingProgressHandlerBase.skipWaitingScreenUnlock()) {
@@ -279,6 +280,10 @@
                 // Fast Pair one
                 connection.pair();
             }
+
+            // TODO(b/213373051): Merge logic with pairingProgressHandlerBase or delete the
+            // pairingProgressHandlerBase class.
+            manager.showPairingSuccessHalfSheet(connection.getPublicAddress());
             pairingProgressHandlerBase.onPairingSuccess(connection.getPublicAddress());
         } catch (BluetoothException
                 | InterruptedException
@@ -287,7 +292,11 @@
                 | ExecutionException
                 | PairingException
                 | GeneralSecurityException e) {
-            Log.e(TAG, "FastPair: Error");
+            Log.e(TAG, "Failed to pair.", e);
+
+            // TODO(b/213373051): Merge logic with pairingProgressHandlerBase or delete the
+            // pairingProgressHandlerBase class.
+            manager.showPairingFailed();
             pairingProgressHandlerBase.onPairingFailed(e);
         }
     }
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManager.java b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManager.java
index cfac904..6f79e6e 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManager.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManager.java
@@ -29,11 +29,15 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.nearby.FastPairDevice;
+import android.nearby.FastPairStatusCallback;
+import android.nearby.PairStatusMetadata;
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.util.Log;
 
-import com.android.server.nearby.common.locator.Locator;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
 import com.android.server.nearby.fastpair.FastPairController;
 import com.android.server.nearby.fastpair.cache.DiscoveryItem;
 import com.android.server.nearby.util.Environment;
@@ -50,17 +54,22 @@
     private static final String ACTIVITY_INTENT_ACTION = "android.nearby.SHOW_HALFSHEET";
     private static final String HALF_SHEET_CLASS_NAME =
             "com.android.nearby.halfsheet.HalfSheetActivity";
+    private static final String TAG = "FPHalfSheetManager";
 
     private String mHalfSheetApkPkgName;
-    private Context mContext;
+    private final LocatorContextWrapper mLocatorContextWrapper;
 
-    /**
-     * Construct function
-     */
+    FastPairService mFastPairService;
+
     public FastPairHalfSheetManager(Context context) {
-        mContext = context;
+        this(new LocatorContextWrapper(context));
     }
 
+    @VisibleForTesting
+    FastPairHalfSheetManager(LocatorContextWrapper locatorContextWrapper) {
+        mLocatorContextWrapper = locatorContextWrapper;
+        mFastPairService = new FastPairService();
+    }
 
     /**
      * Invokes half sheet in the other apk. This function can only be called in Nearby because other
@@ -68,13 +77,17 @@
      */
     public void showHalfSheet(Cache.ScanFastPairStoreItem scanFastPairStoreItem) {
         try {
-            if (mContext != null) {
+            if (mLocatorContextWrapper != null) {
                 String packageName = getHalfSheetApkPkgName();
-                HalfSheetCallback callback = new HalfSheetCallback();
-                callback.setmFastPairController(Locator.get(mContext, FastPairController.class));
+                if (packageName == null) {
+                    Log.e(TAG, "package name is null");
+                    return;
+                }
+                mFastPairService.setFastPairController(
+                        mLocatorContextWrapper.getLocator().get(FastPairController.class));
                 Bundle bundle = new Bundle();
-                bundle.putBinder(EXTRA_BINDER, callback);
-                mContext
+                bundle.putBinder(EXTRA_BINDER, mFastPairService);
+                mLocatorContextWrapper
                         .startActivityAsUser(new Intent(ACTIVITY_INTENT_ACTION)
                                         .putExtra(EXTRA_HALF_SHEET_INFO,
                                                 scanFastPairStoreItem.toByteArray())
@@ -84,11 +97,9 @@
                                         .setComponent(new ComponentName(packageName,
                                                 HALF_SHEET_CLASS_NAME)),
                                 UserHandle.CURRENT);
-
             }
         } catch (IllegalStateException e) {
-            Log.e("FastPairHalfSheetManager",
-                    "Can't resolve package that contains half sheet");
+            Log.e(TAG, "Can't resolve package that contains half sheet");
         }
     }
 
@@ -96,7 +107,15 @@
      * Shows pairing fail half sheet.
      */
     public void showPairingFailed() {
-        Log.d("FastPairHalfSheetManager", "show fail half sheet");
+        FastPairStatusCallback pairStatusCallback = mFastPairService.getPairStatusCallback();
+        if (pairStatusCallback != null) {
+            Log.v(TAG, "showPairingFailed: pairStatusCallback not NULL");
+            pairStatusCallback.onPairUpdate(new FastPairDevice.Builder().build(),
+                    new PairStatusMetadata(PairStatusMetadata.Status.FAIL));
+        } else {
+            Log.w(TAG, "FastPairHalfSheetManager failed to show success half sheet because "
+                    + "the pairStatusCallback is null");
+        }
     }
 
     /**
@@ -116,14 +135,22 @@
      * This function will handle pairing steps for half sheet.
      */
     public void showPairingHalfSheet(DiscoveryItem item) {
-        Log.d("FastPairHalfSheetManager", "show pairing half sheet");
+        Log.d(TAG, "show pairing half sheet");
     }
 
     /**
      * Shows pairing success info.
      */
     public void showPairingSuccessHalfSheet(String address) {
-        Log.d("FastPairHalfSheetManager", "show success half sheet");
+        FastPairStatusCallback pairStatusCallback = mFastPairService.getPairStatusCallback();
+        if (pairStatusCallback != null) {
+            pairStatusCallback.onPairUpdate(
+                    new FastPairDevice.Builder().setBluetoothAddress(address).build(),
+                    new PairStatusMetadata(PairStatusMetadata.Status.SUCCESS));
+        } else {
+            Log.w(TAG, "FastPairHalfSheetManager failed to show success half sheet because "
+                    + "the pairStatusCallback is null");
+        }
     }
 
     /**
@@ -153,7 +180,7 @@
         if (mHalfSheetApkPkgName != null) {
             return mHalfSheetApkPkgName;
         }
-        List<ResolveInfo> resolveInfos = mContext
+        List<ResolveInfo> resolveInfos = mLocatorContextWrapper
                 .getPackageManager().queryIntentActivities(
                         new Intent(ACTION_RESOURCES_APK),
                         PackageManager.MATCH_SYSTEM_ONLY);
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairService.java b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairService.java
new file mode 100644
index 0000000..8c0d572
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairService.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.halfsheet;
+
+import static com.android.server.nearby.fastpair.Constant.TAG;
+
+import android.nearby.FastPairDevice;
+import android.nearby.FastPairStatusCallback;
+import android.nearby.PairStatusMetadata;
+import android.nearby.aidl.IFastPairClient;
+import android.nearby.aidl.IFastPairStatusCallback;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.server.nearby.fastpair.FastPairController;
+
+/**
+ * Service implementing Fast Pair functionality.
+ *
+ * @hide
+ */
+public class FastPairService extends IFastPairClient.Stub {
+
+    private IBinder mStatusCallbackProxy;
+    private FastPairController mFastPairController;
+    private FastPairStatusCallback mFastPairStatusCallback;
+
+    /**
+     * Registers the Binder call back in the server notifies the proxy when there is an update
+     * in the server.
+     */
+    @Override
+    public void registerHalfSheet(IFastPairStatusCallback iFastPairStatusCallback) {
+        mStatusCallbackProxy = iFastPairStatusCallback.asBinder();
+        mFastPairStatusCallback = new FastPairStatusCallback() {
+            @Override
+            public void onPairUpdate(FastPairDevice fastPairDevice,
+                    PairStatusMetadata pairStatusMetadata) {
+                try {
+                    iFastPairStatusCallback.onPairUpdate(fastPairDevice, pairStatusMetadata);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Failed to update pair status.", e);
+                }
+            }
+        };
+    }
+
+    /**
+     * Unregisters the Binder call back in the server.
+     */
+    @Override
+    public void unregisterHalfSheet(IFastPairStatusCallback iFastPairStatusCallback) {
+        mStatusCallbackProxy = null;
+        mFastPairStatusCallback = null;
+    }
+
+    /**
+     * Asks the Fast Pair service to pair the device.
+     */
+    @Override
+    public void connect(FastPairDevice fastPairDevice) {
+        if (mFastPairController != null) {
+            mFastPairController.pair(fastPairDevice);
+        } else {
+            Log.w(TAG, "Failed to connect because there is no FastPairController.");
+        }
+    }
+
+    public FastPairStatusCallback getPairStatusCallback() {
+        return mFastPairStatusCallback;
+    }
+
+    /**
+     * Sets function for Fast Pair controller.
+     */
+    public void setFastPairController(FastPairController fastPairController) {
+        mFastPairController = fastPairController;
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/HalfSheetCallback.java b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/HalfSheetCallback.java
deleted file mode 100644
index 2c792ed..0000000
--- a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/HalfSheetCallback.java
+++ /dev/null
@@ -1,52 +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.android.server.nearby.fastpair.halfsheet;
-
-import android.content.Intent;
-import android.nearby.IFastPairHalfSheetCallback;
-import android.util.Log;
-
-import com.android.server.nearby.fastpair.FastPairController;
-
-
-/**
- * Callback to send ux action back to nearby service.
- */
-public class HalfSheetCallback extends IFastPairHalfSheetCallback.Stub {
-    private FastPairController mFastPairController;
-
-    public HalfSheetCallback() {
-    }
-
-    /**
-     * Set function for Fast Pair controller.
-     */
-    public void setmFastPairController(FastPairController fastPairController) {
-        mFastPairController = fastPairController;
-    }
-
-    /**
-     * Half Sheet connection button clicked.
-     */
-    @Override
-    public void onHalfSheetConnectionConfirm(Intent intent) {
-        Log.d("FastPairHalfSheet", "Call back receiver");
-        if (mFastPairController != null) {
-            mFastPairController.pair(intent);
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java b/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
new file mode 100644
index 0000000..c355df2
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.to128BitUuid;
+
+import java.util.UUID;
+
+/**
+ * Constants for Nearby Presence operations.
+ */
+public class PresenceConstants {
+
+    /** Presence advertisement service data uuid. */
+    public static final UUID PRESENCE_UUID = to128BitUuid((short) 0xFCF1);
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java b/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
new file mode 100644
index 0000000..7a77aa9
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import android.nearby.NearbyDevice;
+import android.nearby.PresenceDevice;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a Presence discovery result.
+ */
+public class PresenceDiscoveryResult {
+
+    /**
+     * Creates a {@link PresenceDiscoveryResult} from the scan data.
+     */
+    public static PresenceDiscoveryResult fromScanData(byte[] scanData, int rssi) {
+        return new PresenceDiscoveryResult.Builder().setRssi(rssi).build();
+    }
+
+    private final int mTxPower;
+    private final int mRssi;
+    private final byte[] mSalt;
+    private final List<Integer> mPresenceActions;
+    private final PublicCredential mPublicCredential;
+
+    private PresenceDiscoveryResult(int txPower, int rssi, byte[] salt,
+            List<Integer> presenceActions, PublicCredential publicCredential) {
+        mTxPower = txPower;
+        mRssi = rssi;
+        mSalt = salt;
+        mPresenceActions = presenceActions;
+        mPublicCredential = publicCredential;
+    }
+
+    /**
+     * Returns whether the discovery result matches the scan filter.
+     */
+    public boolean matches(PresenceScanFilter scanFilter) {
+        return pathLossMatches(scanFilter.getMaxPathLoss())
+                && actionMatches(scanFilter.getPresenceActions())
+                && credentialMatches(scanFilter.getCredentials());
+    }
+
+    private boolean pathLossMatches(int maxPathLoss) {
+        return (mTxPower - mRssi) <= maxPathLoss;
+    }
+
+    private boolean actionMatches(List<Integer> filterActions) {
+        if (filterActions.isEmpty()) {
+            return true;
+        }
+        return filterActions.stream().anyMatch(mPresenceActions::contains);
+    }
+
+    private boolean credentialMatches(List<PublicCredential> credentials) {
+        return credentials.contains(mPublicCredential);
+    }
+
+    /**
+     * Converts a presence device from the discovery result.
+     */
+    public PresenceDevice toPresenceDevice() {
+        return new PresenceDevice.Builder()
+                .setRssi(mRssi)
+                .setSalt(mSalt)
+                .setSecretId(mPublicCredential.getSecretId())
+                .addMedium(NearbyDevice.Medium.BLE).build();
+    }
+
+    /**
+     * Builder for {@link PresenceDiscoveryResult}.
+     */
+    public static class Builder {
+        private int mTxPower;
+        private int mRssi;
+        private byte[] mSalt;
+
+        private PublicCredential mPublicCredential;
+        private final List<Integer> mPresenceActions;
+
+        public Builder() {
+            mPresenceActions = new ArrayList<>();
+        }
+
+        /**
+         * Sets the calibrated tx power for the discovery result.
+         */
+        public Builder setTxPower(int txPower) {
+            mTxPower = txPower;
+            return this;
+        }
+
+        /**
+         * Sets the rssi for the discovery result.
+         */
+        public Builder setRssi(int rssi) {
+            mRssi = rssi;
+            return this;
+        }
+
+        /**
+         * Sets the salt for the discovery result.
+         */
+        public Builder setSalt(byte[] salt) {
+            mSalt = salt;
+            return this;
+        }
+
+        /**
+         * Sets the public credential for the discovery result.
+         */
+        public Builder setPublicCredential(PublicCredential publicCredential) {
+            mPublicCredential = publicCredential;
+            return this;
+        }
+
+        /**
+         * Adds presence action of the discovery result.
+         */
+        public Builder addPresenceAction(int presenceAction) {
+            mPresenceActions.add(presenceAction);
+            return this;
+        }
+
+        /**
+         * Builds a {@link PresenceDiscoveryResult}.
+         */
+        public PresenceDiscoveryResult build() {
+            return new PresenceDiscoveryResult(mTxPower, mRssi, mSalt, mPresenceActions,
+                    mPublicCredential);
+        }
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
index ae7f133..a989143 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
@@ -21,6 +21,7 @@
 import android.annotation.Nullable;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanCallback;
 import android.bluetooth.le.ScanFilter;
 import android.bluetooth.le.ScanRecord;
 import android.bluetooth.le.ScanResult;
@@ -34,8 +35,11 @@
 
 import com.android.server.nearby.common.bluetooth.fastpair.Constants;
 import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.presence.PresenceConstants;
 import com.android.server.nearby.util.ForegroundThread;
 
+import com.google.common.annotations.VisibleForTesting;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -46,7 +50,10 @@
  */
 public class BleDiscoveryProvider extends AbstractDiscoveryProvider {
 
-    private static final ParcelUuid FAST_PAIR_UUID = new ParcelUuid(Constants.FastPairService.ID);
+    @VisibleForTesting
+    static final ParcelUuid FAST_PAIR_UUID = new ParcelUuid(Constants.FastPairService.ID);
+    private static final ParcelUuid PRESENCE_UUID = new ParcelUuid(PresenceConstants.PRESENCE_UUID);
+
     // Don't block the thread as it may be used by other services.
     private static final Executor NEARBY_EXECUTOR = ForegroundThread.getExecutor();
     private final Injector mInjector;
@@ -69,6 +76,11 @@
                         byte[] fastPairData = serviceDataMap.get(FAST_PAIR_UUID);
                         if (fastPairData != null) {
                             builder.setData(serviceDataMap.get(FAST_PAIR_UUID));
+                        } else {
+                            byte [] presenceData = serviceDataMap.get(PRESENCE_UUID);
+                            if (presenceData != null) {
+                                builder.setData(serviceDataMap.get(PRESENCE_UUID));
+                            }
                         }
                     }
                     mExecutor.execute(() -> mListener.onNearbyDeviceDiscovered(builder.build()));
@@ -148,6 +160,7 @@
             if (bluetoothLeScanner == null) {
                 Log.w(TAG, "BleDiscoveryProvider failed to start BLE scanning "
                         + "because BluetoothLeScanner is null.");
+                return;
             }
             bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallback);
         } catch (NullPointerException | IllegalStateException | SecurityException e) {
@@ -180,4 +193,9 @@
         }
         return new ScanSettings.Builder().setScanMode(bleScanMode).build();
     }
+
+    @VisibleForTesting
+    ScanCallback getScanCallback() {
+        return mScanCallback;
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
index 27f3acb..825a4ab 100644
--- a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
@@ -16,11 +16,15 @@
 
 package com.android.server.nearby.provider;
 
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
 import static com.android.server.nearby.NearbyService.TAG;
 
 import android.content.Context;
 import android.nearby.IScanListener;
 import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.ScanFilter;
 import android.nearby.ScanRequest;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -29,10 +33,13 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.metrics.NearbyMetrics;
+import com.android.server.nearby.presence.PresenceDiscoveryResult;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.stream.Collectors;
 
 /**
  * Manages all aspects of discovery providers.
@@ -56,6 +63,16 @@
                     Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
                     continue;
                 }
+                if (nearbyDevice.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
+                    List<ScanFilter> presenceFilters =
+                            record.getScanRequest().getScanFilters().stream().filter(
+                                    scanFilter -> scanFilter.getType()
+                                            == SCAN_TYPE_NEARBY_PRESENCE).collect(
+                                    Collectors.toList());
+                    if (!presenceFilterMatches(nearbyDevice, presenceFilters)) {
+                        continue;
+                    }
+                }
                 try {
                     record.getScanListener().onDiscovered(
                             PrivacyFilter.filter(record.getScanRequest().getScanType(),
@@ -175,6 +192,22 @@
         mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
     }
 
+    private static boolean presenceFilterMatches(NearbyDeviceParcelable device,
+            List<ScanFilter> scanFilters) {
+        if (scanFilters.isEmpty()) {
+            return true;
+        }
+        PresenceDiscoveryResult discoveryResult = PresenceDiscoveryResult.fromScanData(
+                device.getData(), device.getRssi());
+        for (ScanFilter scanFilter : scanFilters) {
+            PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
+            if (discoveryResult.matches(presenceScanFilter)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private static class ScanListenerRecord {
 
         private final ScanRequest mScanRequest;
diff --git a/nearby/service/java/com/android/server/nearby/util/DataUtils.java b/nearby/service/java/com/android/server/nearby/util/DataUtils.java
index ce738c8..0b01bc0 100644
--- a/nearby/service/java/com/android/server/nearby/util/DataUtils.java
+++ b/nearby/service/java/com/android/server/nearby/util/DataUtils.java
@@ -17,6 +17,7 @@
 package com.android.server.nearby.util;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import service.proto.Cache.ScanFastPairStoreItem;
 import service.proto.Cache.StoredDiscoveryItem;
@@ -36,20 +37,36 @@
      * Converts a {@link GetObservedDeviceResponse} to a {@link ScanFastPairStoreItem}.
      */
     public static ScanFastPairStoreItem toScanFastPairStoreItem(
-            GetObservedDeviceResponse observedDeviceResponse, @NonNull String bleAddress) {
+            GetObservedDeviceResponse observedDeviceResponse,
+            @NonNull String bleAddress, @Nullable String account) {
         Device device = observedDeviceResponse.getDevice();
+        String deviceName = device.getName();
         return ScanFastPairStoreItem.newBuilder()
                 .setAddress(bleAddress)
                 .setActionUrl(device.getIntentUri())
-                .setDeviceName(device.getName())
+                .setDeviceName(deviceName)
                 .setIconPng(observedDeviceResponse.getImage())
                 .setIconFifeUrl(device.getImageUrl())
                 .setAntiSpoofingPublicKey(device.getAntiSpoofingKeyPair().getPublicKey())
-                .setFastPairStrings(getFastPairStrings(observedDeviceResponse))
+                .setFastPairStrings(getFastPairStrings(observedDeviceResponse, deviceName, account))
                 .build();
     }
 
     /**
+     * Prints readable string for a {@link ScanFastPairStoreItem}.
+     */
+    public static String toString(ScanFastPairStoreItem item) {
+        return "ScanFastPairStoreItem=[address:" + item.getAddress()
+                + ", actionUr:" + item.getActionUrl()
+                + ", deviceName:" + item.getDeviceName()
+                + ", iconPng:" + item.getIconPng()
+                + ", iconFifeUrl:" + item.getIconFifeUrl()
+                + ", antiSpoofingKeyPair:" + item.getAntiSpoofingPublicKey()
+                + ", fastPairStrings:" + toString(item.getFastPairStrings())
+                + "]";
+    }
+
+    /**
      * Prints readable string for a {@link FastPairStrings}
      */
     public static String toString(FastPairStrings fastPairStrings) {
@@ -76,13 +93,17 @@
                 + "]";
     }
 
-    private static FastPairStrings getFastPairStrings(GetObservedDeviceResponse response) {
+    private static FastPairStrings getFastPairStrings(GetObservedDeviceResponse response,
+            String deviceName, @Nullable String account) {
         ObservedDeviceStrings strings = response.getStrings();
         return FastPairStrings.newBuilder()
                 .setTapToPairWithAccount(strings.getInitialNotificationDescription())
                 .setTapToPairWithoutAccount(
                         strings.getInitialNotificationDescriptionNoAccount())
-                .setInitialPairingDescription(strings.getInitialPairingDescription())
+                .setInitialPairingDescription(account == null
+                        ? strings.getInitialNotificationDescriptionNoAccount()
+                        : String.format(strings.getInitialPairingDescription(),
+                                deviceName, account))
                 .setPairingFinishedCompanionAppInstalled(
                         strings.getConnectSuccessCompanionAppInstalled())
                 .setPairingFinishedCompanionAppNotInstalled(
diff --git a/nearby/service/java/com/android/server/nearby/util/Environment.java b/nearby/service/java/com/android/server/nearby/util/Environment.java
index e7277cd..d397862 100644
--- a/nearby/service/java/com/android/server/nearby/util/Environment.java
+++ b/nearby/service/java/com/android/server/nearby/util/Environment.java
@@ -33,7 +33,7 @@
 
     /**
      * The path where the Nearby apex is mounted.
-     * Current value = "/apex/com.android.nearby"
+     * Current value = "/apex/com.android.tethering"
      */
     private static final String NEARBY_APEX_PATH =
             new File("/apex", NEARBY_APEX_NAME).getAbsolutePath();
@@ -55,7 +55,7 @@
 
     /**
      * Returns true if the app is in the nearby apex, false otherwise.
-     * Checks if the app's path starts with "/apex/com.android.nearby".
+     * Checks if the app's path starts with "/apex/com.android.tethering".
      */
     public static boolean isAppInNearbyApex(ApplicationInfo appInfo) {
         return appInfo.sourceDir.startsWith(NEARBY_APEX_PATH);
diff --git a/nearby/service/java/com/android/server/nearby/util/FastPairDecoder.java b/nearby/service/java/com/android/server/nearby/util/FastPairDecoder.java
index 5bb76c9..6021ff6 100644
--- a/nearby/service/java/com/android/server/nearby/util/FastPairDecoder.java
+++ b/nearby/service/java/com/android/server/nearby/util/FastPairDecoder.java
@@ -145,7 +145,6 @@
         return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER_NO_NOTIFICATION);
     }
 
-
     /**
      * Get random resolvableData
      */
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 9cbd7d0..599fe5c 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -29,7 +29,7 @@
     ],
     libs: [
         "android.test.base",
-        "framework-connectivity-tiramisu.impl",
+        "framework-connectivity-t.impl",
     ],
     srcs: ["src/**/*.java"],
     test_suites: [
diff --git a/nearby/tests/multidevices/clients/proguard.flags b/nearby/tests/multidevices/clients/proguard.flags
index ec8f526..2e34dce 100644
--- a/nearby/tests/multidevices/clients/proguard.flags
+++ b/nearby/tests/multidevices/clients/proguard.flags
@@ -3,6 +3,11 @@
      *;
 }
 
+# Keep simulator reflection callback.
+-keep class com.android.server.nearby.common.bluetooth.fastpair.testing.** {
+     *;
+}
+
 # Do not touch Mobly.
 -keep class com.google.android.mobly.** {
   *;
diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp
index 50c395a..851dd08 100644
--- a/nearby/tests/unit/Android.bp
+++ b/nearby/tests/unit/Android.bp
@@ -39,12 +39,17 @@
         "guava",
         "junit",
         "libprotobuf-java-lite",
-        "mockito-target",
+        "mockito-target-extended-minus-junit4",
         "platform-test-annotations",
         "service-nearby",
         "truth-prebuilt",
         // "Robolectric_all-target",
     ],
+    // these are needed for Extended Mockito
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
     test_suites: [
         "general-tests",
         "mts-tethering",
diff --git a/nearby/tests/unit/AndroidManifest.xml b/nearby/tests/unit/AndroidManifest.xml
index c756ff2..88c0f5f 100644
--- a/nearby/tests/unit/AndroidManifest.xml
+++ b/nearby/tests/unit/AndroidManifest.xml
@@ -20,8 +20,10 @@
 
     <uses-permission android:name="android.permission.INTERNET"/>
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
 
-    <application>
+    <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
     </application>
 
diff --git a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
new file mode 100644
index 0000000..c20cf22
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby;
+
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.Context;
+import android.nearby.IScanListener;
+import android.nearby.ScanRequest;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+public final class NearbyServiceTest {
+
+    private Context mContext;
+    private NearbyService mService;
+    private ScanRequest mScanRequest;
+    @Mock
+    private IScanListener mScanListener;
+
+    @Before
+    public void setup() {
+        initMocks(this);
+
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mService = new NearbyService(mContext);
+        mScanRequest = createScanRequest();
+    }
+
+    @Test
+    public void test_register() {
+        mService.registerScanListener(mScanRequest, mScanListener);
+    }
+
+    @Test
+    public void test_unregister() {
+        mService.unregisterScanListener(mScanListener);
+    }
+
+    private ScanRequest createScanRequest() {
+        return new ScanRequest.Builder()
+                .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR)
+                .setEnableBle(true)
+                .build();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnectionTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnectionTest.java
index 44bab71..a103a72 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnectionTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnectionTest.java
@@ -27,6 +27,7 @@
 import static org.mockito.ArgumentMatchers.anyShort;
 import static org.mockito.Mockito.doNothing;
 
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.platform.test.annotations.Presubmit;
 
@@ -38,7 +39,9 @@
 import com.android.server.nearby.common.bluetooth.BluetoothGattException;
 import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.protobuf.ByteString;
 
 import junit.framework.TestCase;
 
@@ -47,6 +50,8 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 
 /**
@@ -56,11 +61,16 @@
 @SmallTest
 public class FastPairDualConnectionTest extends TestCase {
 
-    private static final String BLE_ADDRESS = "BLE_ADDRESS";
+    private static final String BLE_ADDRESS = "00:11:22:33:FF:EE";
     private static final String MASKED_BLE_ADDRESS = "MASKED_BLE_ADDRESS";
     private static final short[] PROFILES = {Constants.A2DP_SINK_SERVICE_UUID};
     private static final int NUM_CONNECTION_ATTEMPTS = 1;
     private static final boolean ENABLE_PAIRING_BEHAVIOR = true;
+    private static final BluetoothDevice BLUETOOTH_DEVICE = BluetoothAdapter.getDefaultAdapter()
+            .getRemoteDevice("11:22:33:44:55:66");
+    private static final String DEVICE_NAME = "DEVICE_NAME";
+    private static final byte[] ACCOUNT_KEY = new byte[]{1, 3};
+    private static final byte[] HASH_VALUE = new byte[]{7};
 
     private TestEventLogger mEventLogger;
     @Mock private TimingLogger mTimingLogger;
@@ -70,6 +80,8 @@
     public void setUp() throws Exception {
         super.setUp();
 
+        BluetoothAudioPairer.enableTestMode();
+        FastPairDualConnection.enableTestMode();
         MockitoAnnotations.initMocks(this);
 
         doNothing().when(mBluetoothAudioPairer).connect(anyShort(), anyBoolean());
@@ -127,7 +139,7 @@
 
 
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void appendMoreErrorCode_gattError() {
+    public void testAppendMoreErrorCode_gattError() {
         assertThat(
                 appendMoreErrorCode(
                         GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED,
@@ -170,6 +182,131 @@
                 .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST);
     }
 
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testUnpairNotCrash() {
+        try {
+            new FastPairDualConnection(
+                    ApplicationProvider.getApplicationContext(),
+                    BLE_ADDRESS,
+                    Preferences.builder().build(),
+                    mEventLogger,
+                    mTimingLogger).unpair(BLUETOOTH_DEVICE);
+        } catch (ExecutionException | InterruptedException | ReflectionException
+                | TimeoutException | PairingException e) {
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testSetFastPairHistory() {
+        new FastPairDualConnection(
+                ApplicationProvider.getApplicationContext(),
+                BLE_ADDRESS,
+                Preferences.builder().build(),
+                mEventLogger,
+                mTimingLogger).setFastPairHistory(ImmutableList.of());
+    }
+
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testSetGetProviderDeviceName() {
+        FastPairDualConnection connection = new FastPairDualConnection(
+                ApplicationProvider.getApplicationContext(),
+                BLE_ADDRESS,
+                Preferences.builder().build(),
+                mEventLogger,
+                mTimingLogger);
+        connection.setProviderDeviceName(DEVICE_NAME);
+        connection.getProviderDeviceName();
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetExistingAccountKey() {
+        FastPairDualConnection connection = new FastPairDualConnection(
+                ApplicationProvider.getApplicationContext(),
+                BLE_ADDRESS,
+                Preferences.builder().build(),
+                mEventLogger,
+                mTimingLogger);
+        connection.getExistingAccountKey();
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testPair() {
+        FastPairDualConnection connection = new FastPairDualConnection(
+                ApplicationProvider.getApplicationContext(),
+                BLE_ADDRESS,
+                Preferences.builder().setNumSdpAttempts(0)
+                        .setLogPairWithCachedModelId(false).build(),
+                mEventLogger,
+                mTimingLogger);
+        try {
+            connection.pair();
+        } catch (BluetoothException | InterruptedException | ReflectionException
+                | ExecutionException | TimeoutException | PairingException e) {
+        }
+    }
+
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetPublicAddress() {
+        FastPairDualConnection connection = new FastPairDualConnection(
+                ApplicationProvider.getApplicationContext(),
+                BLE_ADDRESS,
+                Preferences.builder().setNumSdpAttempts(0)
+                        .setLogPairWithCachedModelId(false).build(),
+                mEventLogger,
+                mTimingLogger);
+        connection.getPublicAddress();
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testShouldWriteAccountKeyForExistingCase() {
+        FastPairDualConnection connection = new FastPairDualConnection(
+                ApplicationProvider.getApplicationContext(),
+                BLE_ADDRESS,
+                Preferences.builder().setNumSdpAttempts(0)
+                        .setLogPairWithCachedModelId(false).build(),
+                mEventLogger,
+                mTimingLogger);
+        connection.shouldWriteAccountKeyForExistingCase(ACCOUNT_KEY);
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testReadFirmwareVersion() {
+        FastPairDualConnection connection = new FastPairDualConnection(
+                ApplicationProvider.getApplicationContext(),
+                BLE_ADDRESS,
+                Preferences.builder().setNumSdpAttempts(0)
+                        .setLogPairWithCachedModelId(false).build(),
+                mEventLogger,
+                mTimingLogger);
+        try {
+            connection.readFirmwareVersion();
+        } catch (BluetoothException | InterruptedException | ExecutionException
+                | TimeoutException e) {
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testHistoryItem() {
+        FastPairDualConnection connection = new FastPairDualConnection(
+                ApplicationProvider.getApplicationContext(),
+                BLE_ADDRESS,
+                Preferences.builder().setNumSdpAttempts(0)
+                        .setLogPairWithCachedModelId(false).build(),
+                mEventLogger,
+                mTimingLogger);
+        ImmutableList.Builder<FastPairHistoryItem> historyBuilder = ImmutableList.builder();
+        FastPairHistoryItem historyItem1 =
+                FastPairHistoryItem.create(
+                        ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(HASH_VALUE));
+        historyBuilder.add(historyItem1);
+
+        connection.setFastPairHistory(historyBuilder.build());
+        assertThat(connection.mPairedHistoryFinder.isInPairedHistory("11:22:33:44:55:88"))
+                .isFalse();
+    }
+
     static class TestEventLogger implements EventLogger {
 
         private List<Item> mLogs = new ArrayList<>();
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManagerTest.java
new file mode 100644
index 0000000..2f80a30
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManagerTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.BluetoothGattException;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
+
+import com.google.common.collect.ImmutableSet;
+
+import junit.framework.TestCase;
+
+import java.time.Duration;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Unit tests for {@link GattConnectionManager}.
+ */
+@Presubmit
+@SmallTest
+public class GattConnectionManagerTest extends TestCase {
+
+    private static final String FAST_PAIR_ADDRESS = "BB:BB:BB:BB:BB:1E";
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        GattConnectionManager.enableTestMode();
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGattConnectionManagerConstructor() throws Exception {
+        GattConnectionManager manager = createManager(Preferences.builder());
+        try {
+            manager.getConnection();
+        } catch (ExecutionException e) {
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testIsNoRetryError() {
+        Preferences preferences =
+                Preferences.builder()
+                        .setGattConnectionAndSecretHandshakeNoRetryGattError(
+                                ImmutableSet.of(257, 999))
+                        .build();
+
+        assertThat(
+                GattConnectionManager.isNoRetryError(
+                        preferences, new BluetoothGattException("Test", 133)))
+                .isFalse();
+        assertThat(
+                GattConnectionManager.isNoRetryError(
+                        preferences, new BluetoothGattException("Test", 257)))
+                .isTrue();
+        assertThat(
+                GattConnectionManager.isNoRetryError(
+                        preferences, new BluetoothGattException("Test", 999)))
+                .isTrue();
+        assertThat(GattConnectionManager.isNoRetryError(
+                preferences, new BluetoothException("Test")))
+                .isFalse();
+        assertThat(
+                GattConnectionManager.isNoRetryError(
+                        preferences, new BluetoothOperationTimeoutException("Test")))
+                .isFalse();
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetTimeoutNotOverShortRetryMaxSpentTimeGetShort() {
+        Preferences preferences = Preferences.builder().build();
+
+        assertThat(
+                createManager(Preferences.builder(), () -> {})
+                        .getTimeoutMs(
+                                preferences.getGattConnectShortTimeoutRetryMaxSpentTimeMs() - 1))
+                .isEqualTo(preferences.getGattConnectShortTimeoutMs());
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetTimeoutOverShortRetryMaxSpentTimeGetLong() {
+        Preferences preferences = Preferences.builder().build();
+
+        assertThat(
+                createManager(Preferences.builder(), () -> {})
+                        .getTimeoutMs(
+                                preferences.getGattConnectShortTimeoutRetryMaxSpentTimeMs() + 1))
+                .isEqualTo(preferences.getGattConnectLongTimeoutMs());
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetTimeoutRetryNotEnabledGetOrigin() {
+        Preferences preferences = Preferences.builder().build();
+
+        assertThat(
+                createManager(
+                        Preferences.builder().setRetryGattConnectionAndSecretHandshake(false),
+                        () -> {})
+                        .getTimeoutMs(0))
+                .isEqualTo(Duration.ofSeconds(
+                        preferences.getGattConnectionTimeoutSeconds()).toMillis());
+    }
+
+    private GattConnectionManager createManager(Preferences.Builder prefs) {
+        return createManager(prefs, () -> {});
+    }
+
+    private GattConnectionManager createManager(
+            Preferences.Builder prefs, ToggleBluetoothTask toggleBluetooth) {
+        return createManager(prefs, toggleBluetooth,
+                /* fastPairSignalChecker= */ null);
+    }
+
+    private GattConnectionManager createManager(
+            Preferences.Builder prefs,
+            ToggleBluetoothTask toggleBluetooth,
+            @Nullable FastPairConnection.FastPairSignalChecker fastPairSignalChecker) {
+        return new GattConnectionManager(
+                ApplicationProvider.getApplicationContext(),
+                prefs.build(),
+                new EventLoggerWrapper(null),
+                BluetoothAdapter.getDefaultAdapter(),
+                toggleBluetooth,
+                FAST_PAIR_ADDRESS,
+                new TimingLogger("GattConnectionManager", prefs.build()),
+                fastPairSignalChecker,
+                /* setMtu= */ false);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
new file mode 100644
index 0000000..58e4c47
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.halfsheet;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.FastPairController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import service.proto.Cache;
+
+public class FastPairHalfSheetManagerTest {
+    private static final String BLEADDRESS = "11:22:44:66";
+    private static final String NAME = "device_name";
+    private FastPairHalfSheetManager mFastPairHalfSheetManager;
+    private Cache.ScanFastPairStoreItem mScanFastPairStoreItem;
+    @Mock
+    LocatorContextWrapper mContextWrapper;
+    @Mock
+    ResolveInfo mResolveInfo;
+    @Mock
+    PackageManager mPackageManager;
+    @Mock
+    Locator mLocator;
+    @Mock
+    FastPairController mFastPairController;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mScanFastPairStoreItem = Cache.ScanFastPairStoreItem.newBuilder()
+                .setAddress(BLEADDRESS)
+                .setDeviceName(NAME)
+                .build();
+    }
+
+    @Test
+    public void verifyFastPairHalfSheetManagerBehavior() {
+        mLocator.overrideBindingForTest(FastPairController.class, mFastPairController);
+        ResolveInfo resolveInfo = new ResolveInfo();
+        List<ResolveInfo> resolveInfoList = new ArrayList<>();
+
+        mPackageManager = mock(PackageManager.class);
+        when(mContextWrapper.getPackageManager()).thenReturn(mPackageManager);
+        resolveInfo.activityInfo = new ActivityInfo();
+        ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.sourceDir = "/apex/com.android.tethering";
+        applicationInfo.packageName = "test.package";
+        resolveInfo.activityInfo.applicationInfo = applicationInfo;
+        resolveInfoList.add(resolveInfo);
+        when(mPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(resolveInfoList);
+        when(mPackageManager.canRequestPackageInstalls()).thenReturn(false);
+
+        mFastPairHalfSheetManager =
+                new FastPairHalfSheetManager(mContextWrapper);
+
+        when(mContextWrapper.getLocator()).thenReturn(mLocator);
+
+        ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
+
+        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+
+        verify(mContextWrapper, atLeastOnce())
+                .startActivityAsUser(intentArgumentCaptor.capture(), eq(UserHandle.CURRENT));
+    }
+
+    @Test
+    public void verifyFastPairHalfSheetManagerHalfSheetApkNotValidBehavior() {
+        mLocator.overrideBindingForTest(FastPairController.class, mFastPairController);
+        ResolveInfo resolveInfo = new ResolveInfo();
+        List<ResolveInfo> resolveInfoList = new ArrayList<>();
+
+        mPackageManager = mock(PackageManager.class);
+        when(mContextWrapper.getPackageManager()).thenReturn(mPackageManager);
+        resolveInfo.activityInfo = new ActivityInfo();
+        ApplicationInfo applicationInfo = new ApplicationInfo();
+        // application directory is wrong
+        applicationInfo.sourceDir = "/apex/com.android.nearby";
+        applicationInfo.packageName = "test.package";
+        resolveInfo.activityInfo.applicationInfo = applicationInfo;
+        resolveInfoList.add(resolveInfo);
+        when(mPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(resolveInfoList);
+        when(mPackageManager.canRequestPackageInstalls()).thenReturn(false);
+
+        mFastPairHalfSheetManager =
+                new FastPairHalfSheetManager(mContextWrapper);
+
+        when(mContextWrapper.getLocator()).thenReturn(mLocator);
+
+        ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
+
+        mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+
+        verify(mContextWrapper, never())
+                .startActivityAsUser(intentArgumentCaptor.capture(), eq(UserHandle.CURRENT));
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/metrics/NearbyMetricsTest.java b/nearby/tests/unit/src/com/android/server/nearby/metrics/NearbyMetricsTest.java
new file mode 100644
index 0000000..252bb90
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/metrics/NearbyMetricsTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.metrics;
+
+import static android.nearby.ScanRequest.SCAN_MODE_BALANCED;
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_SHARE;
+
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.ScanRequest;
+import android.os.WorkSource;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.server.nearby.proto.NearbyStatsLog;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+public class NearbyMetricsTest {
+    private static final int SESSION_ID = 11111;
+    private static final int WORK_SOURCE_UID = 2222;
+
+    private static final String DEVICE_NAME = "testDevice";
+    private static final int SCAN_MEDIUM = 1;
+    private static final int RSSI = -60;
+    private static final String FAST_PAIR_MODEL_ID = "1234";
+    private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
+    private static final byte[] SCAN_DATA = new byte[]{1, 2, 3, 4};
+
+    private final WorkSource mWorkSource = new WorkSource(WORK_SOURCE_UID);
+    private final WorkSource mEmptyWorkSource = new WorkSource();
+
+    private final ScanRequest.Builder mScanRequestBuilder = new ScanRequest.Builder()
+            .setScanMode(SCAN_MODE_BALANCED)
+            .setScanType(SCAN_TYPE_NEARBY_SHARE);
+    private final ScanRequest mScanRequest = mScanRequestBuilder
+            .setWorkSource(mWorkSource)
+            .build();
+    private final ScanRequest mScanRequestWithEmptyWorkSource = mScanRequestBuilder
+            .setWorkSource(mEmptyWorkSource)
+            .build();
+
+    private final NearbyDeviceParcelable mNearbyDevice =
+            new NearbyDeviceParcelable.Builder()
+                    .setName(DEVICE_NAME)
+                    .setMedium(SCAN_MEDIUM)
+                    .setRssi(RSSI)
+                    .setFastPairModelId(FAST_PAIR_MODEL_ID)
+                    .setBluetoothAddress(BLUETOOTH_ADDRESS)
+                    .setData(SCAN_DATA).build();
+
+    private MockitoSession mSession;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mSession = ExtendedMockito.mockitoSession()
+                .strictness(Strictness.LENIENT)
+                .mockStatic(NearbyStatsLog.class)
+                .startMocking();
+    }
+
+    @After
+    public void tearDown() {
+        mSession.finishMocking();
+    }
+
+    @Test
+    public void testLogScanStart() {
+        NearbyMetrics.logScanStarted(SESSION_ID, mScanRequest);
+        ExtendedMockito.verify(() -> NearbyStatsLog.write(
+                NearbyStatsLog.NEARBY_DEVICE_SCAN_STATE_CHANGED,
+                WORK_SOURCE_UID,
+                SESSION_ID,
+                NearbyStatsLog
+                        .NEARBY_DEVICE_SCAN_STATE_CHANGED__SCAN_STATE__NEARBY_SCAN_STATE_STARTED,
+                SCAN_TYPE_NEARBY_SHARE,
+                0,
+                0,
+                "",
+                ""));
+    }
+
+    @Test
+    public void testLogScanStart_emptyWorkSource() {
+        NearbyMetrics.logScanStarted(SESSION_ID, mScanRequestWithEmptyWorkSource);
+        ExtendedMockito.verify(() -> NearbyStatsLog.write(
+                NearbyStatsLog.NEARBY_DEVICE_SCAN_STATE_CHANGED,
+                -1,
+                SESSION_ID,
+                NearbyStatsLog
+                        .NEARBY_DEVICE_SCAN_STATE_CHANGED__SCAN_STATE__NEARBY_SCAN_STATE_STARTED,
+                SCAN_TYPE_NEARBY_SHARE,
+                0,
+                0,
+                "",
+                ""));
+    }
+
+    @Test
+    public void testLogScanStopped() {
+        NearbyMetrics.logScanStopped(SESSION_ID, mScanRequest);
+        ExtendedMockito.verify(() -> NearbyStatsLog.write(
+                NearbyStatsLog.NEARBY_DEVICE_SCAN_STATE_CHANGED,
+                WORK_SOURCE_UID,
+                SESSION_ID,
+                NearbyStatsLog
+                        .NEARBY_DEVICE_SCAN_STATE_CHANGED__SCAN_STATE__NEARBY_SCAN_STATE_STOPPED,
+                SCAN_TYPE_NEARBY_SHARE,
+                0,
+                0,
+                "",
+                ""));
+    }
+
+    @Test
+    public void testLogScanStopped_emptyWorkSource() {
+        NearbyMetrics.logScanStopped(SESSION_ID, mScanRequestWithEmptyWorkSource);
+        ExtendedMockito.verify(() -> NearbyStatsLog.write(
+                NearbyStatsLog.NEARBY_DEVICE_SCAN_STATE_CHANGED,
+                -1,
+                SESSION_ID,
+                NearbyStatsLog
+                        .NEARBY_DEVICE_SCAN_STATE_CHANGED__SCAN_STATE__NEARBY_SCAN_STATE_STOPPED,
+                SCAN_TYPE_NEARBY_SHARE,
+                0,
+                0,
+                "",
+                ""));
+    }
+
+    @Test
+    public void testLogScanDeviceDiscovered() {
+        NearbyMetrics.logScanDeviceDiscovered(SESSION_ID, mScanRequest, mNearbyDevice);
+        ExtendedMockito.verify(() -> NearbyStatsLog.write(
+                NearbyStatsLog.NEARBY_DEVICE_SCAN_STATE_CHANGED,
+                WORK_SOURCE_UID,
+                SESSION_ID,
+                NearbyStatsLog
+                        .NEARBY_DEVICE_SCAN_STATE_CHANGED__SCAN_STATE__NEARBY_SCAN_STATE_DISCOVERED,
+                SCAN_TYPE_NEARBY_SHARE,
+                SCAN_MEDIUM,
+                RSSI,
+                FAST_PAIR_MODEL_ID,
+                ""));
+    }
+
+    @Test
+    public void testLogScanDeviceDiscovered_emptyWorkSource() {
+        NearbyMetrics.logScanDeviceDiscovered(SESSION_ID, mScanRequestWithEmptyWorkSource,
+                mNearbyDevice);
+        ExtendedMockito.verify(() -> NearbyStatsLog.write(
+                NearbyStatsLog.NEARBY_DEVICE_SCAN_STATE_CHANGED,
+                -1,
+                SESSION_ID,
+                NearbyStatsLog
+                        .NEARBY_DEVICE_SCAN_STATE_CHANGED__SCAN_STATE__NEARBY_SCAN_STATE_DISCOVERED,
+                SCAN_TYPE_NEARBY_SHARE,
+                SCAN_MEDIUM,
+                RSSI,
+                FAST_PAIR_MODEL_ID,
+                ""));
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java
new file mode 100644
index 0000000..dd94778
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.PresenceCredential;
+import android.nearby.PresenceDevice;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+/**
+ * Unit tests for {@link PresenceDiscoveryResult}.
+ */
+public class PresenceDiscoveryResultTest {
+    private static final int PRESENCE_ACTION = 123;
+    private static final int TX_POWER = -1;
+    private static final int RSSI = -41;
+    private static final byte[] SALT = new byte[]{12, 34};
+    private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
+    private static final byte[] AUTHENTICITY_KEY = new byte[]{12, 13, 14};
+
+    private PresenceDiscoveryResult.Builder mBuilder;
+    private PublicCredential mCredential;
+
+    @Before
+    public void setUp() {
+        mCredential =
+                new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY)
+                        .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+                        .build();
+        mBuilder = new PresenceDiscoveryResult.Builder()
+                .setPublicCredential(mCredential)
+                .setSalt(SALT)
+                .setTxPower(TX_POWER)
+                .setRssi(RSSI)
+                .addPresenceAction(PRESENCE_ACTION);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testToDevice() {
+        PresenceDiscoveryResult discoveryResult = mBuilder.build();
+        PresenceDevice presenceDevice = discoveryResult.toPresenceDevice();
+
+        assertThat(presenceDevice.getRssi()).isEqualTo(RSSI);
+        assertThat(Arrays.equals(presenceDevice.getSalt(), SALT)).isTrue();
+        assertThat(Arrays.equals(presenceDevice.getSecretId(), SECRET_ID)).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testMatches() {
+        PresenceScanFilter scanFilter = new PresenceScanFilter.Builder()
+                .setMaxPathLoss(80)
+                .addPresenceAction(PRESENCE_ACTION)
+                .addCredential(mCredential)
+                .build();
+
+        PresenceDiscoveryResult discoveryResult = mBuilder.build();
+        assertThat(discoveryResult.matches(scanFilter)).isTrue();
+    }
+
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
new file mode 100644
index 0000000..4c78885
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.nearby.injector.Injector;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public final class BleDiscoveryProviderTest {
+
+    private BluetoothAdapter mBluetoothAdapter;
+    private BleDiscoveryProvider mBleDiscoveryProvider;
+    @Mock
+    private AbstractDiscoveryProvider.Listener mListener;
+
+    @Before
+    public void setup() {
+        initMocks(this);
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        Injector injector = new TestInjector();
+
+        mBluetoothAdapter = context.getSystemService(BluetoothManager.class).getAdapter();
+        mBleDiscoveryProvider = new BleDiscoveryProvider(context, injector);
+    }
+
+    @Test
+    public void test_callback() throws InterruptedException {
+        mBleDiscoveryProvider.getController().setListener(mListener);
+        mBleDiscoveryProvider.onStart();
+        mBleDiscoveryProvider.getScanCallback()
+                .onScanResult(CALLBACK_TYPE_ALL_MATCHES, createScanResult());
+
+        // Wait for callback to be invoked
+        Thread.sleep(500);
+        verify(mListener, times(1)).onNearbyDeviceDiscovered(any());
+    }
+
+    @Test
+    public void test_stopScan() {
+        mBleDiscoveryProvider.onStart();
+        mBleDiscoveryProvider.onStop();
+    }
+
+    private class TestInjector implements Injector {
+        @Override
+        public BluetoothAdapter getBluetoothAdapter() {
+            return mBluetoothAdapter;
+        }
+    }
+
+    private ScanResult createScanResult() {
+        BluetoothDevice bluetoothDevice = mBluetoothAdapter
+                .getRemoteDevice("11:22:33:44:55:66");
+        byte[] scanRecord = new byte[] {2, 1, 6, 6, 22, 44, -2, 113, -116, 23, 2, 10, -11, 0, 0, 0,
+                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+        return new ScanResult(
+                bluetoothDevice,
+                /* eventType= */ 0,
+                /* primaryPhy= */ 0,
+                /* secondaryPhy= */ 0,
+                /* advertisingSid= */ 0,
+                -31,
+                -50,
+                /* periodicAdvertisingInterval= */ 0,
+                parseScanRecord(scanRecord),
+                1645579363003L);
+    }
+
+    private static ScanRecord parseScanRecord(byte[] bytes) {
+        Class<?> scanRecordClass = ScanRecord.class;
+        try {
+            Method method = scanRecordClass
+                    .getDeclaredMethod("parseFromBytes", byte[].class);
+            return (ScanRecord) method.invoke(null, bytes);
+        } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException
+                | InvocationTargetException e) {
+            return null;
+        }
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java
new file mode 100644
index 0000000..9152c07
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.protobuf.ByteString;
+
+import org.junit.Test;
+
+import service.proto.Cache;
+import service.proto.FastPairString.FastPairStrings;
+import service.proto.Rpcs;
+import service.proto.Rpcs.GetObservedDeviceResponse;
+
+public final class DataUtilsTest {
+    private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
+    private static final String APP_PACKAGE = "test_package";
+    private static final String APP_ACTION_URL =
+            "intent:#Intent;action=cto_be_set%3AACTION_MAGIC_PAIR;"
+                    + "package=to_be_set;"
+                    + "component=to_be_set;"
+                    + "to_be_set%3AEXTRA_COMPANION_APP="
+                    + APP_PACKAGE
+                    + ";end";
+    private static final long DEVICE_ID = 12;
+    private static final String DEVICE_NAME = "My device";
+    private static final byte[] DEVICE_PUBLIC_KEY = base16().decode("0123456789ABCDEF");
+    private static final String DEVICE_COMPANY = "Company name";
+    private static final byte[] DEVICE_IMAGE = new byte[] {0x00, 0x01, 0x10, 0x11};
+    private static final String DEVICE_IMAGE_URL = "device_image_url";
+    private static final String AUTHORITY = "com.android.test";
+    private static final String SIGNATURE_HASH = "as8dfbyu2duas7ikanvklpaclo2";
+    private static final String ACCOUNT = "test@gmail.com";
+
+    private static final String MESSAGE_INIT_NOTIFY_DESCRIPTION = "message 1";
+    private static final String MESSAGE_INIT_NOTIFY_DESCRIPTION_NO_ACCOUNT = "message 2";
+    private static final String MESSAGE_INIT_PAIR_DESCRIPTION = "message 3 %s";
+    private static final String MESSAGE_COMPANION_INSTALLED = "message 4";
+    private static final String MESSAGE_COMPANION_NOT_INSTALLED = "message 5";
+    private static final String MESSAGE_SUBSEQUENT_PAIR_DESCRIPTION = "message 6";
+    private static final String MESSAGE_RETROACTIVE_PAIR_DESCRIPTION = "message 7";
+    private static final String MESSAGE_WAIT_LAUNCH_COMPANION_APP_DESCRIPTION = "message 8";
+    private static final String MESSAGE_FAIL_CONNECT_DESCRIPTION = "message 9";
+    private static final String MESSAGE_FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION =
+            "message 10";
+    private static final String MESSAGE_ASSISTANT_HALF_SHEET_DESCRIPTION = "message 11";
+    private static final String MESSAGE_ASSISTANT_NOTIFICATION_DESCRIPTION = "message 12";
+
+    @Test
+    public void test_toScanFastPairStoreItem_withAccount() {
+        Cache.ScanFastPairStoreItem item = DataUtils.toScanFastPairStoreItem(
+                createObservedDeviceResponse(), BLUETOOTH_ADDRESS, ACCOUNT);
+        assertThat(item.getAddress()).isEqualTo(BLUETOOTH_ADDRESS);
+        assertThat(item.getActionUrl()).isEqualTo(APP_ACTION_URL);
+        assertThat(item.getDeviceName()).isEqualTo(DEVICE_NAME);
+        assertThat(item.getIconPng()).isEqualTo(ByteString.copyFrom(DEVICE_IMAGE));
+        assertThat(item.getIconFifeUrl()).isEqualTo(DEVICE_IMAGE_URL);
+        assertThat(item.getAntiSpoofingPublicKey())
+                .isEqualTo(ByteString.copyFrom(DEVICE_PUBLIC_KEY));
+
+        FastPairStrings strings = item.getFastPairStrings();
+        assertThat(strings.getTapToPairWithAccount()).isEqualTo(MESSAGE_INIT_NOTIFY_DESCRIPTION);
+        assertThat(strings.getTapToPairWithoutAccount())
+                .isEqualTo(MESSAGE_INIT_NOTIFY_DESCRIPTION_NO_ACCOUNT);
+        assertThat(strings.getInitialPairingDescription())
+                .isEqualTo(String.format(MESSAGE_INIT_PAIR_DESCRIPTION, DEVICE_NAME));
+        assertThat(strings.getPairingFinishedCompanionAppInstalled())
+                .isEqualTo(MESSAGE_COMPANION_INSTALLED);
+        assertThat(strings.getPairingFinishedCompanionAppNotInstalled())
+                .isEqualTo(MESSAGE_COMPANION_NOT_INSTALLED);
+        assertThat(strings.getSubsequentPairingDescription())
+                .isEqualTo(MESSAGE_SUBSEQUENT_PAIR_DESCRIPTION);
+        assertThat(strings.getRetroactivePairingDescription())
+                .isEqualTo(MESSAGE_RETROACTIVE_PAIR_DESCRIPTION);
+        assertThat(strings.getWaitAppLaunchDescription())
+                .isEqualTo(MESSAGE_WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
+        assertThat(strings.getPairingFailDescription())
+                .isEqualTo(MESSAGE_FAIL_CONNECT_DESCRIPTION);
+        assertThat(strings.getAssistantHalfSheetDescription())
+                .isEqualTo(MESSAGE_ASSISTANT_HALF_SHEET_DESCRIPTION);
+        assertThat(strings.getAssistantNotificationDescription())
+                .isEqualTo(MESSAGE_ASSISTANT_NOTIFICATION_DESCRIPTION);
+        assertThat(strings.getFastPairTvConnectDeviceNoAccountDescription())
+                .isEqualTo(MESSAGE_FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION);
+    }
+
+    @Test
+    public void test_toScanFastPairStoreItem_withoutAccount() {
+        Cache.ScanFastPairStoreItem item = DataUtils.toScanFastPairStoreItem(
+                createObservedDeviceResponse(), BLUETOOTH_ADDRESS, /* account= */ null);
+        FastPairStrings strings = item.getFastPairStrings();
+        assertThat(strings.getInitialPairingDescription())
+                .isEqualTo(MESSAGE_INIT_NOTIFY_DESCRIPTION_NO_ACCOUNT);
+    }
+
+    @Test
+    public void test_toString() {
+        Cache.ScanFastPairStoreItem item = DataUtils.toScanFastPairStoreItem(
+                createObservedDeviceResponse(), BLUETOOTH_ADDRESS, ACCOUNT);
+        FastPairStrings strings = item.getFastPairStrings();
+
+        assertThat(DataUtils.toString(strings))
+                .isEqualTo("FastPairStrings[tapToPairWithAccount=message 1, "
+                        + "tapToPairWithoutAccount=message 2, "
+                        + "initialPairingDescription=message 3 " + DEVICE_NAME + ", "
+                        + "pairingFinishedCompanionAppInstalled=message 4, "
+                        + "pairingFinishedCompanionAppNotInstalled=message 5, "
+                        + "subsequentPairingDescription=message 6, "
+                        + "retroactivePairingDescription=message 7, "
+                        + "waitAppLaunchDescription=message 8, "
+                        + "pairingFailDescription=message 9, "
+                        + "assistantHalfSheetDescription=message 11, "
+                        + "assistantNotificationDescription=message 12, "
+                        + "fastPairTvConnectDeviceNoAccountDescription=message 10]");
+    }
+
+    private static GetObservedDeviceResponse createObservedDeviceResponse() {
+        return GetObservedDeviceResponse.newBuilder()
+                .setDevice(
+                        Rpcs.Device.newBuilder()
+                                .setId(DEVICE_ID)
+                                .setName(DEVICE_NAME)
+                                .setAntiSpoofingKeyPair(
+                                        Rpcs.AntiSpoofingKeyPair
+                                                .newBuilder()
+                                                .setPublicKey(
+                                                        ByteString.copyFrom(DEVICE_PUBLIC_KEY)))
+                                .setIntentUri(APP_ACTION_URL)
+                                .setDataOnlyConnection(true)
+                                .setAssistantSupported(false)
+                                .setCompanionDetail(
+                                        Rpcs.CompanionAppDetails.newBuilder()
+                                                .setAuthority(AUTHORITY)
+                                                .setCertificateHash(SIGNATURE_HASH)
+                                                .build())
+                                .setCompanyName(DEVICE_COMPANY)
+                                .setImageUrl(DEVICE_IMAGE_URL))
+                .setImage(ByteString.copyFrom(DEVICE_IMAGE))
+                .setStrings(
+                        Rpcs.ObservedDeviceStrings.newBuilder()
+                                .setInitialNotificationDescription(MESSAGE_INIT_NOTIFY_DESCRIPTION)
+                                .setInitialNotificationDescriptionNoAccount(
+                                        MESSAGE_INIT_NOTIFY_DESCRIPTION_NO_ACCOUNT)
+                                .setInitialPairingDescription(MESSAGE_INIT_PAIR_DESCRIPTION)
+                                .setConnectSuccessCompanionAppInstalled(MESSAGE_COMPANION_INSTALLED)
+                                .setConnectSuccessCompanionAppNotInstalled(
+                                        MESSAGE_COMPANION_NOT_INSTALLED)
+                                .setSubsequentPairingDescription(
+                                        MESSAGE_SUBSEQUENT_PAIR_DESCRIPTION)
+                                .setRetroactivePairingDescription(
+                                        MESSAGE_RETROACTIVE_PAIR_DESCRIPTION)
+                                .setWaitLaunchCompanionAppDescription(
+                                        MESSAGE_WAIT_LAUNCH_COMPANION_APP_DESCRIPTION)
+                                .setFailConnectGoToSettingsDescription(
+                                        MESSAGE_FAIL_CONNECT_DESCRIPTION)
+                                .setAssistantSetupHalfSheet(
+                                        MESSAGE_ASSISTANT_HALF_SHEET_DESCRIPTION)
+                                .setAssistantSetupNotification(
+                                        MESSAGE_ASSISTANT_NOTIFICATION_DESCRIPTION)
+                                .setFastPairTvConnectDeviceNoAccountDescription(
+                                        MESSAGE_FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION))
+                .build();
+    }
+}