Merge "Add subsequent related db save info after pairing success" into tm-dev
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 a2b248b..cb4e6cb 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
@@ -91,6 +91,7 @@
     /** A notification ID which should be dismissed */
     public static final String EXTRA_NOTIFICATION_ID = ACTION_PREFIX + "EXTRA_NOTIFICATION_ID";
     public static final String ACTION_RESOURCES_APK = "android.nearby.SHOW_HALFSHEET";
+    public static final boolean ENFORCED_SCAN_ENABLED_VALUE = false;
 
     private static Executor sFastPairExecutor;
 
@@ -99,7 +100,7 @@
     final LocatorContextWrapper mLocatorContextWrapper;
     final IntentFilter mIntentFilter;
     final Locator mLocator;
-    private boolean mScanEnabled = false;
+    private boolean mScanEnabled = ENFORCED_SCAN_ENABLED_VALUE;
     private final BroadcastReceiver mScreenBroadcastReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -153,6 +154,7 @@
         Locator.getFromContextWrapper(mLocatorContextWrapper, FastPairCacheManager.class);
         try {
             mScanEnabled = getScanEnabledFromSettings();
+            mScanEnabled = ENFORCED_SCAN_ENABLED_VALUE;
         } catch (Settings.SettingNotFoundException e) {
             Log.w(TAG,
                     "initiate: Failed to get initial scan enabled status from Settings.", e);
@@ -413,6 +415,7 @@
             return;
         }
         mScanEnabled = scanEnabled;
+        mScanEnabled = ENFORCED_SCAN_ENABLED_VALUE;
         invalidateScan();
     }
 
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java
index 4f279a5..b8a9796 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java
@@ -31,6 +31,7 @@
 import com.android.server.nearby.common.ble.util.RangingUtils;
 import com.android.server.nearby.common.fastpair.IconUtils;
 import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -67,6 +68,15 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface ItemState {}
 
+    public DiscoveryItem(LocatorContextWrapper locatorContextWrapper,
+            Cache.StoredDiscoveryItem mStoredDiscoveryItem) {
+        this.mFastPairCacheManager =
+                locatorContextWrapper.getLocator().get(FastPairCacheManager.class);
+        this.mClock =
+            locatorContextWrapper.getLocator().get(Clock.class);
+        this.mStoredDiscoveryItem = mStoredDiscoveryItem;
+    }
+
     public DiscoveryItem(Context context, Cache.StoredDiscoveryItem mStoredDiscoveryItem) {
         this.mFastPairCacheManager = Locator.get(context, FastPairCacheManager.class);
         this.mClock = Locator.get(context, Clock.class);
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 599fe5c..6dc1af8 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -37,6 +37,7 @@
         "general-tests",
         "mts-tethering",
     ],
+    certificate: "platform",
     platform_apis: true,
     sdk_version: "module_current",
     min_sdk_version: "30",
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyFrameworkInitializerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyFrameworkInitializerTest.java
index 370bfe1..cf43cb1 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyFrameworkInitializerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyFrameworkInitializerTest.java
@@ -16,12 +16,16 @@
 
 package android.nearby.cts;
 
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
 import android.nearby.NearbyFrameworkInitializer;
 import android.os.Build;
 
 import androidx.annotation.RequiresApi;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -31,15 +35,11 @@
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class NearbyFrameworkInitializerTest {
 
-//    // TODO(b/215435710) This test cannot pass now because our test cannot access system API.
-//    // run "adb root && adb shell setenforce permissive" and uncomment testServicesRegistered,
-//    // test passes.
-//    @Test
-//    public void testServicesRegistered() {
-//        Context ctx = InstrumentationRegistry.getInstrumentation().getContext();
-//        assertNotNull( "NearbyManager not registered",
-//                ctx.getSystemService(Context.NEARBY_SERVICE));
-//    }
+    @Test
+    public void testServicesRegistered() {
+        Context ctx = InstrumentationRegistry.getInstrumentation().getContext();
+        assertThat(ctx.getSystemService(Context.NEARBY_SERVICE)).isNotNull();
+    }
 
     // registerServiceWrappers can only be called during initialization and should throw otherwise
     @Test(expected = IllegalStateException.class)
diff --git a/nearby/tests/multidevices/clients/Android.bp b/nearby/tests/multidevices/clients/Android.bp
index 5e0ca15..e3c8bb1 100644
--- a/nearby/tests/multidevices/clients/Android.bp
+++ b/nearby/tests/multidevices/clients/Android.bp
@@ -24,15 +24,12 @@
     ],
     sdk_version: "test_current",
     static_libs: [
-        "NearbyMultiDevicesClientsFastPairLiteProtos",
+        "MoblySnippetHelperLib",
+        "NearbyFastPairProviderLib",
         "androidx.test.core",
-        "error_prone_annotations",
-        "fast-pair-lite-protos",
-        "framework-annotations-lib",
         "gson-prebuilt-jar",
         "kotlin-stdlib",
         "mobly-snippet-lib",
-        "service-nearby",
     ],
 }
 
diff --git a/nearby/tests/multidevices/clients/proguard.flags b/nearby/tests/multidevices/clients/proguard.flags
index 2e34dce..fd494a8 100644
--- a/nearby/tests/multidevices/clients/proguard.flags
+++ b/nearby/tests/multidevices/clients/proguard.flags
@@ -4,7 +4,7 @@
 }
 
 # Keep simulator reflection callback.
--keep class com.android.server.nearby.common.bluetooth.fastpair.testing.** {
+-keep class android.nearby.fastpair.provider.** {
      *;
 }
 
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorSnippet.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorSnippet.kt
index a2f50b4..a03085c 100644
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorSnippet.kt
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorSnippet.kt
@@ -21,7 +21,7 @@
 import android.content.Context
 import android.os.Build
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.server.nearby.common.bluetooth.fastpair.testing.FastPairSimulator
+import android.nearby.fastpair.provider.FastPairSimulator
 import com.google.android.mobly.snippet.Snippet
 import com.google.android.mobly.snippet.rpc.AsyncRpc
 import com.google.android.mobly.snippet.rpc.Rpc
@@ -70,7 +70,9 @@
         fastPairSimulator =
             FastPairSimulator(
                 context,
-                FastPairSimulator.Options.builder(modelId)
+                FastPairSimulator.Options.builder(
+                    modelId
+                )
                     .setAdvertisingModelId(modelId)
                     .setBluetoothAddress(null)
                     .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/ProviderStatusEvents.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/ProviderStatusEvents.kt
index 20c8e85..eef4b8b 100644
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/ProviderStatusEvents.kt
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/ProviderStatusEvents.kt
@@ -16,7 +16,7 @@
 
 package android.nearby.multidevices.fastpair.provider
 
-import android.nearby.multidevices.common.postSnippetEvent
+import com.google.android.mobly.snippet.util.postSnippetEvent
 
 /** The Mobly snippet events to report to the Python side. */
 class ProviderStatusEvents(private val callbackId: String) :
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/CompanionAppUtils.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/CompanionAppUtils.kt
deleted file mode 100644
index 7ed4372..0000000
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/CompanionAppUtils.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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.multidevices.fastpair.seeker
-
-fun generateCompanionAppLaunchIntentUri(
-        companionAppPackageName: String? = null,
-        activityName: String? = null,
-        action: String? = null
-): String {
-    if (companionAppPackageName.isNullOrEmpty() || activityName.isNullOrEmpty()) {
-        return ""
-    }
-    var intentUriString = "intent:#Intent;"
-    if (!action.isNullOrEmpty()) {
-        intentUriString += "action=$action;"
-    }
-    intentUriString += "package=$companionAppPackageName;"
-    intentUriString += "component=$companionAppPackageName/$activityName;"
-    return "${intentUriString}end"
-}
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ScanCallbackEvents.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ScanCallbackEvents.kt
index 55a6b8f..5385238 100644
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ScanCallbackEvents.kt
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ScanCallbackEvents.kt
@@ -18,7 +18,7 @@
 
 import android.nearby.NearbyDevice
 import android.nearby.ScanCallback
-import android.nearby.multidevices.common.postSnippetEvent
+import com.google.android.mobly.snippet.util.postSnippetEvent
 
 /** The Mobly snippet events to report to the Python side. */
 class ScanCallbackEvents(private val callbackId: String) : ScanCallback {
diff --git a/nearby/tests/multidevices/clients/proto/Android.bp b/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp
similarity index 62%
copy from nearby/tests/multidevices/clients/proto/Android.bp
copy to nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp
index 80e09b4..dc3a919 100644
--- a/nearby/tests/multidevices/clients/proto/Android.bp
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp
@@ -16,15 +16,20 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-java_library {
-    name: "NearbyMultiDevicesClientsFastPairLiteProtos",
-    proto: {
-        type: "lite",
-        canonical_path_from_root: false,
-    },
-    sdk_version: "system_current",
-    min_sdk_version: "30",
-    srcs: ["src/*/*.proto"],
+android_library {
+    name: "NearbyFastPairProviderLib",
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    sdk_version: "test_current",
+    static_libs: [
+        "NearbyFastPairProviderLiteProtos",
+        "androidx.test.core",
+        "error_prone_annotations",
+        "fast-pair-lite-protos",
+        "framework-annotations-lib",
+        "kotlin-stdlib",
+        "service-nearby",
+    ],
 }
-
-
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/AndroidManifest.xml b/nearby/tests/multidevices/clients/test_support/fastpair_provider/AndroidManifest.xml
new file mode 100644
index 0000000..400a434
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.nearby.fastpair.provider">
+
+    <uses-feature android:name="android.hardware.bluetooth" />
+    <uses-feature android:name="android.hardware.bluetooth_le" />
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+    <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
+
+</manifest>
diff --git a/nearby/tests/multidevices/clients/proto/Android.bp b/nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/Android.bp
similarity index 90%
rename from nearby/tests/multidevices/clients/proto/Android.bp
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/Android.bp
index 80e09b4..7ae43e5 100644
--- a/nearby/tests/multidevices/clients/proto/Android.bp
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/Android.bp
@@ -17,14 +17,14 @@
 }
 
 java_library {
-    name: "NearbyMultiDevicesClientsFastPairLiteProtos",
+    name: "NearbyFastPairProviderLiteProtos",
     proto: {
         type: "lite",
         canonical_path_from_root: false,
     },
     sdk_version: "system_current",
     min_sdk_version: "30",
-    srcs: ["src/*/*.proto"],
+    srcs: ["*.proto"],
 }
 
 
diff --git a/nearby/tests/multidevices/clients/proto/src/fastpair/event_stream_protocol.proto b/nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/event_stream_protocol.proto
similarity index 94%
rename from nearby/tests/multidevices/clients/proto/src/fastpair/event_stream_protocol.proto
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/event_stream_protocol.proto
index 69ed1ea..54db34a 100644
--- a/nearby/tests/multidevices/clients/proto/src/fastpair/event_stream_protocol.proto
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/event_stream_protocol.proto
@@ -1,8 +1,8 @@
 syntax = "proto2";
 
-package android.nearby.multidevices.fastpair;
+package android.nearby.fastpair.provider;
 
-option java_package = "android.nearby.multidevices.fastpair";
+option java_package = "android.nearby.fastpair.provider";
 option java_outer_classname = "EventStreamProtocol";
 
 enum EventGroup {
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/Android.bp b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/Android.bp
new file mode 100644
index 0000000..f3eed51
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/Android.bp
@@ -0,0 +1,45 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "NearbyFastPairProviderSimulatorApp",
+    sdk_version: "test_current",
+    static_libs: ["NearbyFastPairProviderSimulatorLib"],
+    optimize: {
+        enabled: true,
+        shrink: true,
+        proguard_flags_files: ["proguard.flags"],
+    },
+}
+
+android_library {
+    name: "NearbyFastPairProviderSimulatorLib",
+    sdk_version: "test_current",
+        srcs: [
+            "src/**/*.java",
+            "src/**/*.kt",
+        ],
+        static_libs: [
+            "NearbyFastPairProviderLib",
+            "NearbyFastPairProviderLiteProtos",
+            "NearbyFastPairProviderSimulatorLiteProtos",
+            "androidx.annotation_annotation",
+            "error_prone_annotations",
+            "fast-pair-lite-protos",
+        ],
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/AndroidManifest.xml b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/AndroidManifest.xml
new file mode 100644
index 0000000..8880b11
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.nearby.fastpair.provider.simulator.app" >
+
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
+
+    <application
+        android:allowBackup="true"
+        android:label="@string/app_name" >
+        <activity
+            android:name=".MainActivity"
+            android:windowSoftInputMode="stateHidden"
+            android:screenOrientation="portrait"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proguard.flags b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proguard.flags
new file mode 100644
index 0000000..28680b3
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proguard.flags
@@ -0,0 +1,19 @@
+# Keep simulator reflection callback.
+-keep class android.nearby.fastpair.provider.** {
+     *;
+}
+
+# Keep names for easy debugging.
+-dontobfuscate
+
+# Necessary to allow debugging.
+-keepattributes *
+
+# By default, proguard leaves all classes in their original package, which
+# needlessly repeats com.google.android.apps.etc.
+-repackageclasses ""
+
+# Allows proguard to make private and protected methods and fields public as
+# part of optimization. This lets proguard inline trivial getter/setter
+# methods.
+-allowaccessmodification
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/proto/Android.bp b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proto/Android.bp
similarity index 90%
copy from nearby/tests/multidevices/clients/proto/Android.bp
copy to nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proto/Android.bp
index 80e09b4..e964800 100644
--- a/nearby/tests/multidevices/clients/proto/Android.bp
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proto/Android.bp
@@ -17,14 +17,14 @@
 }
 
 java_library {
-    name: "NearbyMultiDevicesClientsFastPairLiteProtos",
+    name: "NearbyFastPairProviderSimulatorLiteProtos",
     proto: {
         type: "lite",
         canonical_path_from_root: false,
     },
     sdk_version: "system_current",
     min_sdk_version: "30",
-    srcs: ["src/*/*.proto"],
+    srcs: ["*.proto"],
 }
 
 
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proto/simulator_stream_protocol.proto b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proto/simulator_stream_protocol.proto
new file mode 100644
index 0000000..9b17fda
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proto/simulator_stream_protocol.proto
@@ -0,0 +1,110 @@
+syntax = "proto2";
+
+package android.nearby.fastpair.provider.simulator;
+
+option java_package = "android.nearby.fastpair.provider.simulator";
+option java_outer_classname = "SimulatorStreamProtocol";
+
+// Used by remote devices to control simulator behaviors.
+message Command {
+  // Type of this command.
+  required Code code = 1;
+
+  // Required for SHOW_BATTERY.
+  optional BatteryInfo battery_info = 2;
+
+  enum Code {
+    // Request for simulator's acknowledge message.
+    POLLING = 0;
+
+    // Reset and clear bluetooth state.
+    RESET = 1;
+
+    // Present battery information in the advertisement.
+    SHOW_BATTERY = 2;
+
+    // Remove battery information in the advertisement.
+    HIDE_BATTERY = 3;
+
+    // Request for BR/EDR address.
+    REQUEST_BLUETOOTH_ADDRESS_PUBLIC = 4;
+
+    // Request for BLE address.
+    REQUEST_BLUETOOTH_ADDRESS_BLE = 5;
+
+    // Request for account key.
+    REQUEST_ACCOUNT_KEY = 6;
+  }
+
+  // Battery information for true wireless headsets.
+  // https://devsite.googleplex.com/nearby/fast-pair/early-access/spec#BatteryNotification
+  message BatteryInfo {
+    // Show or hide the battery UI notification.
+    optional bool suppress_notification = 1;
+    repeated BatteryValue battery_values = 2;
+
+    // Advertised battery level data.
+    message BatteryValue {
+      // The charging flag.
+      required bool charging = 1;
+
+      // Battery level from 0 to 100.
+      required uint32 level = 2;
+    }
+  }
+}
+
+// Notify the remote devices when states are changed or response the command on
+// the simulator.
+message Event {
+  // Type of this event.
+  required Code code = 1;
+
+  // Required for BLUETOOTH_STATE_BOND.
+  optional int32 bond_state = 2;
+
+  // Required for BLUETOOTH_STATE_CONNECTION.
+  optional int32 connection_state = 3;
+
+  // Required for BLUETOOTH_STATE_SCAN_MODE.
+  optional int32 scan_mode = 4;
+
+  // Required for BLUETOOTH_ADDRESS_PUBLIC.
+  optional string public_address = 5;
+
+  // Required for BLUETOOTH_ADDRESS_BLE.
+  optional string ble_address = 6;
+
+  // Required for BLUETOOTH_ALIAS_NAME.
+  optional string alias_name = 7;
+
+  // Required for REQUEST_ACCOUNT_KEY.
+  optional bytes account_key = 8;
+
+  enum Code {
+    // Response the polling.
+    ACKNOWLEDGE = 0;
+
+    // Notify the event android.bluetooth.device.action.BOND_STATE_CHANGED
+    BLUETOOTH_STATE_BOND = 1;
+
+    // Notify the event
+    // android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED
+    BLUETOOTH_STATE_CONNECTION = 2;
+
+    // Notify the event android.bluetooth.adapter.action.SCAN_MODE_CHANGED
+    BLUETOOTH_STATE_SCAN_MODE = 3;
+
+    // Notify the current BR/EDR address
+    BLUETOOTH_ADDRESS_PUBLIC = 4;
+
+    // Notify the current BLE address
+    BLUETOOTH_ADDRESS_BLE = 5;
+
+    // Notify the event android.bluetooth.device.action.ALIAS_CHANGED
+    BLUETOOTH_ALIAS_NAME = 6;
+
+    // Response the REQUEST_ACCOUNT_KEY.
+    ACCOUNT_KEY = 7;
+  }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/layout/activity_main.xml b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/layout/activity_main.xml
new file mode 100644
index 0000000..b7e85eb
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/layout/activity_main.xml
@@ -0,0 +1,190 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:layout_margin="16dp"
+    android:keepScreenOn="true"
+    tools:context=".MainActivity">
+
+    <TextView
+        android:id="@+id/bluetooth_address_text_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textSize="14dp"
+        android:textStyle="bold"
+        android:padding="8dp"/>
+
+    <TextView
+        android:id="@+id/device_name_text_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textSize="14dp"
+        android:textStyle="bold"
+        android:padding="8dp"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="8dp"
+        android:orientation="horizontal">
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textSize="14dp"
+                android:textStyle="bold"
+                android:text="Model ID:"/>
+            <Spinner
+                android:id="@+id/model_id_spinner"
+                android:textSize="14dp"
+                android:textStyle="bold"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1" />
+        <TextView
+            android:id="@+id/tx_power_text_view"
+            android:layout_width="0dp"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:textSize="14dp"
+            android:textStyle="bold" />
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/anti_spoofing_private_key_text_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textSize="14dp"
+        android:textStyle="bold"
+        android:padding="8dp"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+        <TextView
+            android:id="@+id/is_advertising_text_view"
+            android:layout_width="0dp"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:textSize="14dp"
+            android:textStyle="bold"
+            android:padding="8dp"/>
+        <TextView
+            android:id="@+id/scan_mode_text_view"
+            android:layout_width="0dp"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:textSize="14dp"
+            android:textStyle="bold"
+            android:padding="8dp"/>
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/remote_device_text_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textSize="14dp"
+        android:textStyle="bold"
+        android:padding="8dp"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+        <TextView
+            android:id="@+id/is_paired_text_view"
+            android:layout_width="0dp"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:textSize="14dp"
+            android:textStyle="bold"
+            android:padding="8dp"/>
+        <TextView
+            android:id="@+id/is_connected_text_view"
+            android:layout_width="0dp"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:textSize="14dp"
+            android:textStyle="bold"
+            android:padding="8dp"/>
+    </LinearLayout>
+
+    <Button
+        android:id="@+id/reset_button"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="Reset"
+        android:onClick="onResetButtonClicked"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:layout_marginBottom="8dp"
+        android:orientation="horizontal"
+        android:layout_gravity="center_vertical">
+
+      <Spinner
+          android:id="@+id/event_stream_spinner"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"/>
+
+      <Button
+          android:id="@+id/send_event_message_button"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:text="Send Event Message"
+          android:onClick="onSendEventStreamMessageButtonClicked"/>
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="8dp"
+        android:orientation="horizontal"
+        android:layout_gravity="center_vertical">
+        <Switch
+            android:id="@+id/fail_switch"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Force Fail" />
+        <Switch
+            android:id="@+id/app_launch_switch"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Trigger app launch"
+            android:paddingLeft="8dp"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/adv_options"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:layout_marginBottom="8dp"
+        android:orientation="horizontal"
+        android:layout_gravity="center_vertical">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginRight="8dp"
+            android:textColor="@android:color/black"
+            android:text="adv options"/>
+
+        <Spinner
+            android:id="@+id/adv_option_spinner"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
+
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/text_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="bottom"
+        android:scrollbars="vertical"/>
+</LinearLayout>
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/layout/user_input_dialog.xml b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/layout/user_input_dialog.xml
new file mode 100644
index 0000000..980b057
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/layout/user_input_dialog.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="16dp">
+
+  <EditText
+      android:id="@+id/userInputDialog"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:hint="@string/firmware_input_hint"
+      android:inputType="text" />
+
+</LinearLayout>
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/menu/menu.xml b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/menu/menu.xml
new file mode 100644
index 0000000..f225522
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/menu/menu.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:context=".MainActivity">
+
+  <item
+      android:id="@+id/sign_out_menu_item"
+      android:title="Sign out"/>
+  <item
+      android:id="@+id/reset_account_keys_menu_item"
+      android:title="Reset Account Keys"/>
+  <item
+      android:id="@+id/reset_device_name_menu_item"
+      android:title="Reset Device Name"/>
+  <item
+    android:id="@+id/set_firmware_version"
+    android:title="Set Firmware Version"/>
+  <item
+      android:id="@+id/set_simulator_capability"
+      android:title="Set Simulator Capability"/>
+  <item
+    android:id="@+id/use_new_gatt_characteristics_id"
+    android:checkable="true"
+    android:checked="false"
+    android:title="Use new GATT characteristics id"/>
+</menu>
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/values/dimens.xml b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/values/dimens.xml
new file mode 100644
index 0000000..47c8224
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/values/dimens.xml
@@ -0,0 +1,5 @@
+<resources>
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/values/strings.xml b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/values/strings.xml
new file mode 100644
index 0000000..5123038
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/values/strings.xml
@@ -0,0 +1,31 @@
+<resources>
+    <string name="app_name">Fast Pair Provider Simulator</string>
+    <string-array name="adv_options">
+        <item>0: No battery info</item>
+        <item>1: Show L(⬆) + R(⬆) + C(⬆)</item>
+        <item>2: Show L + R + C(unknown)</item>
+        <item>3: Show L(low 10) + R(low 9) + C(low 25)</item>
+        <item>4: Suppress battery w/o level changes</item>
+        <item>5: Suppress L(low 10) + R(11) + C</item>
+        <item>6: Suppress L(low ⬆) + R(low ⬆) + C(low 10)</item>
+        <item>7: Suppress L(low ⬆) + R(low ⬆) + C(low ⬆)</item>
+        <item>8: Show subsequent pairing notification</item>
+        <item>9: Suppress subsequent pairing notification</item>
+    </string-array>
+    <string-array name="event_stream_options">
+        <item>OHD event</item>
+        <item>Log event</item>
+        <item>Battery event</item>
+    </string-array>
+    <string name="firmware_dialog_title">Firmware version number</string>
+    <string name="firmware_input_hint">Type in version number</string>
+    <string name="passkey_dialog_title">Passkey needed</string>
+    <string name="passkey_input_hint">Type in passkey</string>
+    <!-- Passkey confirmation dialog title. [CHAR_LIMIT=NONE]-->
+    <string name="confirm_passkey">Confirm passkey</string>
+    <string name="model_id_progress_title">Get models from server</string>
+
+    <!-- Fast Pair Simulator: pair one device only. -->
+    <string name="fast_pair_simulator" translatable="false">Fast Pair Simulator</string>
+
+</resources>
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/AppLogger.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/AppLogger.java
new file mode 100644
index 0000000..befc64b
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/AppLogger.java
@@ -0,0 +1,44 @@
+/*
+ * 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.fastpair.provider.simulator.app;
+
+import android.util.Log;
+
+import com.google.errorprone.annotations.FormatMethod;
+
+/** Sends log to logcat with TAG. */
+public class AppLogger {
+    private static final String TAG = "FastPairSimulator";
+
+    @FormatMethod
+    public static void log(String message, Object... objects) {
+        Log.i(TAG, String.format(message, objects));
+    }
+
+    @FormatMethod
+    public static void warning(String message, Object... objects) {
+        Log.w(TAG, String.format(message, objects));
+    }
+
+    @FormatMethod
+    public static void error(String message, Object... objects) {
+        Log.e(TAG, String.format(message, objects));
+    }
+
+    private AppLogger() {
+    }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/BluetoothController.kt b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/BluetoothController.kt
new file mode 100644
index 0000000..ed04eae
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/BluetoothController.kt
@@ -0,0 +1,286 @@
+/*
+ * 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.fastpair.provider.simulator.app
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothManager
+import android.bluetooth.BluetoothProfile
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.nearby.fastpair.provider.FastPairSimulator
+import android.nearby.fastpair.provider.simulator.app.AppLogger.*
+import android.nearby.fastpair.provider.simulator.testing.Reflect
+import android.nearby.fastpair.provider.simulator.testing.ReflectionException
+import android.os.SystemClock
+import android.provider.Settings
+
+/** Controls the local Bluetooth adapter for Fast Pair testing. */
+class BluetoothController(
+    private val context: Context,
+    private val listener: EventListener,
+) : BroadcastReceiver() {
+    private val bluetoothAdapter: BluetoothAdapter =
+        (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter!!
+    private var remoteDevice: BluetoothDevice? = null
+    private var remoteDeviceConnectionState: Int = BluetoothAdapter.STATE_DISCONNECTED
+    private var a2dpSinkProxy: BluetoothProfile? = null
+
+    /** Turns on the local Bluetooth adapter */
+    fun enableBluetooth() {
+        if (!bluetoothAdapter.isEnabled) {
+            bluetoothAdapter.enable()
+            waitForBluetoothState(BluetoothAdapter.STATE_ON)
+        }
+    }
+
+    /**
+     * Sets the Input/Output capability of the device for both classic Bluetooth and BLE operations.
+     * Note: In order to let changes take effect, this method will make sure the Bluetooth stack is
+     * restarted by blocking calling thread.
+     *
+     * @param ioCapabilityClassic One of {@link #IO_CAPABILITY_IO}, {@link #IO_CAPABILITY_NONE},
+     * ```
+     *     {@link #IO_CAPABILITY_KBDISP} or more in {@link BluetoothAdapter}.
+     * @param ioCapabilityBLE
+     * ```
+     * One of {@link #IO_CAPABILITY_IO}, {@link #IO_CAPABILITY_NONE}, {@link
+     * ```
+     *     #IO_CAPABILITY_KBDISP} or more in {@link BluetoothAdapter}.
+     * ```
+     */
+    fun setIoCapability(ioCapabilityClassic: Int, ioCapabilityBLE: Int) {
+        try {
+            Reflect.on(bluetoothAdapter)
+                .withMethod("setIoCapability", Int::class.javaPrimitiveType)[
+                    ioCapabilityClassic]
+        } catch (e: ReflectionException) {
+            warning("Error setIoCapability to %s: %s", ioCapabilityClassic, e)
+        }
+        try {
+            Reflect.on(bluetoothAdapter)
+                .withMethod("setLeIoCapability", Int::class.javaPrimitiveType)[
+                    ioCapabilityBLE]
+        } catch (e: ReflectionException) {
+            warning("Error setLeIoCapability to %s: %s", ioCapabilityBLE, e)
+        }
+
+        // Toggling airplane mode on/off to restart Bluetooth stack and reset the BLE.
+        // Since it also increases reliability, we will do so even if ReflectionException is caught.
+        try {
+            Settings.Global.putInt(
+                context.contentResolver,
+                Settings.Global.AIRPLANE_MODE_ON,
+                TURN_AIRPLANE_MODE_ON
+            )
+        } catch (expectedOnNonCustomAndroid: SecurityException) {
+            warning("Requires custom Android to toggle airplane mode")
+            // Fall back to turn off Bluetooth.
+            bluetoothAdapter.disable()
+        }
+        waitForBluetoothState(BluetoothAdapter.STATE_OFF)
+        try {
+            Settings.Global.putInt(
+                context.contentResolver,
+                Settings.Global.AIRPLANE_MODE_ON,
+                TURN_AIRPLANE_MODE_OFF
+            )
+        } catch (expectedOnNonCustomAndroid: SecurityException) {
+            error("SecurityException while toggled airplane mode.")
+        } finally {
+            // Double confirm that Bluetooth is turned on.
+            bluetoothAdapter.enable()
+        }
+        waitForBluetoothState(BluetoothAdapter.STATE_ON)
+    }
+
+    /** Registers this Bluetooth state change receiver. */
+    fun registerBluetoothStateReceiver() {
+        val bondStateFilter =
+            IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED).apply {
+                addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
+                addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)
+            }
+        context.registerReceiver(
+            this,
+            bondStateFilter,
+            /* broadcastPermission= */ null,
+            /* scheduler= */ null
+        )
+    }
+
+    /** Unregisters this Bluetooth state change receiver. */
+    fun unregisterBluetoothStateReceiver() {
+        context.unregisterReceiver(this)
+    }
+
+    /** Clears current remote device. */
+    fun clearRemoteDevice() {
+        remoteDevice = null
+    }
+
+    /** Gets current remote device. */
+    fun getRemoteDevice(): BluetoothDevice? = remoteDevice
+
+    /** Gets current remote device as string. */
+    fun getRemoteDeviceAsString(): String = remoteDevice?.remoteDeviceToString() ?: "none"
+
+    /** Connects the Bluetooth A2DP sink profile service. */
+    fun connectA2DPSinkProfile() {
+        // Get the A2DP proxy before continuing with initialization.
+        bluetoothAdapter.getProfileProxy(
+            context,
+            object : BluetoothProfile.ServiceListener {
+                override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
+                    // When Bluetooth turns off and then on again, this is called again. But we only care
+                    // the first time. There doesn't seem to be a way to unregister our listener.
+                    if (a2dpSinkProxy == null) {
+                        a2dpSinkProxy = proxy
+                        listener.onA2DPSinkProfileConnected()
+                    }
+                }
+
+                override fun onServiceDisconnected(profile: Int) {}
+            },
+            BLUETOOTH_PROFILE_A2DP_SINK
+        )
+    }
+
+    /** Get the current Bluetooth scan mode of the local Bluetooth adapter. */
+    fun getScanMode(): Int = bluetoothAdapter.scanMode
+
+    /** Return true if the remote device is connected to the local adapter. */
+    fun isConnected(): Boolean = remoteDeviceConnectionState == BluetoothAdapter.STATE_CONNECTED
+
+    /** Return true if the remote device is bonded (paired) to the local adapter. */
+    fun isPaired(): Boolean = bluetoothAdapter.bondedDevices.contains(remoteDevice)
+
+    /** Gets the A2DP sink profile proxy. */
+    fun getA2DPSinkProfileProxy(): BluetoothProfile? = a2dpSinkProxy
+
+    /**
+     * Callback method for receiving Intent broadcast of Bluetooth state.
+     *
+     * See [BroadcastReceiver#onReceive].
+     *
+     * @param context the Context in which the receiver is running.
+     * @param intent the Intent being received.
+     */
+    override fun onReceive(context: Context, intent: Intent) {
+        log("BluetoothController received intent, action=%s", intent.action)
+
+        when (intent.action) {
+            BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {
+                // After a device starts bonding, we only pay attention to intents about that device.
+                val device =
+                    intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
+                val bondState =
+                    intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)
+                remoteDevice =
+                    when (bondState) {
+                        BluetoothDevice.BOND_BONDING, BluetoothDevice.BOND_BONDED -> device
+                        BluetoothDevice.BOND_NONE -> null
+                        else -> remoteDevice
+                    }
+                log(
+                    "ACTION_BOND_STATE_CHANGED, the bound state of the remote device (%s) change to %s.",
+                    remoteDevice?.remoteDeviceToString(),
+                    bondState.bondStateToString()
+                )
+                listener.onBondStateChanged(bondState)
+            }
+            BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED -> {
+                remoteDeviceConnectionState =
+                    intent.getIntExtra(
+                        BluetoothAdapter.EXTRA_CONNECTION_STATE,
+                        BluetoothAdapter.STATE_DISCONNECTED
+                    )
+                log(
+                    "ACTION_CONNECTION_STATE_CHANGED, the new connectionState: %s",
+                    remoteDeviceConnectionState
+                )
+                listener.onConnectionStateChanged(remoteDeviceConnectionState)
+            }
+            BluetoothAdapter.ACTION_SCAN_MODE_CHANGED -> {
+                val scanMode =
+                    intent.getIntExtra(
+                        BluetoothAdapter.EXTRA_SCAN_MODE,
+                        BluetoothAdapter.SCAN_MODE_NONE
+                    )
+                log(
+                    "ACTION_SCAN_MODE_CHANGED, the new scanMode: %s",
+                    FastPairSimulator.scanModeToString(scanMode)
+                )
+                listener.onScanModeChange(scanMode)
+            }
+            else -> {}
+        }
+    }
+
+    private fun waitForBluetoothState(state: Int) {
+        while (bluetoothAdapter.state != state) {
+            SystemClock.sleep(1000)
+        }
+    }
+
+    private fun BluetoothDevice.remoteDeviceToString(): String = "${this.name}-${this.address}"
+
+    private fun Int.bondStateToString(): String =
+        when (this) {
+            BluetoothDevice.BOND_NONE -> "BOND_NONE"
+            BluetoothDevice.BOND_BONDING -> "BOND_BONDING"
+            BluetoothDevice.BOND_BONDED -> "BOND_BONDED"
+            else -> "BOND_ERROR"
+        }
+
+    /** Interface for listening the events from Bluetooth controller. */
+    interface EventListener {
+        /** The callback for the first onServiceConnected of A2DP sink profile. */
+        fun onA2DPSinkProfileConnected()
+
+        /**
+         * Reports the current bond state of the remote device.
+         *
+         * @param bondState the bond state of the remote device.
+         */
+        fun onBondStateChanged(bondState: Int)
+
+        /**
+         * Reports the current connection state of the remote device.
+         *
+         * @param connectionState the bond state of the remote device.
+         */
+        fun onConnectionStateChanged(connectionState: Int)
+
+        /**
+         * Reports the current scan mode of the local Adapter.
+         *
+         * @param mode the current scan mode of the local Adapter.
+         */
+        fun onScanModeChange(mode: Int)
+    }
+
+    companion object {
+        /** Hidden SystemApi field in [BluetoothProfile] interface. */
+        private const val BLUETOOTH_PROFILE_A2DP_SINK = 11
+
+        private const val TURN_AIRPLANE_MODE_OFF = 0
+        private const val TURN_AIRPLANE_MODE_ON = 1
+    }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/FutureCallbackWrapper.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/FutureCallbackWrapper.java
new file mode 100644
index 0000000..4db8560
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/FutureCallbackWrapper.java
@@ -0,0 +1,69 @@
+/*
+ * 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.fastpair.provider.simulator.app;
+
+import android.util.Log;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+/** Wrapper for {@link FutureCallback} to prevent the memory linkage. */
+public abstract class FutureCallbackWrapper<T> implements FutureCallback<T> {
+    private static final String TAG = FutureCallback.class.getSimpleName();
+
+    public static FutureCallbackWrapper<Void> createRegisterCallback(MainActivity activity) {
+        String id = activity.mRemoteDeviceId;
+        return new FutureCallbackWrapper<Void>() {
+            @Override
+            public void onSuccess(Void result) {
+                Log.d(TAG, String.format("%s was registered", id));
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                Log.w(TAG, String.format("Failed to register %s", id), t);
+            }
+        };
+    }
+
+    public static FutureCallbackWrapper<Void> createDefaultIOCallback(MainActivity activity) {
+        String id = activity.mRemoteDeviceId;
+        return new FutureCallbackWrapper<Void>() {
+            @Override
+            public void onSuccess(Void result) {
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                Log.w(TAG, String.format("IO stream error on %s", id), t);
+            }
+        };
+    }
+
+    public static FutureCallbackWrapper<Void> createDestroyCallback() {
+        return new FutureCallbackWrapper<Void>() {
+            @Override
+            public void onSuccess(Void result) {
+                Log.d(TAG, "remote devices manager is destroyed");
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                Log.w(TAG, "Failed to destroy remote devices manager", t);
+            }
+        };
+    }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/MainActivity.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/MainActivity.java
new file mode 100644
index 0000000..9252173
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/MainActivity.java
@@ -0,0 +1,1043 @@
+/*
+ * 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.fastpair.provider.simulator.app;
+
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.BLUETOOTH_STATE_BOND;
+import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.BLUETOOTH_STATE_CONNECTION;
+import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.BLUETOOTH_STATE_SCAN_MODE;
+import static android.nearby.fastpair.provider.simulator.app.AppLogger.log;
+import static android.nearby.fastpair.provider.simulator.app.AppLogger.warning;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.io.BaseEncoding.base64;
+
+import android.Manifest.permission;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.le.AdvertiseSettings;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.nearby.fastpair.provider.EventStreamProtocol.EventGroup;
+import android.nearby.fastpair.provider.FastPairSimulator;
+import android.nearby.fastpair.provider.FastPairSimulator.BatteryValue;
+import android.nearby.fastpair.provider.FastPairSimulator.KeyInputCallback;
+import android.nearby.fastpair.provider.FastPairSimulator.PasskeyEventCallback;
+import android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event;
+import android.nearby.fastpair.provider.simulator.testing.RemoteDevice;
+import android.nearby.fastpair.provider.simulator.testing.RemoteDevicesManager;
+import android.nearby.fastpair.provider.simulator.testing.StreamIOHandlerFactory;
+import android.nearby.fastpair.provider.utils.Logger;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.InputType;
+import android.text.TextUtils;
+import android.text.method.ScrollingMovementMethod;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.Switch;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.core.util.Consumer;
+
+import com.google.common.base.Ascii;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.FormatMethod;
+import com.google.protobuf.ByteString;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.Executors;
+
+import service.proto.Rpcs.AntiSpoofingKeyPair;
+import service.proto.Rpcs.Device;
+import service.proto.Rpcs.DeviceType;
+
+/**
+ * Simulates a Fast Pair device (e.g. a headset).
+ *
+ * <p>See README in this directory, and {http://go/fast-pair-spec}.
+ */
+@SuppressLint("SetTextI18n")
+public class MainActivity extends Activity {
+    /** Device has a display and the ability to input Yes/No. */
+    private static final int IO_CAPABILITY_IO = 1;
+
+    /** Device only has a keyboard for entry but no display. */
+    private static final int IO_CAPABILITY_IN = 2;
+
+    /** Device has no Input or Output capability. */
+    private static final int IO_CAPABILITY_NONE = 3;
+
+    /** Device has a display and a full keyboard. */
+    private static final int IO_CAPABILITY_KBDISP = 4;
+
+    private static final String SHARED_PREFS_NAME =
+            "android.nearby.fastpair.provider.simulator.app";
+    private static final String EXTRA_MODEL_ID = "MODEL_ID";
+    private static final String EXTRA_BLUETOOTH_ADDRESS = "BLUETOOTH_ADDRESS";
+    private static final String EXTRA_TX_POWER_LEVEL = "TX_POWER_LEVEL";
+    private static final String EXTRA_FIRMWARE_VERSION = "FIRMWARE_VERSION";
+    private static final String EXTRA_SUPPORT_DYNAMIC_SIZE = "SUPPORT_DYNAMIC_SIZE";
+    private static final String EXTRA_USE_RANDOM_SALT_FOR_ACCOUNT_KEY_ROTATION =
+            "USE_RANDOM_SALT_FOR_ACCOUNT_KEY_ROTATION";
+    private static final String EXTRA_REMOTE_DEVICE_ID = "REMOTE_DEVICE_ID";
+    private static final String EXTRA_USE_NEW_GATT_CHARACTERISTICS_ID =
+            "USE_NEW_GATT_CHARACTERISTICS_ID";
+    public static final String EXTRA_REMOVE_ALL_DEVICES_DURING_PAIRING =
+            "REMOVE_ALL_DEVICES_DURING_PAIRING";
+    private static final String KEY_ACCOUNT_NAME = "ACCOUNT_NAME";
+    private static final String[] PERMISSIONS =
+            new String[]{permission.BLUETOOTH, permission.BLUETOOTH_ADMIN, permission.GET_ACCOUNTS};
+    private static final int LIGHT_GREEN = 0xFFC8FFC8;
+    private static final String ANTI_SPOOFING_KEY_LABEL = "Anti-spoofing key";
+
+    private static final ImmutableMap<String, String> ANTI_SPOOFING_PRIVATE_KEY_MAP =
+            new ImmutableMap.Builder<String, String>()
+                    .put("361A2E", "/1rMqyJRGeOK6vkTNgM70xrytxdKg14mNQkITeusK20=")
+                    .put("00000D", "03/MAmUPTGNsN+2iA/1xASXoPplDh3Ha5/lk2JgEBx4=")
+                    .put("00000C", "Cbj9eCJrTdDgSYxLkqtfADQi86vIaMvxJsQ298sZYWE=")
+                    // BLE only devices
+                    .put("49426D", "I5QFOJW0WWFgKKZiwGchuseXsq/p9RN/aYtNsGEVGT0=")
+                    .put("01E5CE", "FbHt8STpHJDd4zFQFjimh4Zt7IU94U28MOEIXgUEeCw=")
+                    .put("8D13B9", "mv++LcJB1n0mbLNGWlXCv/8Gb6aldctrJC4/Ma/Q3Rg=")
+                    .put("9AB0F6", "9eKQNwJUr5vCg0c8rtOXkJcWTAsBmmvEKSgXIqAd50Q=")
+                    // Android Auto
+                    .put("8E083D", "hGQeREDKM/H1834zWMmTIe0Ap4Zl5igThgE62OtdcKA=")
+                    .buildOrThrow();
+
+    private static final Uri REMOTE_DEVICE_INPUT_STREAM_URI =
+            Uri.fromFile(new File("/data/local/nearby/tmp/read.pipe"));
+
+    private static final Uri REMOTE_DEVICE_OUTPUT_STREAM_URI =
+            Uri.fromFile(new File("/data/local/nearby/tmp/write.pipe"));
+
+    private static final String MODEL_ID_DEFAULT = "00000C";
+
+    private static final String MODEL_ID_APP_LAUNCH = "60EB56";
+
+    private static final int MODEL_ID_LENGTH = 6;
+
+    private BluetoothController mBluetoothController;
+    private final BluetoothController.EventListener mEventListener =
+            new BluetoothController.EventListener() {
+
+                @Override
+                public void onBondStateChanged(int bondState) {
+                    sendEventToRemoteDevice(
+                            Event.newBuilder().setCode(BLUETOOTH_STATE_BOND).setBondState(
+                                    bondState));
+                    updateStatusView();
+                }
+
+                @Override
+                public void onConnectionStateChanged(int connectionState) {
+                    sendEventToRemoteDevice(
+                            Event.newBuilder()
+                                    .setCode(BLUETOOTH_STATE_CONNECTION)
+                                    .setConnectionState(connectionState));
+                    updateStatusView();
+                }
+
+                @Override
+                public void onScanModeChange(int mode) {
+                    sendEventToRemoteDevice(
+                            Event.newBuilder().setCode(BLUETOOTH_STATE_SCAN_MODE).setScanMode(
+                                    mode));
+                    updateStatusView();
+                }
+
+                @Override
+                public void onA2DPSinkProfileConnected() {
+                    reset();
+                }
+            };
+
+    @Nullable
+    private FastPairSimulator mFastPairSimulator;
+    @Nullable
+    private AlertDialog mInputPasskeyDialog;
+    private Switch mFailSwitch;
+    private Switch mAppLaunchSwitch;
+    private Spinner mAdvOptionSpinner;
+    private Spinner mEventStreamSpinner;
+    private EventGroup mEventGroup;
+    private SharedPreferences mSharedPreferences;
+    private Spinner mModelIdSpinner;
+    private final RemoteDevicesManager mRemoteDevicesManager = new RemoteDevicesManager();
+    @Nullable
+    private RemoteDeviceListener mInputStreamListener;
+    @Nullable
+    String mRemoteDeviceId;
+    private final Map<String, Device> mModelsMap = new LinkedHashMap<>();
+    private boolean mRemoveAllDevicesDuringPairing = true;
+
+    void sendEventToRemoteDevice(Event.Builder eventBuilder) {
+        if (mRemoteDeviceId == null) {
+            return;
+        }
+
+        log("Send data to output stream: %s", eventBuilder.getCode().getNumber());
+        mRemoteDevicesManager.writeDataToRemoteDevice(
+                mRemoteDeviceId,
+                eventBuilder.build().toByteString(),
+                FutureCallbackWrapper.createDefaultIOCallback(this));
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.activity_main);
+
+        mSharedPreferences = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
+
+        mRemoveAllDevicesDuringPairing =
+                getIntent().getBooleanExtra(EXTRA_REMOVE_ALL_DEVICES_DURING_PAIRING, true);
+
+        mFailSwitch = findViewById(R.id.fail_switch);
+        mFailSwitch.setOnCheckedChangeListener((CompoundButton buttonView, boolean isChecked) -> {
+            if (mFastPairSimulator != null) {
+                mFastPairSimulator.setShouldFailPairing(isChecked);
+            }
+        });
+
+        mAppLaunchSwitch = findViewById(R.id.app_launch_switch);
+        mAppLaunchSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> reset());
+
+        mAdvOptionSpinner = findViewById(R.id.adv_option_spinner);
+        mEventStreamSpinner = findViewById(R.id.event_stream_spinner);
+        ArrayAdapter<CharSequence> advOptionAdapter =
+                ArrayAdapter.createFromResource(
+                        this, R.array.adv_options, android.R.layout.simple_spinner_item);
+        ArrayAdapter<CharSequence> eventStreamAdapter =
+                ArrayAdapter.createFromResource(
+                        this, R.array.event_stream_options, android.R.layout.simple_spinner_item);
+        advOptionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        mAdvOptionSpinner.setAdapter(advOptionAdapter);
+        mEventStreamSpinner.setAdapter(eventStreamAdapter);
+        mAdvOptionSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(AdapterView<?> adapterView, View view, int position,
+                    long id) {
+                startAdvertisingBatteryInformationBasedOnOption(position);
+            }
+
+            @Override
+            public void onNothingSelected(AdapterView<?> adapterView) {
+            }
+        });
+        mEventStreamSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(AdapterView<?> parent, View view, int position,
+                    long id) {
+                switch (EventGroup.forNumber(position + 1)) {
+                    case BLUETOOTH:
+                        mEventGroup = EventGroup.BLUETOOTH;
+                        break;
+                    case LOGGING:
+                        mEventGroup = EventGroup.LOGGING;
+                        break;
+                    case DEVICE:
+                        mEventGroup = EventGroup.DEVICE;
+                        break;
+                    default:
+                        // fall through
+                }
+            }
+
+            @Override
+            public void onNothingSelected(AdapterView<?> parent) {
+            }
+        });
+        setupModelIdSpinner();
+        setupRemoteDevices();
+        if (checkPermissions(PERMISSIONS)) {
+            mBluetoothController = new BluetoothController(this, mEventListener);
+            mBluetoothController.registerBluetoothStateReceiver();
+            mBluetoothController.enableBluetooth();
+            mBluetoothController.connectA2DPSinkProfile();
+
+            if (mSharedPreferences.getString(KEY_ACCOUNT_NAME, "").isEmpty()) {
+                putFixedModelLocal();
+                resetModelIdSpinner();
+                reset();
+            }
+        } else {
+            requestPermissions(PERMISSIONS, 0 /* requestCode */);
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.menu, menu);
+        menu.findItem(R.id.use_new_gatt_characteristics_id).setChecked(
+                getFromIntentOrPrefs(
+                        EXTRA_USE_NEW_GATT_CHARACTERISTICS_ID, /* defaultValue= */ false));
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == R.id.sign_out_menu_item) {
+            recreate();
+            return true;
+        } else if (item.getItemId() == R.id.reset_account_keys_menu_item) {
+            resetAccountKeys();
+            return true;
+        } else if (item.getItemId() == R.id.reset_device_name_menu_item) {
+            resetDeviceName();
+            return true;
+        } else if (item.getItemId() == R.id.set_firmware_version) {
+            setFirmware();
+            return true;
+        } else if (item.getItemId() == R.id.set_simulator_capability) {
+            setSimulatorCapability();
+            return true;
+        } else if (item.getItemId() == R.id.use_new_gatt_characteristics_id) {
+            if (!item.isChecked()) {
+                item.setChecked(true);
+                mSharedPreferences.edit()
+                        .putBoolean(EXTRA_USE_NEW_GATT_CHARACTERISTICS_ID, true).apply();
+            } else {
+                item.setChecked(false);
+                mSharedPreferences.edit()
+                        .putBoolean(EXTRA_USE_NEW_GATT_CHARACTERISTICS_ID, false).apply();
+            }
+            reset();
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    private void setFirmware() {
+        View firmwareInputView =
+                LayoutInflater.from(getApplicationContext()).inflate(R.layout.user_input_dialog,
+                        null);
+        EditText userInputDialogEditText = firmwareInputView.findViewById(R.id.userInputDialog);
+        new AlertDialog.Builder(MainActivity.this)
+                .setView(firmwareInputView)
+                .setCancelable(false)
+                .setPositiveButton(android.R.string.ok, (dialogBox, id) -> {
+                    String input = userInputDialogEditText.getText().toString();
+                    mSharedPreferences.edit().putString(EXTRA_FIRMWARE_VERSION,
+                            input).apply();
+                    reset();
+                })
+                .setNegativeButton(android.R.string.cancel, null)
+                .setTitle(R.string.firmware_dialog_title)
+                .show();
+    }
+
+    private void setSimulatorCapability() {
+        String[] capabilityKeys = new String[]{EXTRA_SUPPORT_DYNAMIC_SIZE};
+        String[] capabilityNames = new String[]{"Dynamic Buffer Size"};
+        // Default values.
+        boolean[] capabilitySelected = new boolean[]{false};
+        // Get from preferences if exist.
+        for (int i = 0; i < capabilityKeys.length; i++) {
+            capabilitySelected[i] =
+                    mSharedPreferences.getBoolean(capabilityKeys[i], capabilitySelected[i]);
+        }
+
+        new AlertDialog.Builder(MainActivity.this)
+                .setMultiChoiceItems(
+                        capabilityNames,
+                        capabilitySelected,
+                        (dialog, which, isChecked) -> capabilitySelected[which] = isChecked)
+                .setCancelable(false)
+                .setPositiveButton(
+                        android.R.string.ok,
+                        (dialogBox, id) -> {
+                            for (int i = 0; i < capabilityKeys.length; i++) {
+                                mSharedPreferences
+                                        .edit()
+                                        .putBoolean(capabilityKeys[i], capabilitySelected[i])
+                                        .apply();
+                            }
+                            setCapabilityToSimulator();
+                        })
+                .setNegativeButton(android.R.string.cancel, null)
+                .setTitle("Simulator Capability")
+                .show();
+    }
+
+    private void setCapabilityToSimulator() {
+        if (mFastPairSimulator != null) {
+            mFastPairSimulator.setDynamicBufferSize(
+                    getFromIntentOrPrefs(EXTRA_SUPPORT_DYNAMIC_SIZE, false));
+        }
+    }
+
+    private static String getModelIdString(long id) {
+        String result = Ascii.toUpperCase(Long.toHexString(id));
+        while (result.length() < MODEL_ID_LENGTH) {
+            result = "0" + result;
+        }
+        return result;
+    }
+
+    private void putFixedModelLocal() {
+        mModelsMap.put(
+                "00000C",
+                Device.newBuilder()
+                        .setId(12)
+                        .setAntiSpoofingKeyPair(AntiSpoofingKeyPair.newBuilder().build())
+                        .setDeviceType(DeviceType.HEADPHONES)
+                        .build());
+    }
+
+    private void setupModelIdSpinner() {
+        mModelIdSpinner = findViewById(R.id.model_id_spinner);
+
+        ArrayAdapter<String> modelIdAdapter =
+                new ArrayAdapter<>(this, android.R.layout.simple_spinner_item);
+        modelIdAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        mModelIdSpinner.setAdapter(modelIdAdapter);
+        resetModelIdSpinner();
+        mModelIdSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(AdapterView<?> parent, View view, int position,
+                    long id) {
+                setModelId(mModelsMap.keySet().toArray(new String[0])[position]);
+            }
+
+            @Override
+            public void onNothingSelected(AdapterView<?> adapterView) {
+            }
+        });
+    }
+
+    private void setupRemoteDevices() {
+        if (Strings.isNullOrEmpty(getIntent().getStringExtra(EXTRA_REMOTE_DEVICE_ID))) {
+            log("Can't get remote device id");
+            return;
+        }
+        mRemoteDeviceId = getIntent().getStringExtra(EXTRA_REMOTE_DEVICE_ID);
+        mInputStreamListener = new RemoteDeviceListener(this);
+
+        try {
+            mRemoteDevicesManager.registerRemoteDevice(
+                    mRemoteDeviceId,
+                    new RemoteDevice(
+                            mRemoteDeviceId,
+                            StreamIOHandlerFactory.createStreamIOHandler(
+                                    StreamIOHandlerFactory.Type.LOCAL_FILE,
+                                    REMOTE_DEVICE_INPUT_STREAM_URI,
+                                    REMOTE_DEVICE_OUTPUT_STREAM_URI),
+                            mInputStreamListener));
+        } catch (IOException e) {
+            warning("Failed to create stream IO handler: %s", e);
+        }
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    @UiThread
+    private void resetModelIdSpinner() {
+        ArrayAdapter adapter = (ArrayAdapter) mModelIdSpinner.getAdapter();
+        if (adapter == null) {
+            return;
+        }
+
+        adapter.clear();
+        if (!mModelsMap.isEmpty()) {
+            for (String modelId : mModelsMap.keySet()) {
+                adapter.add(modelId + "-" + mModelsMap.get(modelId).getName());
+            }
+            mModelIdSpinner.setEnabled(true);
+            int newPos = getPositionFromModelId(getModelId());
+            if (newPos < 0) {
+                String newModelId = mModelsMap.keySet().iterator().next();
+                Toast.makeText(this,
+                        "Can't find Model ID " + getModelId() + " from console, reset it to "
+                                + newModelId, Toast.LENGTH_SHORT).show();
+                setModelId(newModelId);
+                newPos = 0;
+            }
+            mModelIdSpinner.setSelection(newPos, /* animate= */ false);
+        } else {
+            mModelIdSpinner.setEnabled(false);
+        }
+    }
+
+    private String getModelId() {
+        return getFromIntentOrPrefs(EXTRA_MODEL_ID, MODEL_ID_DEFAULT).toUpperCase(Locale.US);
+    }
+
+    private boolean setModelId(String modelId) {
+        String validModelId = getValidModelId(modelId);
+        if (TextUtils.isEmpty(validModelId)) {
+            log("Can't do setModelId because inputted modelId is invalid!");
+            return false;
+        }
+
+        if (getModelId().equals(validModelId)) {
+            return false;
+        }
+        mSharedPreferences.edit().putString(EXTRA_MODEL_ID, validModelId).apply();
+        reset();
+        return true;
+    }
+
+    @Nullable
+    private static String getValidModelId(String modelId) {
+        if (TextUtils.isEmpty(modelId) || modelId.length() < MODEL_ID_LENGTH) {
+            return null;
+        }
+
+        return modelId.substring(0, MODEL_ID_LENGTH).toUpperCase(Locale.US);
+    }
+
+    private int getPositionFromModelId(String modelId) {
+        int i = 0;
+        for (String id : mModelsMap.keySet()) {
+            if (id.equals(modelId)) {
+                return i;
+            }
+            i++;
+        }
+        return -1;
+    }
+
+    private void resetAccountKeys() {
+        if (mFastPairSimulator != null) {
+            mFastPairSimulator.resetAccountKeys();
+            mFastPairSimulator.startAdvertising();
+        }
+    }
+
+    private void resetDeviceName() {
+        if (mFastPairSimulator != null) {
+            mFastPairSimulator.resetDeviceName();
+        }
+    }
+
+    /** Called via activity_main.xml */
+    public void onResetButtonClicked(View view) {
+        reset();
+    }
+
+    /** Called via activity_main.xml */
+    public void onSendEventStreamMessageButtonClicked(View view) {
+        if (mFastPairSimulator != null) {
+            mFastPairSimulator.sendEventStreamMessageToRfcommDevices(mEventGroup);
+        }
+    }
+
+    void reset() {
+        Button resetButton = findViewById(R.id.reset_button);
+        if (mModelsMap.isEmpty() || !resetButton.isEnabled()) {
+            return;
+        }
+        resetButton.setText("Resetting...");
+        resetButton.setEnabled(false);
+        mModelIdSpinner.setEnabled(false);
+        mAppLaunchSwitch.setEnabled(false);
+
+        if (mFastPairSimulator != null) {
+            mFastPairSimulator.stopAdvertising();
+
+            if (mBluetoothController.getRemoteDevice() != null) {
+                if (mRemoveAllDevicesDuringPairing) {
+                    mFastPairSimulator.removeBond(mBluetoothController.getRemoteDevice());
+                }
+                mBluetoothController.clearRemoteDevice();
+            }
+            // To be safe, also unpair from all phones (this covers the case where you kill +
+            // relaunch the
+            // simulator while paired).
+            if (mRemoveAllDevicesDuringPairing) {
+                mFastPairSimulator.disconnectAllBondedDevices();
+            }
+            // Sometimes a device will still be connected even though it's not bonded. :( Clear
+            // that too.
+            BluetoothProfile profileProxy = mBluetoothController.getA2DPSinkProfileProxy();
+            for (BluetoothDevice device : profileProxy.getConnectedDevices()) {
+                mFastPairSimulator.disconnect(profileProxy, device);
+            }
+        }
+        updateStatusView();
+
+        if (mFastPairSimulator != null) {
+            mFastPairSimulator.destroy();
+        }
+        TextView textView = (TextView) findViewById(R.id.text_view);
+        textView.setText("");
+        textView.setMovementMethod(new ScrollingMovementMethod());
+
+        String modelId = getModelId();
+
+        String txPower = getFromIntentOrPrefs(EXTRA_TX_POWER_LEVEL, "HIGH");
+        updateStringStatusView(R.id.tx_power_text_view, "TxPower", txPower);
+
+        String bluetoothAddress = getFromIntentOrPrefs(EXTRA_BLUETOOTH_ADDRESS, "");
+
+        String firmwareVersion = getFromIntentOrPrefs(EXTRA_FIRMWARE_VERSION, "1.1");
+        try {
+            Preconditions.checkArgument(base16().decode(bluetoothAddress).length == 6);
+        } catch (IllegalArgumentException e) {
+            log("Invalid BLUETOOTH_ADDRESS extra (%s), using default.", bluetoothAddress);
+            bluetoothAddress = null;
+        }
+        final String finalBluetoothAddress = bluetoothAddress;
+
+        updateStringStatusView(
+                R.id.anti_spoofing_private_key_text_view, ANTI_SPOOFING_KEY_LABEL, "Loading...");
+
+        boolean useRandomSaltForAccountKeyRotation =
+                getFromIntentOrPrefs(EXTRA_USE_RANDOM_SALT_FOR_ACCOUNT_KEY_ROTATION, false);
+
+        Executors.newSingleThreadExecutor().execute(() -> {
+            // Fetch the anti-spoofing key corresponding to this model ID (if it
+            // exists).
+            // The account must have Project Viewer permission for the project
+            // that owns
+            // the model ID (normally discoverer-test or discoverer-devices).
+            byte[] antiSpoofingKey = getAntiSpoofingKey(modelId);
+            String antiSpoofingKeyString;
+            Device device = mModelsMap.get(modelId);
+            if (antiSpoofingKey != null) {
+                antiSpoofingKeyString = base64().encode(antiSpoofingKey);
+            } else {
+                if (mSharedPreferences.getString(KEY_ACCOUNT_NAME, "").isEmpty()) {
+                    antiSpoofingKeyString = "Can't fetch, no account";
+                } else {
+                    if (device == null) {
+                        antiSpoofingKeyString = String.format(Locale.US,
+                                "Can't find model %s from console", modelId);
+                    } else if (!device.hasAntiSpoofingKeyPair()) {
+                        antiSpoofingKeyString = String.format(Locale.US,
+                                "Can't find AntiSpoofingKeyPair for model %s", modelId);
+                    } else if (device.getAntiSpoofingKeyPair().getPrivateKey().isEmpty()) {
+                        antiSpoofingKeyString = String.format(Locale.US,
+                                "Can't find privateKey for model %s", modelId);
+                    } else {
+                        antiSpoofingKeyString = "Unknown error";
+                    }
+                }
+            }
+
+            int desiredIoCapability = getIoCapabilityFromModelId(modelId);
+
+            mBluetoothController.setIoCapability(
+                    /*ioCapabilityClassic=*/ desiredIoCapability,
+                    /*ioCapabilityBLE=*/ desiredIoCapability);
+
+            runOnUiThread(() -> {
+                updateStringStatusView(
+                        R.id.anti_spoofing_private_key_text_view,
+                        ANTI_SPOOFING_KEY_LABEL,
+                        antiSpoofingKeyString);
+                FastPairSimulator.Options option = FastPairSimulator.Options.builder(modelId)
+                        .setAdvertisingModelId(
+                                mAppLaunchSwitch.isChecked() ? MODEL_ID_APP_LAUNCH : modelId)
+                        .setBluetoothAddress(finalBluetoothAddress)
+                        .setTxPowerLevel(toTxPowerLevel(txPower))
+                        .setCallback(this::updateStatusView)
+                        .setAntiSpoofingPrivateKey(antiSpoofingKey)
+                        .setUseRandomSaltForAccountKeyRotation(useRandomSaltForAccountKeyRotation)
+                        .setDataOnlyConnection(device != null && device.getDataOnlyConnection())
+                        .setIsMemoryTest(mInputStreamListener != null)
+                        .setShowsPasskeyConfirmation(
+                                device.getDeviceType().equals(DeviceType.ANDROID_AUTO))
+                        .setRemoveAllDevicesDuringPairing(mRemoveAllDevicesDuringPairing)
+                        .build();
+                Logger textViewLogger = new Logger(FastPairSimulator.TAG) {
+
+                    @FormatMethod
+                    public void log(@Nullable Throwable exception, String message,
+                            Object... objects) {
+                        super.log(exception, message, objects);
+
+                        String exceptionMessage = (exception == null) ? ""
+                                : " - " + exception.getMessage();
+                        final String finalMessage =
+                                String.format(message, objects) + exceptionMessage;
+
+                        textView.post(() -> {
+                            String newText =
+                                    textView.getText() + "\n\n" + finalMessage;
+                            textView.setText(newText);
+                        });
+                    }
+                };
+                mFastPairSimulator =
+                        new FastPairSimulator(this, option, textViewLogger);
+                mFastPairSimulator.setFirmwareVersion(firmwareVersion);
+                mFailSwitch.setChecked(
+                        mFastPairSimulator.getShouldFailPairing());
+                mAdvOptionSpinner.setSelection(0);
+                setCapabilityToSimulator();
+
+                updateStringStatusView(R.id.bluetooth_address_text_view,
+                        "Bluetooth address",
+                        mFastPairSimulator.getBluetoothAddress());
+
+                updateStringStatusView(R.id.device_name_text_view,
+                        "Device name",
+                        mFastPairSimulator.getDeviceName());
+
+                resetButton.setText("Reset");
+                resetButton.setEnabled(true);
+                mModelIdSpinner.setEnabled(true);
+                mAppLaunchSwitch.setEnabled(true);
+                mFastPairSimulator.setDeviceNameCallback(deviceName ->
+                        updateStringStatusView(
+                                R.id.device_name_text_view,
+                                "Device name", deviceName));
+
+                if (desiredIoCapability == IO_CAPABILITY_IN
+                        || device.getDeviceType().equals(DeviceType.ANDROID_AUTO)) {
+                    mFastPairSimulator.setPasskeyEventCallback(mPasskeyEventCallback);
+                }
+                if (mInputStreamListener != null) {
+                    mInputStreamListener.setFastPairSimulator(mFastPairSimulator);
+                }
+            });
+        });
+    }
+
+    private int getIoCapabilityFromModelId(String modelId) {
+        Device device = mModelsMap.get(modelId);
+        if (device == null) {
+            return IO_CAPABILITY_NONE;
+        } else {
+            if (getAntiSpoofingKey(modelId) == null) {
+                return IO_CAPABILITY_NONE;
+            } else {
+                switch (device.getDeviceType()) {
+                    case INPUT_DEVICE:
+                        return IO_CAPABILITY_IN;
+
+                    case DEVICE_TYPE_UNSPECIFIED:
+                        return IO_CAPABILITY_NONE;
+
+                    // Treats wearable to IO_CAPABILITY_KBDISP for simulator because there seems
+                    // no suitable
+                    // type.
+                    case WEARABLE:
+                        return IO_CAPABILITY_KBDISP;
+
+                    default:
+                        return IO_CAPABILITY_IO;
+                }
+            }
+        }
+    }
+
+    @Nullable
+    ByteString getAccontKey() {
+        if (mFastPairSimulator == null) {
+            return null;
+        }
+        return mFastPairSimulator.getAccountKey();
+    }
+
+    @Nullable
+    private byte[] getAntiSpoofingKey(String modelId) {
+        Device device = mModelsMap.get(modelId);
+        if (device != null
+                && device.hasAntiSpoofingKeyPair()
+                && !device.getAntiSpoofingKeyPair().getPrivateKey().isEmpty()) {
+            return base64().decode(device.getAntiSpoofingKeyPair().getPrivateKey().toStringUtf8());
+        } else if (ANTI_SPOOFING_PRIVATE_KEY_MAP.containsKey(modelId)) {
+            return base64().decode(ANTI_SPOOFING_PRIVATE_KEY_MAP.get(modelId));
+        } else {
+            return null;
+        }
+    }
+
+    private final PasskeyEventCallback mPasskeyEventCallback = new PasskeyEventCallback() {
+        @Override
+        public void onPasskeyRequested(KeyInputCallback keyInputCallback) {
+            showInputPasskeyDialog(keyInputCallback);
+        }
+
+        @Override
+        public void onPasskeyConfirmation(int passkey, Consumer<Boolean> isConfirmed) {
+            showConfirmPasskeyDialog(passkey, isConfirmed);
+        }
+
+        @Override
+        public void onRemotePasskeyReceived(int passkey) {
+            if (mInputPasskeyDialog == null) {
+                return;
+            }
+
+            EditText userInputDialogEditText = mInputPasskeyDialog.findViewById(
+                    R.id.userInputDialog);
+            if (userInputDialogEditText == null) {
+                return;
+            }
+
+            userInputDialogEditText.setText(String.format("%d", passkey));
+        }
+    };
+
+    private void showInputPasskeyDialog(KeyInputCallback keyInputCallback) {
+        if (mInputPasskeyDialog == null) {
+            View userInputView =
+                    LayoutInflater.from(getApplicationContext()).inflate(R.layout.user_input_dialog,
+                            null);
+            EditText userInputDialogEditText = userInputView.findViewById(R.id.userInputDialog);
+            userInputDialogEditText.setHint(R.string.passkey_input_hint);
+            userInputDialogEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
+            mInputPasskeyDialog = new AlertDialog.Builder(MainActivity.this)
+                    .setView(userInputView)
+                    .setCancelable(false)
+                    .setPositiveButton(
+                            android.R.string.ok,
+                            (DialogInterface dialogBox, int id) -> {
+                                String input = userInputDialogEditText.getText().toString();
+                                keyInputCallback.onKeyInput(Integer.parseInt(input));
+                            })
+                    .setNegativeButton(android.R.string.cancel, /* listener= */ null)
+                    .setTitle(R.string.passkey_dialog_title)
+                    .create();
+        }
+        if (!mInputPasskeyDialog.isShowing()) {
+            mInputPasskeyDialog.show();
+        }
+    }
+
+    private void showConfirmPasskeyDialog(int passkey, Consumer<Boolean> isConfirmed) {
+        runOnUiThread(() -> new AlertDialog.Builder(MainActivity.this)
+                .setCancelable(false)
+                .setTitle(R.string.confirm_passkey)
+                .setMessage(String.valueOf(passkey))
+                .setPositiveButton(android.R.string.ok,
+                        (d, w) -> isConfirmed.accept(true))
+                .setNegativeButton(android.R.string.cancel,
+                        (d, w) -> isConfirmed.accept(false))
+                .create()
+                .show());
+    }
+
+    @UiThread
+    private void updateStringStatusView(int id, String name, String value) {
+        ((TextView) findViewById(id)).setText(name + ": " + value);
+    }
+
+    @UiThread
+    private void updateStatusView() {
+        TextView remoteDeviceTextView = (TextView) findViewById(R.id.remote_device_text_view);
+        remoteDeviceTextView.setBackgroundColor(
+                mBluetoothController.getRemoteDevice() != null ? LIGHT_GREEN : Color.LTGRAY);
+        String remoteDeviceString = mBluetoothController.getRemoteDeviceAsString();
+        remoteDeviceTextView.setText("Remote device: " + remoteDeviceString);
+
+        updateBooleanStatusView(
+                R.id.is_advertising_text_view,
+                "BLE advertising",
+                mFastPairSimulator != null && mFastPairSimulator.isAdvertising());
+
+        updateStringStatusView(
+                R.id.scan_mode_text_view,
+                "Mode",
+                FastPairSimulator.scanModeToString(mBluetoothController.getScanMode()));
+
+        boolean isPaired = mBluetoothController.isPaired();
+        updateBooleanStatusView(R.id.is_paired_text_view, "Paired", isPaired);
+
+        updateBooleanStatusView(
+                R.id.is_connected_text_view, "Connected", mBluetoothController.isConnected());
+    }
+
+    @UiThread
+    private void updateBooleanStatusView(int id, String name, boolean value) {
+        TextView view = (TextView) findViewById(id);
+        view.setBackgroundColor(value ? LIGHT_GREEN : Color.LTGRAY);
+        view.setText(name + ": " + (value ? "Yes" : "No"));
+    }
+
+    private String getFromIntentOrPrefs(String key, String defaultValue) {
+        Bundle extras = getIntent().getExtras();
+        extras = extras != null ? extras : new Bundle();
+        SharedPreferences prefs = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
+        String value = extras.getString(key, prefs.getString(key, defaultValue));
+        if (value == null) {
+            prefs.edit().remove(key).apply();
+        } else {
+            prefs.edit().putString(key, value).apply();
+        }
+        return value;
+    }
+
+    private boolean getFromIntentOrPrefs(String key, boolean defaultValue) {
+        Bundle extras = getIntent().getExtras();
+        extras = extras != null ? extras : new Bundle();
+        SharedPreferences prefs = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
+        boolean value = extras.getBoolean(key, prefs.getBoolean(key, defaultValue));
+        prefs.edit().putBoolean(key, value).apply();
+        return value;
+    }
+
+    private static int toTxPowerLevel(String txPowerLevelString) {
+        switch (txPowerLevelString.toUpperCase()) {
+            case "3":
+            case "HIGH":
+                return AdvertiseSettings.ADVERTISE_TX_POWER_HIGH;
+            case "2":
+            case "MEDIUM":
+                return AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM;
+            case "1":
+            case "LOW":
+                return AdvertiseSettings.ADVERTISE_TX_POWER_LOW;
+            case "0":
+            case "ULTRA_LOW":
+                return AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW;
+            default:
+                throw new IllegalArgumentException(
+                        "Unexpected TxPower="
+                                + txPowerLevelString
+                                + ", please provide HIGH, MEDIUM, LOW, or ULTRA_LOW.");
+        }
+    }
+
+    private boolean checkPermissions(String[] permissions) {
+        for (String permission : permissions) {
+            if (checkSelfPermission(permission) != PERMISSION_GRANTED) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    protected void onDestroy() {
+        mRemoteDevicesManager.destroy();
+
+        if (mFastPairSimulator != null) {
+            mFastPairSimulator.destroy();
+            mBluetoothController.unregisterBluetoothStateReceiver();
+        }
+
+        // Recover the IO capability.
+        mBluetoothController.setIoCapability(
+                /*ioCapabilityClassic=*/ IO_CAPABILITY_IO, /*ioCapabilityBLE=*/
+                IO_CAPABILITY_KBDISP);
+
+        super.onDestroy();
+    }
+
+    @Override
+    public void onRequestPermissionsResult(
+            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+        // Relaunch this activity.
+        recreate();
+    }
+
+    void startAdvertisingBatteryInformationBasedOnOption(int option) {
+        if (mFastPairSimulator == null) {
+            return;
+        }
+
+        // Option 0 is "No battery info", it means simulator will not pack battery information when
+        // advertising. For the others with battery info, since we are simulating the Presto's
+        // behavior,
+        // there will always be three battery values.
+        switch (option) {
+            case 0:
+                // Option "0: No battery info"
+                mFastPairSimulator.clearBatteryValues();
+                break;
+            case 1:
+                // Option "1: Show L(⬆) + R(⬆) + C(⬆)"
+                mFastPairSimulator.setSuppressBatteryNotification(false);
+                mFastPairSimulator.setBatteryValues(new BatteryValue(true, 60),
+                        new BatteryValue(true, 61),
+                        new BatteryValue(true, 62));
+                break;
+            case 2:
+                // Option "2: Show L + R + C(unknown)"
+                mFastPairSimulator.setSuppressBatteryNotification(false);
+                mFastPairSimulator.setBatteryValues(new BatteryValue(false, 70),
+                        new BatteryValue(false, 71),
+                        new BatteryValue(false, -1));
+                break;
+            case 3:
+                // Option "3: Show L(low 10) + R(low 9) + C(low 25)"
+                mFastPairSimulator.setSuppressBatteryNotification(false);
+                mFastPairSimulator.setBatteryValues(new BatteryValue(false, 10),
+                        new BatteryValue(false, 9),
+                        new BatteryValue(false, 25));
+                break;
+            case 4:
+                // Option "4: Suppress battery w/o level changes"
+                // Just change the suppress bit and keep the battery values the same as before.
+                mFastPairSimulator.setSuppressBatteryNotification(true);
+                break;
+            case 5:
+                // Option "5: Suppress L(low 10) + R(11) + C"
+                mFastPairSimulator.setSuppressBatteryNotification(true);
+                mFastPairSimulator.setBatteryValues(new BatteryValue(false, 10),
+                        new BatteryValue(false, 11),
+                        new BatteryValue(false, 82));
+                break;
+            case 6:
+                // Option "6: Suppress L(low ⬆) + R(low ⬆) + C(low 10)"
+                mFastPairSimulator.setSuppressBatteryNotification(true);
+                mFastPairSimulator.setBatteryValues(new BatteryValue(true, 10),
+                        new BatteryValue(true, 9),
+                        new BatteryValue(false, 10));
+                break;
+            case 7:
+                // Option "7: Suppress L(low ⬆) + R(low ⬆) + C(low ⬆)"
+                mFastPairSimulator.setSuppressBatteryNotification(true);
+                mFastPairSimulator.setBatteryValues(new BatteryValue(true, 10),
+                        new BatteryValue(true, 9),
+                        new BatteryValue(true, 25));
+                break;
+            case 8:
+                // Option "8: Show subsequent pairing notification"
+                mFastPairSimulator.setSuppressSubsequentPairingNotification(false);
+                break;
+            case 9:
+                // Option "9: Suppress subsequent pairing notification"
+                mFastPairSimulator.setSuppressSubsequentPairingNotification(true);
+                break;
+            default:
+                // Unknown option, do nothing.
+                return;
+        }
+
+        mFastPairSimulator.startAdvertising();
+    }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/RemoteDeviceListener.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/RemoteDeviceListener.java
new file mode 100644
index 0000000..fac8cb5
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/app/RemoteDeviceListener.java
@@ -0,0 +1,164 @@
+/*
+ * 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.fastpair.provider.simulator.app;
+
+import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.ACCOUNT_KEY;
+import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.ACKNOWLEDGE;
+import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.BLUETOOTH_ADDRESS_BLE;
+import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.BLUETOOTH_ADDRESS_PUBLIC;
+
+import android.nearby.fastpair.provider.FastPairSimulator;
+import android.nearby.fastpair.provider.FastPairSimulator.BatteryValue;
+import android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Command;
+import android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Command.BatteryInfo;
+import android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event;
+import android.nearby.fastpair.provider.simulator.testing.InputStreamListener;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+/** Listener for input stream of the remote device. */
+public class RemoteDeviceListener implements InputStreamListener {
+    private static final String TAG = RemoteDeviceListener.class.getSimpleName();
+
+    private final MainActivity mMainActivity;
+    @Nullable
+    private FastPairSimulator mFastPairSimulator;
+
+    public RemoteDeviceListener(MainActivity mainActivity) {
+        this.mMainActivity = mainActivity;
+    }
+
+    @Override
+    public void onInputData(ByteString byteString) {
+        Command command;
+        try {
+            command = Command.parseFrom(byteString);
+        } catch (InvalidProtocolBufferException e) {
+            Log.w(TAG, String.format("%s input data is not a Command",
+                    mMainActivity.mRemoteDeviceId), e);
+            return;
+        }
+
+        mMainActivity.runOnUiThread(() -> {
+            Log.d(TAG, String.format("%s new command %s",
+                    mMainActivity.mRemoteDeviceId, command.getCode()));
+            switch (command.getCode()) {
+                case POLLING:
+                    mMainActivity.sendEventToRemoteDevice(
+                            Event.newBuilder().setCode(ACKNOWLEDGE));
+                    break;
+                case RESET:
+                    mMainActivity.reset();
+                    break;
+                case SHOW_BATTERY:
+                    onShowBattery(command.getBatteryInfo());
+                    break;
+                case HIDE_BATTERY:
+                    onHideBattery();
+                    break;
+                case REQUEST_BLUETOOTH_ADDRESS_BLE:
+                    onRequestBleAddress();
+                    break;
+                case REQUEST_BLUETOOTH_ADDRESS_PUBLIC:
+                    onRequestPublicAddress();
+                    break;
+                case REQUEST_ACCOUNT_KEY:
+                    ByteString accountKey = mMainActivity.getAccontKey();
+                    if (accountKey == null) {
+                        break;
+                    }
+                    mMainActivity.sendEventToRemoteDevice(
+                            Event.newBuilder().setCode(ACCOUNT_KEY)
+                                    .setAccountKey(accountKey));
+                    break;
+            }
+        });
+    }
+
+    @Override
+    public void onClose() {
+        Log.d(TAG, String.format("%s input stream is closed", mMainActivity.mRemoteDeviceId));
+    }
+
+    void setFastPairSimulator(FastPairSimulator fastPairSimulator) {
+        this.mFastPairSimulator = fastPairSimulator;
+    }
+
+    private void onShowBattery(@Nullable BatteryInfo batteryInfo) {
+        if (mFastPairSimulator == null || batteryInfo == null) {
+            Log.w(TAG, "skip showing battery");
+            return;
+        }
+
+        if (batteryInfo.getBatteryValuesCount() != 3) {
+            Log.w(TAG, String.format("skip showing battery: count is not valid %d",
+                    batteryInfo.getBatteryValuesCount()));
+            return;
+        }
+
+        Log.d(TAG, String.format("Show battery %s", batteryInfo));
+
+        if (batteryInfo.hasSuppressNotification()) {
+            mFastPairSimulator.setSuppressBatteryNotification(
+                    batteryInfo.getSuppressNotification());
+        }
+        mFastPairSimulator.setBatteryValues(
+                convertFrom(batteryInfo.getBatteryValues(0)),
+                convertFrom(batteryInfo.getBatteryValues(1)),
+                convertFrom(batteryInfo.getBatteryValues(2)));
+        mFastPairSimulator.startAdvertising();
+    }
+
+    private void onHideBattery() {
+        if (mFastPairSimulator == null) {
+            return;
+        }
+
+        mFastPairSimulator.clearBatteryValues();
+        mFastPairSimulator.startAdvertising();
+    }
+
+    private void onRequestBleAddress() {
+        if (mFastPairSimulator == null) {
+            return;
+        }
+
+        mMainActivity.sendEventToRemoteDevice(
+                Event.newBuilder()
+                        .setCode(BLUETOOTH_ADDRESS_BLE)
+                        .setBleAddress(mFastPairSimulator.getBleAddress()));
+    }
+
+    private void onRequestPublicAddress() {
+        if (mFastPairSimulator == null) {
+            return;
+        }
+
+        mMainActivity.sendEventToRemoteDevice(
+                Event.newBuilder()
+                        .setCode(BLUETOOTH_ADDRESS_PUBLIC)
+                        .setPublicAddress(mFastPairSimulator.getBluetoothAddress()));
+    }
+
+    private static BatteryValue convertFrom(BatteryInfo.BatteryValue batteryValue) {
+        return new BatteryValue(batteryValue.getCharging(), batteryValue.getLevel());
+    }
+}
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/FastPairAdvertiser.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/InputStreamListener.java
similarity index 62%
copy from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/FastPairAdvertiser.java
copy to nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/InputStreamListener.java
index 33add27..b29225a 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/FastPairAdvertiser.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/InputStreamListener.java
@@ -14,14 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.common.bluetooth.fastpair.testing;
+package android.nearby.fastpair.provider.simulator.testing;
 
-import androidx.annotation.Nullable;
+import com.google.protobuf.ByteString;
 
-/** Helper for advertising Fast Pair data. */
-public interface FastPairAdvertiser {
+/** Listener for input stream. */
+public interface InputStreamListener {
 
-    void startAdvertising(@Nullable byte[] serviceData);
+    /** Called when new data {@code byteString} is read from the input stream. */
+    void onInputData(ByteString byteString);
 
-    void stopAdvertising();
+    /** Called when the input stream is closed. */
+    void onClose();
 }
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/LocalFileStreamIOHandler.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/LocalFileStreamIOHandler.java
new file mode 100644
index 0000000..cf8b022
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/LocalFileStreamIOHandler.java
@@ -0,0 +1,119 @@
+/*
+ * 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.fastpair.provider.simulator.testing;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.io.BaseEncoding.base16;
+
+import android.net.Uri;
+
+import androidx.annotation.Nullable;
+
+import com.google.protobuf.ByteString;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+
+/**
+ * Opens the {@code inputUri} and {@code outputUri} as local files and provides reading/writing
+ * data operations.
+ *
+ * To support bluetooth testing on real devices, the named pipes are created as local files and the
+ * pipe data are transferred via usb cable, then (1) the peripheral device writes {@code Event} to
+ * the output stream and reads {@code Command} from the input stream (2) the central devices write
+ * {@code Command} to the output stream and read {@code Event} from the input stream.
+ *
+ * The {@code Event} and {@code Command} are special protocols which are defined at
+ * simulator_stream_protocol.proto.
+ */
+public class LocalFileStreamIOHandler implements StreamIOHandler {
+
+    private static final int MAX_IO_DATA_LENGTH_BYTE = 65535;
+
+    private final String mInputPath;
+    private final String mOutputPath;
+
+    LocalFileStreamIOHandler(Uri inputUri, Uri outputUri) throws IOException {
+        if (!isFileExists(inputUri.getPath())) {
+            throw new FileNotFoundException("Input path is not exists.");
+        }
+        if (!isFileExists(outputUri.getPath())) {
+            throw new FileNotFoundException("Output path is not exists.");
+        }
+
+        this.mInputPath = inputUri.getPath();
+        this.mOutputPath = outputUri.getPath();
+    }
+
+    /**
+     * Reads a {@code ByteString} from the input stream. The input stream must be opened before
+     * calling this method.
+     */
+    @Override
+    public ByteString read() throws IOException {
+        try (InputStreamReader inputStream = new InputStreamReader(
+                new FileInputStream(mInputPath))) {
+            int size = inputStream.read();
+            if (size == 0) {
+                throw new IOException(String.format("Missing data size %d", size));
+            }
+
+            if (size > MAX_IO_DATA_LENGTH_BYTE) {
+                throw new IOException("Exceed the maximum data length when reading.");
+            }
+
+            char[] data = new char[size];
+            int count = inputStream.read(data);
+            if (count != size) {
+                throw new IOException(
+                        String.format("Expected size was %s but got %s", size, count));
+            }
+
+            return ByteString.copyFrom(base16().decode(new String(data)));
+        }
+    }
+
+    /**
+     * Writes a {@code output} into the output stream. The output stream must be opened before
+     * calling this method.
+     */
+    @Override
+    public void write(ByteString output) throws IOException {
+        checkArgument(output.size() > 0, "Output data is empty.");
+
+        if (output.size() > MAX_IO_DATA_LENGTH_BYTE) {
+            throw new IOException("Exceed the maximum data length when writing.");
+        }
+
+        try (OutputStreamWriter outputStream =
+                     new OutputStreamWriter(new FileOutputStream(mOutputPath))) {
+            String base16Output = base16().encode(output.toByteArray());
+            outputStream.write(base16Output.length());
+            outputStream.write(base16Output);
+        }
+    }
+
+    private static boolean isFileExists(@Nullable String path) {
+        return path != null && new File(path).exists();
+    }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/Reflect.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/Reflect.java
new file mode 100644
index 0000000..16fbc71
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/Reflect.java
@@ -0,0 +1,103 @@
+/*
+ * 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.fastpair.provider.simulator.testing;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Utilities for calling methods using reflection. The main benefit of using this helper is to avoid
+ * complications around exception handling when calling methods reflectively. It's not safe to use
+ * Java 8's multicatch on such exceptions, because the java compiler converts multicatch into
+ * ReflectiveOperationException in some instances, which doesn't work on older sdk versions.
+ * Instead, use these utilities and catch ReflectionException.
+ *
+ * <p>Example usage:
+ *
+ * <pre>{@code
+ * try {
+ *   Reflect.on(btAdapter)
+ *       .withMethod("setScanMode", int.class)
+ *       .invoke(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE)
+ * } catch (ReflectionException e) { }
+ * }</pre>
+ */
+public final class Reflect {
+    private final Object mTargetObject;
+
+    private Reflect(Object targetObject) {
+        this.mTargetObject = targetObject;
+    }
+
+    /** Creates an instance of this helper to invoke methods on the given target object. */
+    public static Reflect on(Object targetObject) {
+        return new Reflect(targetObject);
+    }
+
+    /** Finds a method with the given name and parameter types. */
+    public ReflectionMethod withMethod(String methodName, Class<?>... paramTypes)
+            throws ReflectionException {
+        try {
+            return new ReflectionMethod(mTargetObject.getClass().getMethod(methodName, paramTypes));
+        } catch (NoSuchMethodException e) {
+            throw new ReflectionException(e);
+        }
+    }
+
+    /** Represents an invokable method found reflectively. */
+    public final class ReflectionMethod {
+        private final Method mMethod;
+
+        private ReflectionMethod(Method method) {
+            this.mMethod = method;
+        }
+
+        /**
+         * Invokes this instance method with the given parameters. The called method does not return
+         * a value.
+         */
+        public void invoke(Object... parameters) throws ReflectionException {
+            try {
+                mMethod.invoke(mTargetObject, parameters);
+            } catch (IllegalAccessException e) {
+                throw new ReflectionException(e);
+            } catch (InvocationTargetException e) {
+                throw new ReflectionException(e);
+            }
+        }
+
+        /**
+         * Invokes this instance method with the given parameters. The called method returns a non
+         * null
+         * value.
+         */
+        public Object get(Object... parameters) throws ReflectionException {
+            Object value;
+            try {
+                value = mMethod.invoke(mTargetObject, parameters);
+            } catch (IllegalAccessException e) {
+                throw new ReflectionException(e);
+            } catch (InvocationTargetException e) {
+                throw new ReflectionException(e);
+            }
+            if (value == null) {
+                throw new ReflectionException(new NullPointerException());
+            }
+            return value;
+        }
+    }
+}
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/FastPairAdvertiser.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/ReflectionException.java
similarity index 63%
copy from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/FastPairAdvertiser.java
copy to nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/ReflectionException.java
index 33add27..2a65f39 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/FastPairAdvertiser.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/ReflectionException.java
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.common.bluetooth.fastpair.testing;
+package android.nearby.fastpair.provider.simulator.testing;
 
-import androidx.annotation.Nullable;
-
-/** Helper for advertising Fast Pair data. */
-public interface FastPairAdvertiser {
-
-    void startAdvertising(@Nullable byte[] serviceData);
-
-    void stopAdvertising();
+/**
+ * An exception thrown during a reflection operation. Like ReflectiveOperationException, except
+ * compatible on older API versions.
+ */
+public final class ReflectionException extends Exception {
+    public ReflectionException(Throwable cause) {
+        super(cause.getMessage(), cause);
+    }
 }
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/RemoteDevice.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/RemoteDevice.java
new file mode 100644
index 0000000..11ec9cb
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/RemoteDevice.java
@@ -0,0 +1,46 @@
+/*
+ * 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.fastpair.provider.simulator.testing;
+
+/** Represents a remote device and provides a {@link StreamIOHandler} to communicate with it. */
+public class RemoteDevice {
+    private final String mId;
+    private final StreamIOHandler mStreamIOHandler;
+    private final InputStreamListener mInputStreamListener;
+
+    public RemoteDevice(
+            String id, StreamIOHandler streamIOHandler, InputStreamListener inputStreamListener) {
+        this.mId = id;
+        this.mStreamIOHandler = streamIOHandler;
+        this.mInputStreamListener = inputStreamListener;
+    }
+
+    /** The id used by this device. */
+    public String getId() {
+        return mId;
+    }
+
+    /** The handler processes input and output data channels. */
+    public StreamIOHandler getStreamIOHandler() {
+        return mStreamIOHandler;
+    }
+
+    /** Listener for the input stream. */
+    public InputStreamListener getInputStreamListener() {
+        return mInputStreamListener;
+    }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/RemoteDevicesManager.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/RemoteDevicesManager.java
new file mode 100644
index 0000000..02260c2
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/RemoteDevicesManager.java
@@ -0,0 +1,140 @@
+/*
+ * 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.fastpair.provider.simulator.testing;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import android.util.Log;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.protobuf.ByteString;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+
+/**
+ * Manages the IO streams with remote devices.
+ *
+ * <p>The caller must invoke {@link #registerRemoteDevice} before starting to communicate with the
+ * remote device, and invoke {@link #unregisterRemoteDevice} after finishing tasks. If this instance
+ * is not used anymore, the caller need to invoke {@link #destroy} to release all resources.
+ *
+ * <p>All of the methods are thread-safe.
+ */
+public class RemoteDevicesManager {
+    private static final String TAG = "RemoteDevicesManager";
+
+    private final Map<String, RemoteDevice> mRemoteDeviceMap = new HashMap<>();
+    private final ListeningExecutorService mBackgroundExecutor =
+            MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
+    private final ListeningExecutorService mListenInputStreamExecutors =
+            MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+    private final Map<String, ListenableFuture<Void>> mListeningTaskMap = new HashMap<>();
+
+    /**
+     * Opens input and output data streams for {@code remoteDevice} in the background and notifies
+     * the
+     * open result via {@code callback}, and assigns a dedicated executor to listen the input data
+     * stream if data streams are opened successfully. The dedicated executor will invoke the
+     * {@code
+     * remoteDevice.inputStreamListener().onInputData()} directly if the new data exists in the
+     * input
+     * stream and invoke the {@code remoteDevice.inputStreamListener().onClose()} if the input
+     * stream
+     * is closed.
+     */
+    public synchronized void registerRemoteDevice(String id, RemoteDevice remoteDevice) {
+        checkState(mRemoteDeviceMap.put(id, remoteDevice) == null,
+                "The %s is already registered", id);
+        startListeningInputStreamTask(remoteDevice);
+    }
+
+    /**
+     * Closes the data streams for specific remote device {@code id} in the background and notifies
+     * the result via {@code callback}.
+     */
+    public synchronized void unregisterRemoteDevice(String id) {
+        RemoteDevice remoteDevice = mRemoteDeviceMap.remove(id);
+        checkState(remoteDevice != null, "The %s is not registered", id);
+        if (mListeningTaskMap.containsKey(id)) {
+            mListeningTaskMap.remove(id).cancel(/* mayInterruptIfRunning= */ true);
+        }
+    }
+
+    /** Closes all data streams of registered remote devices and stop all background tasks. */
+    public synchronized void destroy() {
+        mRemoteDeviceMap.clear();
+        mListeningTaskMap.clear();
+        mListenInputStreamExecutors.shutdownNow();
+    }
+
+    /**
+     * Writes {@code data} into the output data stream of specific remote device {@code id} in the
+     * background and notifies the result via {@code callback}.
+     */
+    public synchronized void writeDataToRemoteDevice(
+            String id, ByteString data, FutureCallback<Void> callback) {
+        RemoteDevice remoteDevice = mRemoteDeviceMap.get(id);
+        checkState(remoteDevice != null, "The %s is not registered", id);
+
+        runInBackground(() -> {
+            remoteDevice.getStreamIOHandler().write(data);
+            return null;
+        }, callback);
+    }
+
+    private void runInBackground(Callable<Void> callable, FutureCallback<Void> callback) {
+        Futures.addCallback(
+                mBackgroundExecutor.submit(callable), callback, MoreExecutors.directExecutor());
+    }
+
+    private void startListeningInputStreamTask(RemoteDevice remoteDevice) {
+        ListenableFuture<Void> listenFuture = mListenInputStreamExecutors.submit(() -> {
+            Log.i(TAG, "Start listening " + remoteDevice.getId());
+            while (true) {
+                ByteString data;
+                try {
+                    data = remoteDevice.getStreamIOHandler().read();
+                } catch (IOException | IllegalStateException e) {
+                    break;
+                }
+                remoteDevice.getInputStreamListener().onInputData(data);
+            }
+        }, /* result= */ null);
+        Futures.addCallback(listenFuture, new FutureCallback<Void>() {
+            @Override
+            public void onSuccess(Void result) {
+                Log.i(TAG, "Stop listening " + remoteDevice.getId());
+                remoteDevice.getInputStreamListener().onClose();
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                Log.w(TAG, "Stop listening " + remoteDevice.getId() + ", cause: " + t);
+                remoteDevice.getInputStreamListener().onClose();
+            }
+        }, MoreExecutors.directExecutor());
+        mListeningTaskMap.put(remoteDevice.getId(), listenFuture);
+    }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/StreamIOHandler.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/StreamIOHandler.java
new file mode 100644
index 0000000..d5fdb9e
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/StreamIOHandler.java
@@ -0,0 +1,43 @@
+/*
+ * 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.fastpair.provider.simulator.testing;
+
+import com.google.protobuf.ByteString;
+
+import java.io.IOException;
+
+/**
+ * Opens input and output data channels, then provides read and write operations to the data
+ * channels.
+ */
+public interface StreamIOHandler {
+    /**
+     * Reads stream data from the input channel.
+     *
+     * @return a protocol buffer contains the input message
+     * @throws IOException errors occur when reading the input stream
+     */
+    ByteString read() throws IOException;
+
+    /**
+     * Writes stream data to the output channel.
+     *
+     * @param output a protocol buffer contains the output message
+     * @throws IOException errors occur when writing the output message to output stream
+     */
+    void write(ByteString output) throws IOException;
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/StreamIOHandlerFactory.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/StreamIOHandlerFactory.java
new file mode 100644
index 0000000..24cfe56
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/StreamIOHandlerFactory.java
@@ -0,0 +1,44 @@
+/*
+ * 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.fastpair.provider.simulator.testing;
+
+import android.net.Uri;
+
+import java.io.IOException;
+
+/** A simple factory creating {@link StreamIOHandler} according to {@link Type}. */
+public class StreamIOHandlerFactory {
+
+    /** Types for creating {@link StreamIOHandler}. */
+    public enum Type {
+
+        /**
+         * A {@link StreamIOHandler} accepts local file uris and provides reading/writing file
+         * operations.
+         */
+        LOCAL_FILE
+    }
+
+    /** Creates an instance of {@link StreamIOHandler}. */
+    public static StreamIOHandler createStreamIOHandler(Type type, Uri input, Uri output)
+            throws IOException {
+        if (type.equals(Type.LOCAL_FILE)) {
+            return new LocalFileStreamIOHandler(input, output);
+        }
+        throw new IllegalArgumentException(String.format("Can't support %s", type));
+    }
+}
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/FastPairAdvertiser.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairAdvertiser.java
similarity index 92%
rename from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/FastPairAdvertiser.java
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairAdvertiser.java
index 33add27..95c077b 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/FastPairAdvertiser.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairAdvertiser.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.common.bluetooth.fastpair.testing;
+package android.nearby.fastpair.provider;
 
 import androidx.annotation.Nullable;
 
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/FastPairSimulator.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulator.java
similarity index 76%
rename from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/FastPairSimulator.java
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulator.java
index cf8be76..931f2e0 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/FastPairSimulator.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulator.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.common.bluetooth.fastpair.testing;
+package android.nearby.fastpair.provider;
 
 import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
 import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE;
@@ -29,6 +29,8 @@
 import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_NOTIFY;
 import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_READ;
 import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE;
+import static android.nearby.fastpair.provider.bluetooth.BluetoothManager.wrap;
+import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.CONNECTED;
 
 import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.AES_BLOCK_LENGTH;
 import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.encrypt;
@@ -37,8 +39,6 @@
 import static com.android.server.nearby.common.bluetooth.fastpair.Constants.TransportDiscoveryService.BLUETOOTH_SIG_ORGANIZATION_ID;
 import static com.android.server.nearby.common.bluetooth.fastpair.EllipticCurveDiffieHellmanExchange.PUBLIC_KEY_LENGTH;
 import static com.android.server.nearby.common.bluetooth.fastpair.MessageStreamHmacEncoder.SECTION_NONCE_LENGTH;
-import static com.android.server.nearby.common.bluetooth.fastpair.testing.RfcommServer.State.CONNECTED;
-import static com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothManager.wrap;
 
 import static com.google.common.io.BaseEncoding.base16;
 import static com.google.common.primitives.Bytes.concat;
@@ -56,12 +56,22 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.nearby.multidevices.fastpair.EventStreamProtocol.AcknowledgementEventCode;
-import android.nearby.multidevices.fastpair.EventStreamProtocol.DeviceActionEventCode;
-import android.nearby.multidevices.fastpair.EventStreamProtocol.DeviceCapabilitySyncEventCode;
-import android.nearby.multidevices.fastpair.EventStreamProtocol.DeviceConfigurationEventCode;
-import android.nearby.multidevices.fastpair.EventStreamProtocol.DeviceEventCode;
-import android.nearby.multidevices.fastpair.EventStreamProtocol.EventGroup;
+import android.nearby.fastpair.provider.EventStreamProtocol.AcknowledgementEventCode;
+import android.nearby.fastpair.provider.EventStreamProtocol.DeviceActionEventCode;
+import android.nearby.fastpair.provider.EventStreamProtocol.DeviceCapabilitySyncEventCode;
+import android.nearby.fastpair.provider.EventStreamProtocol.DeviceConfigurationEventCode;
+import android.nearby.fastpair.provider.EventStreamProtocol.DeviceEventCode;
+import android.nearby.fastpair.provider.EventStreamProtocol.EventGroup;
+import android.nearby.fastpair.provider.bluetooth.BluetoothGattServerConfig;
+import android.nearby.fastpair.provider.bluetooth.BluetoothGattServerConfig.ServiceConfig;
+import android.nearby.fastpair.provider.bluetooth.BluetoothGattServerConnection;
+import android.nearby.fastpair.provider.bluetooth.BluetoothGattServerConnection.Notifier;
+import android.nearby.fastpair.provider.bluetooth.BluetoothGattServerHelper;
+import android.nearby.fastpair.provider.bluetooth.BluetoothGattServlet;
+import android.nearby.fastpair.provider.bluetooth.RfcommServer;
+import android.nearby.fastpair.provider.crypto.Crypto;
+import android.nearby.fastpair.provider.crypto.E2eeCalculator;
+import android.nearby.fastpair.provider.utils.Logger;
 import android.os.Build.VERSION;
 import android.os.Build.VERSION_CODES;
 import android.os.Handler;
@@ -99,12 +109,6 @@
 import com.android.server.nearby.common.bluetooth.fastpair.NamingEncoder;
 import com.android.server.nearby.common.bluetooth.fastpair.Reflect;
 import com.android.server.nearby.common.bluetooth.fastpair.ReflectionException;
-import com.android.server.nearby.common.bluetooth.gatt.server.BluetoothGattServerConfig;
-import com.android.server.nearby.common.bluetooth.gatt.server.BluetoothGattServerConfig.ServiceConfig;
-import com.android.server.nearby.common.bluetooth.gatt.server.BluetoothGattServerConnection;
-import com.android.server.nearby.common.bluetooth.gatt.server.BluetoothGattServerConnection.Notifier;
-import com.android.server.nearby.common.bluetooth.gatt.server.BluetoothGattServerHelper;
-import com.android.server.nearby.common.bluetooth.gatt.server.BluetoothGattServlet;
 
 import com.google.common.base.Ascii;
 import com.google.common.primitives.Bytes;
@@ -156,8 +160,8 @@
  * @see {http://go/fast-pair-2-spec}
  */
 public class FastPairSimulator {
-    private static final String TAG = "FastPairSimulator";
-    private final Logger logger = new Logger(TAG);
+    public static final String TAG = "FastPairSimulator";
+    private final Logger mLogger;
 
     /**
      * Headphones. Generated by
@@ -207,11 +211,11 @@
      *       </ul>
      * </ul>
      */
-    private String deviceFirmwareVersion = "1.1.0";
+    private String mDeviceFirmwareVersion = "1.1.0";
 
-    private byte[] sessionNonce;
+    private byte[] mSessionNonce;
 
-    private boolean useLogFullEvent = true;
+    private boolean mUseLogFullEvent = true;
 
     private enum ResultCode {
         SUCCESS((byte) 0x00),
@@ -220,10 +224,10 @@
         UNSUPPORTED_ORGANIZATION_ID((byte) 0x03),
         OPERATION_FAILED((byte) 0x04);
 
-        private final byte byteValue;
+        private final byte mByteValue;
 
         ResultCode(byte byteValue) {
-            this.byteValue = byteValue;
+            this.mByteValue = byteValue;
         }
     }
 
@@ -232,33 +236,33 @@
         ON((byte) 0x01),
         TEMPORARILY_UNAVAILABLE((byte) 0x10);
 
-        private final byte byteValue;
+        private final byte mByteValue;
 
         TransportState(byte byteValue) {
-            this.byteValue = byteValue;
+            this.mByteValue = byteValue;
         }
     }
 
-    private final Context context;
-    private final Options options;
-    private final Handler uiThreadHandler = new Handler(Looper.getMainLooper());
+    private final Context mContext;
+    private final Options mOptions;
+    private final Handler mUiThreadHandler = new Handler(Looper.getMainLooper());
     // No thread pool: Only used in test app (outside gmscore) and in javatests/.../gmscore/.
-    private final ScheduledExecutorService executor =
+    private final ScheduledExecutorService mExecutor =
             Executors.newSingleThreadScheduledExecutor(); // exempt
-    private final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
-    private final BroadcastReceiver broadcastReceiver =
+    private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+    private final BroadcastReceiver mBroadcastReceiver =
             new BroadcastReceiver() {
                 @Override
                 public void onReceive(Context context, Intent intent) {
-                    if (shouldFailPairing) {
-                        logger.log("Pairing disabled by test app switch");
+                    if (mShouldFailPairing) {
+                        mLogger.log("Pairing disabled by test app switch");
                         return;
                     }
-                    if (isDestroyed) {
+                    if (mIsDestroyed) {
                         // Sometimes this receiver does not successfully unregister in destroy()
                         // which causes events to occur after the simulator is stopped, so ignore
                         // those events.
-                        logger.log("Intent received after simulator destroyed, ignoring");
+                        mLogger.log("Intent received after simulator destroyed, ignoring");
                         return;
                     }
                     BluetoothDevice device = intent.getParcelableExtra(
@@ -266,52 +270,52 @@
                     switch (intent.getAction()) {
                         case BluetoothAdapter.ACTION_SCAN_MODE_CHANGED:
                             if (isDiscoverable()) {
-                                isDiscoverableLatch.countDown();
+                                mIsDiscoverableLatch.countDown();
                             }
                             break;
                         case BluetoothDevice.ACTION_PAIRING_REQUEST:
                             int variant = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT,
                                     ERROR);
                             int key = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, ERROR);
-                            logger.log(
+                            mLogger.log(
                                     "Pairing request, variant=%d, key=%s", variant,
                                     key == ERROR ? "(none)" : key);
 
                             // Prevent Bluetooth Settings from getting the pairing request.
                             abortBroadcast();
 
-                            pairingDevice = device;
-                            if (secret == null) {
+                            mPairingDevice = device;
+                            if (mSecret == null) {
                                 // We haven't done the handshake over GATT to agree on the shared
                                 // secret. For now, just accept anyway (so we can still simulate
                                 // old 1.0 model IDs).
-                                logger.log("No handshake, auto-accepting anyway.");
+                                mLogger.log("No handshake, auto-accepting anyway.");
                                 setPasskeyConfirmation(true);
                             } else if (variant
                                     == BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION) {
                                 // Store the passkey. And check it, since there's a race (see
                                 // method for why). Usually this check is a no-op and we'll get
                                 // the passkey later over GATT.
-                                localPasskey = key;
+                                mLocalPasskey = key;
                                 checkPasskey();
                             } else if (variant == PAIRING_VARIANT_DISPLAY_PASSKEY) {
-                                if (passkeyEventCallback != null) {
-                                    passkeyEventCallback.onPasskeyRequested(
+                                if (mPasskeyEventCallback != null) {
+                                    mPasskeyEventCallback.onPasskeyRequested(
                                             FastPairSimulator.this::enterPassKey);
                                 } else {
-                                    logger.log("passkeyEventCallback is not set!");
+                                    mLogger.log("passkeyEventCallback is not set!");
                                     enterPassKey(key);
                                 }
                             } else if (variant == PAIRING_VARIANT_CONSENT) {
                                 setPasskeyConfirmation(true);
 
                             } else if (variant == BluetoothDevice.PAIRING_VARIANT_PIN) {
-                                if (passkeyEventCallback != null) {
-                                    passkeyEventCallback.onPasskeyRequested(
+                                if (mPasskeyEventCallback != null) {
+                                    mPasskeyEventCallback.onPasskeyRequested(
                                             (int pin) -> {
                                                 byte[] newPin = convertPinToBytes(
                                                         String.format(Locale.ENGLISH, "%d", pin));
-                                                pairingDevice.setPin(newPin);
+                                                mPairingDevice.setPin(newPin);
                                             });
                                 }
                             } else {
@@ -324,36 +328,36 @@
                             int bondState =
                                     intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
                                             BluetoothDevice.BOND_NONE);
-                            logger.log("Bond state to %s changed to %d", device, bondState);
+                            mLogger.log("Bond state to %s changed to %d", device, bondState);
                             switch (bondState) {
                                 case BluetoothDevice.BOND_BONDING:
                                     // If we've started bonding, we shouldn't be advertising.
-                                    advertiser.stopAdvertising();
+                                    mAdvertiser.stopAdvertising();
                                     // Not discoverable anymore, but still connectable.
                                     setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
                                     break;
                                 case BluetoothDevice.BOND_BONDED:
                                     // Once bonded, advertise the account keys.
-                                    advertiser.startAdvertising(accountKeysServiceData());
+                                    mAdvertiser.startAdvertising(accountKeysServiceData());
                                     setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
 
                                     // If it is subsequent pair, we need to add paired device here.
-                                    if (isSubsequentPair
-                                            && secret != null
-                                            && secret.length == AES_BLOCK_LENGTH) {
-                                        addAccountKey(secret, pairingDevice);
+                                    if (mIsSubsequentPair
+                                            && mSecret != null
+                                            && mSecret.length == AES_BLOCK_LENGTH) {
+                                        addAccountKey(mSecret, mPairingDevice);
                                     }
                                     break;
                                 case BluetoothDevice.BOND_NONE:
                                     // If the bonding process fails, we should be advertising again.
-                                    advertiser.startAdvertising(getServiceData());
+                                    mAdvertiser.startAdvertising(getServiceData());
                                     break;
                                 default:
                                     break;
                             }
                             break;
                         case BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED:
-                            logger.log(
+                            mLogger.log(
                                     "Connection state to %s changed to %d",
                                     device,
                                     intent.getIntExtra(
@@ -362,7 +366,7 @@
                             break;
                         case BluetoothAdapter.ACTION_STATE_CHANGED:
                             int state = intent.getIntExtra(EXTRA_STATE, -1);
-                            logger.log("Bluetooth adapter state=%s", state);
+                            mLogger.log("Bluetooth adapter state=%s", state);
                             switch (state) {
                                 case STATE_ON:
                                     startRfcommServer();
@@ -374,7 +378,7 @@
                             }
                             break;
                         default:
-                            logger.log(new IllegalArgumentException(intent.toString()),
+                            mLogger.log(new IllegalArgumentException(intent.toString()),
                                     "Received unexpected intent");
                             break;
                     }
@@ -394,7 +398,7 @@
         return pinBytes;
     }
 
-    private final NotifiableGattServlet passkeyServlet =
+    private final NotifiableGattServlet mPasskeyServlet =
             new NotifiableGattServlet() {
                 @Override
                 // Simulating deprecated API {@code PasskeyCharacteristic.ID} for testing.
@@ -409,28 +413,28 @@
                 @Override
                 public void write(
                         BluetoothGattServerConnection connection, int offset, byte[] value) {
-                    logger.log("Got value from passkey servlet: %s", base16().encode(value));
-                    if (secret == null) {
-                        logger.log("Ignoring write to passkey characteristic, no pairing secret.");
+                    mLogger.log("Got value from passkey servlet: %s", base16().encode(value));
+                    if (mSecret == null) {
+                        mLogger.log("Ignoring write to passkey characteristic, no pairing secret.");
                         return;
                     }
 
                     try {
-                        remotePasskey = PasskeyCharacteristic.decrypt(
-                                PasskeyCharacteristic.Type.SEEKER, secret, value);
-                        if (passkeyEventCallback != null) {
-                            passkeyEventCallback.onRemotePasskeyReceived(remotePasskey);
+                        mRemotePasskey = PasskeyCharacteristic.decrypt(
+                                PasskeyCharacteristic.Type.SEEKER, mSecret, value);
+                        if (mPasskeyEventCallback != null) {
+                            mPasskeyEventCallback.onRemotePasskeyReceived(mRemotePasskey);
                         }
                         checkPasskey();
                     } catch (GeneralSecurityException e) {
-                        logger.log(
+                        mLogger.log(
                                 "Decrypting passkey value %s failed using key %s",
-                                base16().encode(value), base16().encode(secret));
+                                base16().encode(value), base16().encode(mSecret));
                     }
                 }
             };
 
-    private final NotifiableGattServlet deviceNameServlet =
+    private final NotifiableGattServlet mDeviceNameServlet =
             new NotifiableGattServlet() {
                 @Override
                 // Simulating deprecated API {@code NameCharacteristic.ID} for testing.
@@ -445,112 +449,112 @@
                 @Override
                 public void write(
                         BluetoothGattServerConnection connection, int offset, byte[] value) {
-                    logger.log("Got value from device naming servlet: %s", base16().encode(value));
-                    if (secret == null) {
-                        logger.log("Ignoring write to name characteristic, no pairing secret.");
+                    mLogger.log("Got value from device naming servlet: %s", base16().encode(value));
+                    if (mSecret == null) {
+                        mLogger.log("Ignoring write to name characteristic, no pairing secret.");
                         return;
                     }
                     // Parse the device name from seeker to write name into provider.
-                    logger.log("Got name byte array size = %d", value.length);
+                    mLogger.log("Got name byte array size = %d", value.length);
                     try {
                         String decryptedDeviceName =
-                                NamingEncoder.decodeNamingPacket(secret, value);
+                                NamingEncoder.decodeNamingPacket(mSecret, value);
                         if (decryptedDeviceName != null) {
                             setDeviceName(decryptedDeviceName.getBytes(StandardCharsets.UTF_8));
-                            logger.log("write device name = %s", decryptedDeviceName);
+                            mLogger.log("write device name = %s", decryptedDeviceName);
                         }
                     } catch (GeneralSecurityException e) {
-                        logger.log(e, "Failed to decrypt device name.");
+                        mLogger.log(e, "Failed to decrypt device name.");
                     }
                     // For testing to make sure we get the new provider name from simulator.
-                    if (writeNameCountDown != null) {
-                        logger.log("finish count down latch to write device name.");
-                        writeNameCountDown.countDown();
+                    if (mWriteNameCountDown != null) {
+                        mLogger.log("finish count down latch to write device name.");
+                        mWriteNameCountDown.countDown();
                     }
                 }
             };
 
-    private Value bluetoothAddress;
-    private final FastPairAdvertiser advertiser;
+    private Value mBluetoothAddress;
+    private final FastPairAdvertiser mAdvertiser;
     private final Map<String, BluetoothGattServerHelper> mBluetoothGattServerHelpers =
             new HashMap<>();
-    private CountDownLatch isDiscoverableLatch = new CountDownLatch(1);
-    private ScheduledFuture<?> revertDiscoverableFuture;
-    private boolean shouldFailPairing = false;
-    private boolean isDestroyed = false;
-    private boolean isAdvertising;
+    private CountDownLatch mIsDiscoverableLatch = new CountDownLatch(1);
+    private ScheduledFuture<?> mRevertDiscoverableFuture;
+    private boolean mShouldFailPairing = false;
+    private boolean mIsDestroyed = false;
+    private boolean mIsAdvertising;
     @Nullable
-    private String bleAddress;
-    private BluetoothDevice pairingDevice;
-    private int localPasskey;
-    private int remotePasskey;
+    private String mBleAddress;
+    private BluetoothDevice mPairingDevice;
+    private int mLocalPasskey;
+    private int mRemotePasskey;
     @Nullable
-    private byte[] secret;
+    private byte[] mSecret;
     @Nullable
-    private byte[] accountKey; // The latest account key added.
+    private byte[] mAccountKey; // The latest account key added.
     // The first account key added. Eddystone treats that account as the owner of the device.
     @Nullable
-    private byte[] ownerAccountKey;
+    private byte[] mOwnerAccountKey;
     @Nullable
-    private PasskeyConfirmationCallback passkeyConfirmationCallback;
+    private PasskeyConfirmationCallback mPasskeyConfirmationCallback;
     @Nullable
-    private DeviceNameCallback deviceNameCallback;
+    private DeviceNameCallback mDeviceNameCallback;
     @Nullable
-    private PasskeyEventCallback passkeyEventCallback;
-    private final List<BatteryValue> batteryValues;
-    private boolean suppressBatteryNotification = false;
-    private boolean suppressSubsequentPairingNotification = false;
-    HandshakeRequest handshakeRequest;
+    private PasskeyEventCallback mPasskeyEventCallback;
+    private final List<BatteryValue> mBatteryValues;
+    private boolean mSuppressBatteryNotification = false;
+    private boolean mSuppressSubsequentPairingNotification = false;
+    HandshakeRequest mHandshakeRequest;
     @Nullable
-    private CountDownLatch writeNameCountDown;
-    private final RfcommServer rfcommServer = new RfcommServer();
-    private final boolean dataOnlyConnection;
-    private boolean supportDynamicBufferSize = false;
-    private NotifiableGattServlet beaconActionsServlet;
-    private final FastPairSimulatorDatabase fastPairSimulatorDatabase;
-    private boolean isSubsequentPair = false;
+    private CountDownLatch mWriteNameCountDown;
+    private final RfcommServer mRfcommServer = new RfcommServer();
+    private final boolean mDataOnlyConnection;
+    private boolean mSupportDynamicBufferSize = false;
+    private NotifiableGattServlet mBeaconActionsServlet;
+    private final FastPairSimulatorDatabase mFastPairSimulatorDatabase;
+    private boolean mIsSubsequentPair = false;
 
     /** Sets the flag for failing paring for debug purpose. */
     public void setShouldFailPairing(boolean shouldFailPairing) {
-        this.shouldFailPairing = shouldFailPairing;
+        this.mShouldFailPairing = shouldFailPairing;
     }
 
     /** Gets the flag for failing paring for debug purpose. */
     public boolean getShouldFailPairing() {
-        return shouldFailPairing;
+        return mShouldFailPairing;
     }
 
     /** Clear the battery values, then no battery information is packed when advertising. */
     public void clearBatteryValues() {
-        batteryValues.clear();
+        mBatteryValues.clear();
     }
 
     /** Sets the battery items which will be included in the advertisement packet. */
     public void setBatteryValues(BatteryValue... batteryValues) {
-        this.batteryValues.clear();
-        Collections.addAll(this.batteryValues, batteryValues);
+        this.mBatteryValues.clear();
+        Collections.addAll(this.mBatteryValues, batteryValues);
     }
 
     /** Sets whether the battery advertisement packet is within suppress type or not. */
     public void setSuppressBatteryNotification(boolean suppressBatteryNotification) {
-        this.suppressBatteryNotification = suppressBatteryNotification;
+        this.mSuppressBatteryNotification = suppressBatteryNotification;
     }
 
     /** Sets whether the account key data is within suppress type or not. */
     public void setSuppressSubsequentPairingNotification(boolean isSuppress) {
-        suppressSubsequentPairingNotification = isSuppress;
+        mSuppressSubsequentPairingNotification = isSuppress;
     }
 
     /** Calls this to start advertising after some values are changed. */
     public void startAdvertising() {
-        advertiser.startAdvertising(getServiceData());
+        mAdvertiser.startAdvertising(getServiceData());
     }
 
     /** Send Event Message on to rfcomm connected devices. */
     public void sendEventStreamMessageToRfcommDevices(EventGroup eventGroup) {
         // Send fake log when event code is logging and type is not using Log_Full event.
-        if (eventGroup == EventGroup.LOGGING && !useLogFullEvent) {
-            rfcommServer.sendFakeEventStreamLoggingMessage(
+        if (eventGroup == EventGroup.LOGGING && !mUseLogFullEvent) {
+            mRfcommServer.sendFakeEventStreamLoggingMessage(
                     getDeviceName()
                             + " "
                             + getBleAddress()
@@ -558,12 +562,12 @@
                             + new SimpleDateFormat("HH:mm:ss:SSS", Locale.US)
                             .format(Calendar.getInstance().getTime()));
         } else {
-            rfcommServer.sendFakeEventStreamMessage(eventGroup);
+            mRfcommServer.sendFakeEventStreamMessage(eventGroup);
         }
     }
 
     public void setUseLogFullEvent(boolean useLogFullEvent) {
-        this.useLogFullEvent = useLogFullEvent;
+        this.mUseLogFullEvent = useLogFullEvent;
     }
 
     /** An optional way to get status updates. */
@@ -787,7 +791,7 @@
             private String mBluetoothAddress;
 
             @Nullable
-            private String mbleAddress;
+            private String mBleAddress;
 
             private boolean mDataOnlyConnection;
 
@@ -824,7 +828,7 @@
                 this.mModelId = option.mModelId;
                 this.mAdvertisingModelId = option.mAdvertisingModelId;
                 this.mBluetoothAddress = option.mBluetoothAddress;
-                this.mbleAddress = option.mBleAddress;
+                this.mBleAddress = option.mBleAddress;
                 this.mDataOnlyConnection = option.mDataOnlyConnection;
                 this.mTxPowerLevel = option.mTxPowerLevel;
                 this.mEnableNameCharacteristic = option.mEnableNameCharacteristic;
@@ -860,7 +864,7 @@
             }
 
             public Builder setBleAddress(@Nullable String bleAddress) {
-                this.mbleAddress = bleAddress;
+                this.mBleAddress = bleAddress;
                 return this;
             }
 
@@ -955,7 +959,7 @@
                         Ascii.toUpperCase(mModelId),
                         Ascii.toUpperCase(mAdvertisingModelId),
                         mBluetoothAddress,
-                        mbleAddress,
+                        mBleAddress,
                         mDataOnlyConnection,
                         mTxPowerLevel,
                         mEnableNameCharacteristic,
@@ -974,10 +978,15 @@
     }
 
     public FastPairSimulator(Context context, Options options) {
-        this.context = context;
-        this.options = options;
+        this(context, options, new Logger(TAG));
+    }
 
-        this.batteryValues = new ArrayList<>();
+    public FastPairSimulator(Context context, Options options, Logger logger) {
+        this.mContext = context;
+        this.mOptions = options;
+        this.mLogger = logger;
+
+        this.mBatteryValues = new ArrayList<>();
 
         String bluetoothAddress =
                 !TextUtils.isEmpty(options.getBluetoothAddress())
@@ -987,29 +996,29 @@
         if (bluetoothAddress == null && VERSION.SDK_INT >= VERSION_CODES.O) {
             // Requires a modified Android O build for access to bluetoothAdapter.getAddress().
             // See http://google3/java/com/google/location/nearby/apps/fastpair/simulator/README.md.
-            bluetoothAddress = bluetoothAdapter.getAddress();
+            bluetoothAddress = mBluetoothAdapter.getAddress();
         }
-        this.bluetoothAddress =
+        this.mBluetoothAddress =
                 new Value(BluetoothAddress.decode(bluetoothAddress), ByteOrder.BIG_ENDIAN);
-        this.bleAddress = options.getBleAddress();
-        this.dataOnlyConnection = options.getDataOnlyConnection();
-        this.advertiser = new OreoFastPairAdvertiser(this);
+        this.mBleAddress = options.getBleAddress();
+        this.mDataOnlyConnection = options.getDataOnlyConnection();
+        this.mAdvertiser = new OreoFastPairAdvertiser(this);
 
-        fastPairSimulatorDatabase = new FastPairSimulatorDatabase(context);
+        mFastPairSimulatorDatabase = new FastPairSimulatorDatabase(context);
 
         byte[] deviceName = getDeviceNameInBytes();
-        logger.log(
+        mLogger.log(
                 "Provider default device name is %s",
                 deviceName != null ? new String(deviceName, StandardCharsets.UTF_8) : null);
 
-        if (dataOnlyConnection) {
+        if (mDataOnlyConnection) {
             // To get BLE address, we need to start advertising first, and then
             // {@code#setBleAddress} will be called with BLE address.
-            advertiser.startAdvertising(modelIdServiceData(/* forAdvertising= */ true));
+            mAdvertiser.startAdvertising(modelIdServiceData(/* forAdvertising= */ true));
         } else {
             // Make this so that the simulator doesn't start automatically.
             // This is tricky since the simulator is used in our integ tests as well.
-            start(bleAddress != null ? bleAddress : bluetoothAddress);
+            start(mBleAddress != null ? mBleAddress : bluetoothAddress);
         }
     }
 
@@ -1019,23 +1028,22 @@
         filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
         filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
         filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
-        context.registerReceiver(broadcastReceiver, filter);
+        mContext.registerReceiver(mBroadcastReceiver, filter);
 
-        BluetoothManager bluetoothManager =
-                (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
+        BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class);
         BluetoothGattServerHelper bluetoothGattServerHelper =
-                new BluetoothGattServerHelper(context, wrap(bluetoothManager));
+                new BluetoothGattServerHelper(mContext, wrap(bluetoothManager));
         mBluetoothGattServerHelpers.put(address, bluetoothGattServerHelper);
 
-        if (options.getBecomeDiscoverable()) {
+        if (mOptions.getBecomeDiscoverable()) {
             try {
                 becomeDiscoverable();
             } catch (InterruptedException | TimeoutException e) {
-                logger.log(e, "Error becoming discoverable");
+                mLogger.log(e, "Error becoming discoverable");
             }
         }
 
-        advertiser.startAdvertising(modelIdServiceData(/* forAdvertising= */ true));
+        mAdvertiser.startAdvertising(modelIdServiceData(/* forAdvertising= */ true));
         startGattServer(bluetoothGattServerHelper);
         startRfcommServer();
         scheduleAdvertisingRefresh();
@@ -1047,16 +1055,16 @@
      */
     @SuppressWarnings("FutureReturnValueIgnored")
     private void scheduleAdvertisingRefresh() {
-        executor.scheduleAtFixedRate(
+        mExecutor.scheduleAtFixedRate(
                 () -> {
-                    if (isAdvertising) {
-                        advertiser.startAdvertising(getServiceData());
+                    if (mIsAdvertising) {
+                        mAdvertiser.startAdvertising(getServiceData());
                     }
                 },
-                options.getIsMemoryTest()
+                mOptions.getIsMemoryTest()
                         ? ADVERTISING_REFRESH_DELAY_5_MINS
                         : ADVERTISING_REFRESH_DELAY_1_MIN,
-                options.getIsMemoryTest()
+                mOptions.getIsMemoryTest()
                         ? ADVERTISING_REFRESH_DELAY_5_MINS
                         : ADVERTISING_REFRESH_DELAY_1_MIN,
                 TimeUnit.MILLISECONDS);
@@ -1064,49 +1072,50 @@
 
     public void destroy() {
         try {
-            logger.log("Destroying simulator");
-            isDestroyed = true;
-            context.unregisterReceiver(broadcastReceiver);
-            advertiser.stopAdvertising();
+            mLogger.log("Destroying simulator");
+            mIsDestroyed = true;
+            mContext.unregisterReceiver(mBroadcastReceiver);
+            mAdvertiser.stopAdvertising();
             for (BluetoothGattServerHelper helper : mBluetoothGattServerHelpers.values()) {
                 helper.close();
             }
             stopRfcommServer();
-            deviceNameCallback = null;
-            executor.shutdownNow();
+            mDeviceNameCallback = null;
+            mExecutor.shutdownNow();
         } catch (IllegalArgumentException ignored) {
             // Happens if you haven't given us permissions yet, so we didn't register the receiver.
         }
     }
 
     public boolean isDestroyed() {
-        return isDestroyed;
+        return mIsDestroyed;
     }
 
     @Nullable
     public String getBluetoothAddress() {
-        return BluetoothAddress.encode(bluetoothAddress.getBytes(ByteOrder.BIG_ENDIAN));
+        return BluetoothAddress.encode(mBluetoothAddress.getBytes(ByteOrder.BIG_ENDIAN));
     }
 
     public boolean isAdvertising() {
-        return isAdvertising;
+        return mIsAdvertising;
     }
 
     public void setIsAdvertising(boolean isAdvertising) {
-        if (this.isAdvertising != isAdvertising) {
-            this.isAdvertising = isAdvertising;
-            options.getCallback().onAdvertisingChanged();
+        if (this.mIsAdvertising != isAdvertising) {
+            this.mIsAdvertising = isAdvertising;
+            mOptions.getCallback().onAdvertisingChanged();
         }
     }
 
     public void stopAdvertising() {
-        advertiser.stopAdvertising();
+        mAdvertiser.stopAdvertising();
     }
 
     public void setBleAddress(String bleAddress) {
-        this.bleAddress = bleAddress;
-        if (dataOnlyConnection) {
-            bluetoothAddress = new Value(BluetoothAddress.decode(bleAddress), ByteOrder.BIG_ENDIAN);
+        this.mBleAddress = bleAddress;
+        if (mDataOnlyConnection) {
+            mBluetoothAddress = new Value(BluetoothAddress.decode(bleAddress),
+                    ByteOrder.BIG_ENDIAN);
             start(bleAddress);
         }
         // When BLE address changes, needs to send BLE address to the client again.
@@ -1115,28 +1124,28 @@
         // If we are advertising something other than the model id (e.g. the bloom filter), restart
         // the advertisement so that it is updated with the new address.
         if (isAdvertising() && !isDiscoverable()) {
-            advertiser.startAdvertising(getServiceData());
+            mAdvertiser.startAdvertising(getServiceData());
         }
     }
 
     @Nullable
     public String getBleAddress() {
-        return bleAddress;
+        return mBleAddress;
     }
 
     // This method is only for testing to make test block until write name success or time out.
     @VisibleForTesting
     public void setCountDownLatchToWriteName(CountDownLatch countDownLatch) {
-        logger.log("Set up count down latch to write device name.");
-        writeNameCountDown = countDownLatch;
+        mLogger.log("Set up count down latch to write device name.");
+        mWriteNameCountDown = countDownLatch;
     }
 
     public boolean areBeaconActionsNotificationsEnabled() {
-        return beaconActionsServlet.areNotificationsEnabled();
+        return mBeaconActionsServlet.areNotificationsEnabled();
     }
 
     private abstract class NotifiableGattServlet extends BluetoothGattServlet {
-        private final Map<BluetoothGattServerConnection, Notifier> connections = new HashMap<>();
+        private final Map<BluetoothGattServerConnection, Notifier> mConnections = new HashMap<>();
 
         abstract BluetoothGattCharacteristic getBaseCharacteristic();
 
@@ -1155,39 +1164,39 @@
         @Override
         public void enableNotification(BluetoothGattServerConnection connection, Notifier notifier)
                 throws BluetoothGattException {
-            logger.log("Registering notifier for %s", getCharacteristic());
-            connections.put(connection, notifier);
+            mLogger.log("Registering notifier for %s", getCharacteristic());
+            mConnections.put(connection, notifier);
         }
 
         @Override
         public void disableNotification(BluetoothGattServerConnection connection, Notifier notifier)
                 throws BluetoothGattException {
-            logger.log("Removing notifier for %s", getCharacteristic());
-            connections.remove(connection);
+            mLogger.log("Removing notifier for %s", getCharacteristic());
+            mConnections.remove(connection);
         }
 
         boolean areNotificationsEnabled() {
-            return !connections.isEmpty();
+            return !mConnections.isEmpty();
         }
 
         void sendNotification(byte[] data) {
-            if (connections.isEmpty()) {
-                logger.log("Not sending notify as no notifier registered");
+            if (mConnections.isEmpty()) {
+                mLogger.log("Not sending notify as no notifier registered");
                 return;
             }
             // Needs to be on a separate thread to avoid deadlocking and timing out (waits for a
             // callback from OS, which happens on the main thread).
-            executor.execute(
+            mExecutor.execute(
                     () -> {
                         for (Map.Entry<BluetoothGattServerConnection, Notifier> entry :
-                                connections.entrySet()) {
+                                mConnections.entrySet()) {
                             try {
-                                logger.log("Sending notify %s to %s",
+                                mLogger.log("Sending notify %s to %s",
                                         getCharacteristic(),
                                         entry.getKey().getDevice().getAddress());
                                 entry.getValue().notify(data);
                             } catch (BluetoothException e) {
-                                logger.log(
+                                mLogger.log(
                                         e,
                                         "Failed to notify (indicate) result of %s to %s",
                                         getCharacteristic(),
@@ -1199,17 +1208,17 @@
     }
 
     private void startRfcommServer() {
-        rfcommServer.setRequestHandler(this::handleRfcommServerRequest);
-        rfcommServer.setStateMonitor(state -> {
-            logger.log("RfcommServer is in %s state", state);
+        mRfcommServer.setRequestHandler(this::handleRfcommServerRequest);
+        mRfcommServer.setStateMonitor(state -> {
+            mLogger.log("RfcommServer is in %s state", state);
             if (CONNECTED.equals(state)) {
                 sendModelId();
-                sendDeviceBleAddress(bleAddress);
+                sendDeviceBleAddress(mBleAddress);
                 sendFirmwareVersion();
                 sendSessionNonce();
             }
         });
-        rfcommServer.start();
+        mRfcommServer.start();
     }
 
     private void handleRfcommServerRequest(int eventGroup, int eventCode, byte[] data) {
@@ -1221,25 +1230,25 @@
 
                 String deviceValue = base16().encode(data);
                 if (eventCode == DeviceEventCode.DEVICE_CAPABILITY_VALUE) {
-                    logger.log("Received phone capability: %s", deviceValue);
+                    mLogger.log("Received phone capability: %s", deviceValue);
                 } else if (eventCode == DeviceEventCode.PLATFORM_TYPE_VALUE) {
-                    logger.log("Received platform type: %s", deviceValue);
+                    mLogger.log("Received platform type: %s", deviceValue);
                 }
                 break;
             case EventGroup.DEVICE_ACTION_VALUE:
                 if (eventCode == DeviceActionEventCode.DEVICE_ACTION_RING_VALUE) {
-                    logger.log("receive device action with ring value, data = %d",
+                    mLogger.log("receive device action with ring value, data = %d",
                             data[0]);
                     sendDeviceRingActionResponse();
                     // Simulate notifying the seeker that the ringing has stopped due
                     // to user interaction (such as tapping the bud).
-                    uiThreadHandler.postDelayed(this::sendDeviceRingStoppedAction,
+                    mUiThreadHandler.postDelayed(this::sendDeviceRingStoppedAction,
                             5000);
                 }
                 break;
             case EventGroup.DEVICE_CONFIGURATION_VALUE:
                 if (eventCode == DeviceConfigurationEventCode.CONFIGURATION_BUFFER_SIZE_VALUE) {
-                    logger.log(
+                    mLogger.log(
                             "receive device action with buffer size value, data = %s",
                             base16().encode(data));
                     sendSetBufferActionResponse(data);
@@ -1247,7 +1256,7 @@
                 break;
             case EventGroup.DEVICE_CAPABILITY_SYNC_VALUE:
                 if (eventCode == DeviceCapabilitySyncEventCode.REQUEST_CAPABILITY_UPDATE_VALUE) {
-                    logger.log("receive device capability update request.");
+                    mLogger.log("receive device capability update request.");
                     sendCapabilitySync();
                 }
                 break;
@@ -1257,23 +1266,23 @@
     }
 
     private void stopRfcommServer() {
-        rfcommServer.stop();
-        rfcommServer.setRequestHandler(null);
-        rfcommServer.setStateMonitor(null);
+        mRfcommServer.stop();
+        mRfcommServer.setRequestHandler(null);
+        mRfcommServer.setStateMonitor(null);
     }
 
     private void sendModelId() {
-        logger.log("Send model ID to the client");
-        rfcommServer.send(
+        mLogger.log("Send model ID to the client");
+        mRfcommServer.send(
                 EventGroup.DEVICE_VALUE,
                 DeviceEventCode.DEVICE_MODEL_ID_VALUE,
                 modelIdServiceData(/* forAdvertising= */ false));
     }
 
     private void sendDeviceBleAddress(String bleAddress) {
-        logger.log("Send BLE address (%s) to the client", bleAddress);
+        mLogger.log("Send BLE address (%s) to the client", bleAddress);
         if (bleAddress != null) {
-            rfcommServer.send(
+            mRfcommServer.send(
                     EventGroup.DEVICE_VALUE,
                     DeviceEventCode.DEVICE_BLE_ADDRESS_VALUE,
                     BluetoothAddress.decode(bleAddress));
@@ -1281,25 +1290,25 @@
     }
 
     private void sendFirmwareVersion() {
-        logger.log("Send Firmware Version (%s) to the client", deviceFirmwareVersion);
-        rfcommServer.send(
+        mLogger.log("Send Firmware Version (%s) to the client", mDeviceFirmwareVersion);
+        mRfcommServer.send(
                 EventGroup.DEVICE_VALUE,
                 DeviceEventCode.FIRMWARE_VERSION_VALUE,
-                deviceFirmwareVersion.getBytes());
+                mDeviceFirmwareVersion.getBytes());
     }
 
     private void sendSessionNonce() {
-        logger.log("Send SessionNonce (%s) to the client", deviceFirmwareVersion);
+        mLogger.log("Send SessionNonce (%s) to the client", mDeviceFirmwareVersion);
         SecureRandom secureRandom = new SecureRandom();
-        sessionNonce = new byte[SECTION_NONCE_LENGTH];
-        secureRandom.nextBytes(sessionNonce);
-        rfcommServer.send(
-                EventGroup.DEVICE_VALUE, DeviceEventCode.SECTION_NONCE_VALUE, sessionNonce);
+        mSessionNonce = new byte[SECTION_NONCE_LENGTH];
+        secureRandom.nextBytes(mSessionNonce);
+        mRfcommServer.send(
+                EventGroup.DEVICE_VALUE, DeviceEventCode.SECTION_NONCE_VALUE, mSessionNonce);
     }
 
     private void sendDeviceRingActionResponse() {
-        logger.log("Send device ring action response to the client");
-        rfcommServer.send(
+        mLogger.log("Send device ring action response to the client");
+        mRfcommServer.send(
                 EventGroup.ACKNOWLEDGEMENT_VALUE,
                 AcknowledgementEventCode.ACKNOWLEDGEMENT_ACK_VALUE,
                 new byte[]{
@@ -1313,9 +1322,9 @@
         for (ByteString accountKey : getAccountKeys()) {
             try {
                 if (MessageStreamHmacEncoder.verifyHmac(
-                        accountKey.toByteArray(), sessionNonce, data)) {
+                        accountKey.toByteArray(), mSessionNonce, data)) {
                     hmacPassed = true;
-                    logger.log("Buffer size data matches account key %s",
+                    mLogger.log("Buffer size data matches account key %s",
                             base16().encode(accountKey.toByteArray()));
                     break;
                 }
@@ -1324,8 +1333,8 @@
             }
         }
         if (hmacPassed) {
-            logger.log("Send buffer size action response %s to the client", base16().encode(data));
-            rfcommServer.send(
+            mLogger.log("Send buffer size action response %s to the client", base16().encode(data));
+            mRfcommServer.send(
                     EventGroup.ACKNOWLEDGEMENT_VALUE,
                     AcknowledgementEventCode.ACKNOWLEDGEMENT_ACK_VALUE,
                     new byte[]{
@@ -1336,15 +1345,15 @@
                             data[2]
                     });
         } else {
-            logger.log("No matched account key for sendSetBufferActionResponse");
+            mLogger.log("No matched account key for sendSetBufferActionResponse");
         }
     }
 
     private void sendCapabilitySync() {
-        logger.log("Send capability sync to the client");
-        if (supportDynamicBufferSize) {
-            logger.log("Send dynamic buffer size range to the client");
-            rfcommServer.send(
+        mLogger.log("Send capability sync to the client");
+        if (mSupportDynamicBufferSize) {
+            mLogger.log("Send dynamic buffer size range to the client");
+            mRfcommServer.send(
                     EventGroup.DEVICE_CAPABILITY_SYNC_VALUE,
                     DeviceCapabilitySyncEventCode.CONFIGURABLE_BUFFER_SIZE_RANGE_VALUE,
                     new byte[]{
@@ -1358,8 +1367,8 @@
     }
 
     private void sendDeviceRingStoppedAction() {
-        logger.log("Sending device ring stopped action to the client");
-        rfcommServer.send(
+        mLogger.log("Sending device ring stopped action to the client");
+        mRfcommServer.send(
                 EventGroup.DEVICE_ACTION_VALUE,
                 DeviceActionEventCode.DEVICE_ACTION_RING_VALUE,
                 // Additional data for stopping ringing on all components.
@@ -1379,7 +1388,7 @@
                     public void write(
                             BluetoothGattServerConnection connection, int offset, byte[] value)
                             throws BluetoothGattException {
-                        logger.log("Requested TDS Control Point write, value=%s",
+                        mLogger.log("Requested TDS Control Point write, value=%s",
                                 base16().encode(value));
 
                         ResultCode resultCode = checkTdsControlPointRequest(value);
@@ -1387,19 +1396,19 @@
                             try {
                                 becomeDiscoverable();
                             } catch (TimeoutException | InterruptedException e) {
-                                logger.log(e, "Failed to become discoverable");
+                                mLogger.log(e, "Failed to become discoverable");
                                 resultCode = ResultCode.OPERATION_FAILED;
                             }
                         }
 
-                        logger.log("Request complete, resultCode=%s", resultCode);
+                        mLogger.log("Request complete, resultCode=%s", resultCode);
 
-                        logger.log("Sending TDS Control Point response indication");
+                        mLogger.log("Sending TDS Control Point response indication");
                         sendNotification(
                                 Bytes.concat(
                                         new byte[]{
                                                 getTdsControlPointOpCode(value),
-                                                resultCode.byteValue,
+                                                resultCode.mByteValue,
                                         },
                                         resultCode == ResultCode.SUCCESS
                                                 ? TDS_CONTROL_POINT_RESPONSE_PARAMETER
@@ -1420,7 +1429,7 @@
                     public byte[] read(BluetoothGattServerConnection connection, int offset) {
                         return Bytes.concat(
                                 new byte[]{BrHandoverDataCharacteristic.BR_EDR_FEATURES},
-                                bluetoothAddress.getBytes(ByteOrder.LITTLE_ENDIAN),
+                                mBluetoothAddress.getBytes(ByteOrder.LITTLE_ENDIAN),
                                 CLASS_OF_DEVICE.getBytes(ByteOrder.LITTLE_ENDIAN));
                     }
                 };
@@ -1436,7 +1445,7 @@
                                         0 /* no properties */,
                                         0 /* no permissions */);
 
-                        if (options.getIncludeTransportDataDescriptor()) {
+                        if (mOptions.getIncludeTransportDataDescriptor()) {
                             characteristic.addDescriptor(
                                     new BluetoothGattDescriptor(
                                             TransportDiscoveryService.BluetoothSigDataCharacteristic
@@ -1471,16 +1480,16 @@
                     @Override
                     public void write(
                             BluetoothGattServerConnection connection, int offset, byte[] value) {
-                        logger.log("Got value from account key servlet: %s",
+                        mLogger.log("Got value from account key servlet: %s",
                                 base16().encode(value));
                         try {
-                            addAccountKey(AesEcbSingleBlockEncryption.decrypt(secret, value),
-                                    pairingDevice);
+                            addAccountKey(AesEcbSingleBlockEncryption.decrypt(mSecret, value),
+                                    mPairingDevice);
                         } catch (GeneralSecurityException e) {
-                            logger.log(e, "Failed to decrypt account key.");
+                            mLogger.log(e, "Failed to decrypt account key.");
                         }
-                        uiThreadHandler.post(
-                                () -> advertiser.startAdvertising(accountKeysServiceData()));
+                        mUiThreadHandler.post(
+                                () -> mAdvertiser.startAdvertising(accountKeysServiceData()));
                     }
                 };
 
@@ -1494,7 +1503,7 @@
 
                     @Override
                     public byte[] read(BluetoothGattServerConnection connection, int offset) {
-                        return deviceFirmwareVersion.getBytes();
+                        return mDeviceFirmwareVersion.getBytes();
                     }
                 };
 
@@ -1514,10 +1523,10 @@
                     @Override
                     public void write(
                             BluetoothGattServerConnection connection, int offset, byte[] value) {
-                        logger.log("Requesting key based pairing handshake, value=%s",
+                        mLogger.log("Requesting key based pairing handshake, value=%s",
                                 base16().encode(value));
 
-                        secret = null;
+                        mSecret = null;
                         byte[] seekerPublicAddress = null;
                         if (value.length == AES_BLOCK_LENGTH) {
 
@@ -1525,67 +1534,67 @@
                                 byte[] candidateSecret = key.toByteArray();
                                 try {
                                     seekerPublicAddress = handshake(candidateSecret, value);
-                                    secret = candidateSecret;
-                                    isSubsequentPair = true;
+                                    mSecret = candidateSecret;
+                                    mIsSubsequentPair = true;
                                     break;
                                 } catch (GeneralSecurityException e) {
-                                    logger.log(e, "Failed to decrypt with %s",
+                                    mLogger.log(e, "Failed to decrypt with %s",
                                             base16().encode(candidateSecret));
                                 }
                             }
                         } else if (value.length == AES_BLOCK_LENGTH + PUBLIC_KEY_LENGTH
-                                && options.getAntiSpoofingPrivateKey() != null) {
+                                && mOptions.getAntiSpoofingPrivateKey() != null) {
                             try {
                                 byte[] encryptedRequest = Arrays.copyOf(value, AES_BLOCK_LENGTH);
                                 byte[] receivedPublicKey =
                                         Arrays.copyOfRange(value, AES_BLOCK_LENGTH, value.length);
                                 byte[] candidateSecret =
                                         EllipticCurveDiffieHellmanExchange.create(
-                                                        options.getAntiSpoofingPrivateKey())
+                                                        mOptions.getAntiSpoofingPrivateKey())
                                                 .generateSecret(receivedPublicKey);
                                 seekerPublicAddress = handshake(candidateSecret, encryptedRequest);
-                                secret = candidateSecret;
+                                mSecret = candidateSecret;
                             } catch (Exception e) {
-                                logger.log(
+                                mLogger.log(
                                         e,
                                         "Failed to decrypt with anti-spoofing private key %s",
-                                        base16().encode(options.getAntiSpoofingPrivateKey()));
+                                        base16().encode(mOptions.getAntiSpoofingPrivateKey()));
                             }
                         } else {
-                            logger.log("Packet length invalid, %d", value.length);
+                            mLogger.log("Packet length invalid, %d", value.length);
                             return;
                         }
 
-                        if (secret == null) {
-                            logger.log("Couldn't find a usable key to decrypt with.");
+                        if (mSecret == null) {
+                            mLogger.log("Couldn't find a usable key to decrypt with.");
                             return;
                         }
 
-                        logger.log("Found valid decryption key, %s", base16().encode(secret));
+                        mLogger.log("Found valid decryption key, %s", base16().encode(mSecret));
                         byte[] salt = new byte[9];
                         new Random().nextBytes(salt);
                         try {
                             byte[] data = concat(
                                     new byte[]{KeyBasedPairingCharacteristic.Response.TYPE},
-                                    bluetoothAddress.getBytes(ByteOrder.BIG_ENDIAN), salt);
-                            byte[] encryptedAddress = encrypt(secret, data);
-                            logger.log(
+                                    mBluetoothAddress.getBytes(ByteOrder.BIG_ENDIAN), salt);
+                            byte[] encryptedAddress = encrypt(mSecret, data);
+                            mLogger.log(
                                     "Sending handshake response %s with size %d",
                                     base16().encode(encryptedAddress), encryptedAddress.length);
                             sendNotification(encryptedAddress);
 
                             // Notify seeker for NameCharacteristic to get provider device name
                             // when seeker request device name flag is true.
-                            if (options.getEnableNameCharacteristic()
-                                    && handshakeRequest.requestDeviceName()) {
+                            if (mOptions.getEnableNameCharacteristic()
+                                    && mHandshakeRequest.requestDeviceName()) {
                                 byte[] encryptedResponse =
                                         getDeviceNameInBytes() != null ? createEncryptedDeviceName()
                                                 : new byte[0];
-                                logger.log(
+                                mLogger.log(
                                         "Sending device name response %s with size %d",
                                         base16().encode(encryptedResponse),
                                         encryptedResponse.length);
-                                deviceNameServlet.sendNotification(encryptedResponse);
+                                mDeviceNameServlet.sendNotification(encryptedResponse);
                             }
 
                             // Disconnects the current connection to allow the following pairing
@@ -1598,77 +1607,77 @@
                             // If headphones support multiple simultaneous connections, they
                             // should stay connected. But Android fails to pair with the new
                             // device if we don't first disconnect from any other device.
-                            logger.log("Skip remove bond, value=%s",
-                                    options.getRemoveAllDevicesDuringPairing());
-                            if (options.getRemoveAllDevicesDuringPairing()
-                                    && handshakeRequest.getType()
+                            mLogger.log("Skip remove bond, value=%s",
+                                    mOptions.getRemoveAllDevicesDuringPairing());
+                            if (mOptions.getRemoveAllDevicesDuringPairing()
+                                    && mHandshakeRequest.getType()
                                     == HandshakeRequest.Type.KEY_BASED_PAIRING_REQUEST
-                                    && !handshakeRequest.requestRetroactivePair()) {
-                                executor.execute(() -> disconnect());
+                                    && !mHandshakeRequest.requestRetroactivePair()) {
+                                mExecutor.execute(() -> disconnectAllBondedDevices());
                             }
 
-                            if (handshakeRequest.getType()
+                            if (mHandshakeRequest.getType()
                                     == HandshakeRequest.Type.KEY_BASED_PAIRING_REQUEST
-                                    && handshakeRequest.requestProviderInitialBonding()) {
+                                    && mHandshakeRequest.requestProviderInitialBonding()) {
                                 // Run on executor to ensure it doesn't happen until after the
                                 // notify (which tells the remote device what address to expect).
                                 String seekerPublicAddressString =
                                         BluetoothAddress.encode(seekerPublicAddress);
-                                executor.execute(() -> {
-                                    logger.log("Sending pairing request to %s",
+                                mExecutor.execute(() -> {
+                                    mLogger.log("Sending pairing request to %s",
                                             seekerPublicAddressString);
-                                    bluetoothAdapter.getRemoteDevice(
+                                    mBluetoothAdapter.getRemoteDevice(
                                             seekerPublicAddressString).createBond();
                                 });
                             }
                         } catch (GeneralSecurityException e) {
-                            logger.log(e, "Failed to notify of static mac address");
+                            mLogger.log(e, "Failed to notify of static mac address");
                         }
                     }
 
                     @Nullable
                     private byte[] handshake(byte[] key, byte[] encryptedPairingRequest)
                             throws GeneralSecurityException {
-                        handshakeRequest = new HandshakeRequest(key, encryptedPairingRequest);
+                        mHandshakeRequest = new HandshakeRequest(key, encryptedPairingRequest);
 
-                        byte[] decryptedAddress = handshakeRequest.getVerificationData();
-                        if (bleAddress != null
+                        byte[] decryptedAddress = mHandshakeRequest.getVerificationData();
+                        if (mBleAddress != null
                                 && Arrays.equals(decryptedAddress,
-                                BluetoothAddress.decode(bleAddress))
+                                BluetoothAddress.decode(mBleAddress))
                                 || Arrays.equals(decryptedAddress,
-                                bluetoothAddress.getBytes(ByteOrder.BIG_ENDIAN))) {
-                            logger.log("Address matches: %s", base16().encode(decryptedAddress));
+                                mBluetoothAddress.getBytes(ByteOrder.BIG_ENDIAN))) {
+                            mLogger.log("Address matches: %s", base16().encode(decryptedAddress));
                         } else {
                             throw new GeneralSecurityException(
                                     "Address (BLE or BR/EDR) is not correct: "
                                             + base16().encode(decryptedAddress)
                                             + ", "
-                                            + bleAddress
+                                            + mBleAddress
                                             + ", "
                                             + getBluetoothAddress());
                         }
 
-                        switch (handshakeRequest.getType()) {
+                        switch (mHandshakeRequest.getType()) {
                             case KEY_BASED_PAIRING_REQUEST:
-                                return handleKeyBasedPairingRequest(handshakeRequest);
+                                return handleKeyBasedPairingRequest(mHandshakeRequest);
                             case ACTION_OVER_BLE:
-                                return handleActionOverBleRequest(handshakeRequest);
+                                return handleActionOverBleRequest(mHandshakeRequest);
                             case UNKNOWN:
                                 // continue to throw the exception;
                         }
                         throw new GeneralSecurityException(
-                                "Type is not correct: " + handshakeRequest.getType());
+                                "Type is not correct: " + mHandshakeRequest.getType());
                     }
 
                     @Nullable
                     private byte[] handleKeyBasedPairingRequest(HandshakeRequest handshakeRequest)
                             throws GeneralSecurityException {
                         if (handshakeRequest.requestDiscoverable()) {
-                            logger.log("Requested discoverability");
+                            mLogger.log("Requested discoverability");
                             setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
                         }
 
-                        logger.log(
+                        mLogger.log(
                                 "KeyBasedPairing: initialBonding=%s, requestDeviceName=%s, "
                                         + "retroactivePair=%s",
                                 handshakeRequest.requestProviderInitialBonding(),
@@ -1679,13 +1688,14 @@
                         if (handshakeRequest.requestProviderInitialBonding()
                                 || handshakeRequest.requestRetroactivePair()) {
                             seekerPublicAddress = handshakeRequest.getSeekerPublicAddress();
-                            logger.log(
+                            mLogger.log(
                                     "Seeker sends BR/EDR address %s to provider",
                                     BluetoothAddress.encode(seekerPublicAddress));
                         }
 
                         if (handshakeRequest.requestRetroactivePair()) {
-                            if (bluetoothAdapter.getRemoteDevice(seekerPublicAddress).getBondState()
+                            if (mBluetoothAdapter.getRemoteDevice(
+                                    seekerPublicAddress).getBondState()
                                     != BluetoothDevice.BOND_BONDED) {
                                 throw new GeneralSecurityException(
                                         "Address (BR/EDR) is not bonded: "
@@ -1700,14 +1710,14 @@
                     private byte[] handleActionOverBleRequest(HandshakeRequest handshakeRequest) {
                         // TODO(wollohchou): implement action over ble request.
                         if (handshakeRequest.requestDeviceAction()) {
-                            logger.log("Requesting action over BLE, device action");
+                            mLogger.log("Requesting action over BLE, device action");
                         } else if (handshakeRequest.requestFollowedByAdditionalData()) {
-                            logger.log(
+                            mLogger.log(
                                     "Requesting action over BLE, followed by additional data, "
                                             + "type:%s",
                                     handshakeRequest.getAdditionalDataType());
                         } else {
-                            logger.log("Requesting action over BLE");
+                            mLogger.log("Requesting action over BLE");
                         }
                         return null;
                     }
@@ -1718,14 +1728,14 @@
                     private byte[] createEncryptedDeviceName() throws GeneralSecurityException {
                         byte[] deviceName = getDeviceNameInBytes();
                         String providerName = new String(deviceName, StandardCharsets.UTF_8);
-                        logger.log(
+                        mLogger.log(
                                 "Sending handshake response for device name %s with size %d",
                                 providerName, deviceName.length);
-                        return NamingEncoder.encodeNamingPacket(secret, providerName);
+                        return NamingEncoder.encodeNamingPacket(mSecret, providerName);
                     }
                 };
 
-        beaconActionsServlet =
+        mBeaconActionsServlet =
                 new NotifiableGattServlet() {
                     private static final int GATT_ERROR_UNAUTHENTICATED = 0x80;
                     private static final int GATT_ERROR_INVALID_VALUE = 0x81;
@@ -1735,17 +1745,17 @@
                     private static final int IDENTITY_KEY_LENGTH = 32;
                     private static final byte TRANSMISSION_POWER = 0;
 
-                    private final SecureRandom random = new SecureRandom();
-                    private final MessageDigest sha256;
+                    private final SecureRandom mRandom = new SecureRandom();
+                    private final MessageDigest mSha256;
                     @Nullable
-                    private byte[] lastNonce;
+                    private byte[] mLastNonce;
                     @Nullable
-                    private ByteString identityKey = options.getEddystoneIdentityKey();
+                    private ByteString mIdentityKey = mOptions.getEddystoneIdentityKey();
 
                     {
                         try {
-                            sha256 = MessageDigest.getInstance("SHA-256");
-                            sha256.reset();
+                            mSha256 = MessageDigest.getInstance("SHA-256");
+                            mSha256.reset();
                         } catch (NoSuchAlgorithmException e) {
                             throw new IllegalStateException(
                                     "System missing SHA-256 implementation.", e);
@@ -1764,19 +1774,19 @@
 
                     @Override
                     public byte[] read(BluetoothGattServerConnection connection, int offset) {
-                        lastNonce = new byte[NONCE_LENGTH];
-                        random.nextBytes(lastNonce);
-                        return lastNonce;
+                        mLastNonce = new byte[NONCE_LENGTH];
+                        mRandom.nextBytes(mLastNonce);
+                        return mLastNonce;
                     }
 
                     @Override
                     public void write(
                             BluetoothGattServerConnection connection, int offset, byte[] value)
                             throws BluetoothGattException {
-                        logger.log("Got value from beacon actions servlet: %s",
+                        mLogger.log("Got value from beacon actions servlet: %s",
                                 base16().encode(value));
                         if (value.length == 0) {
-                            logger.log("Packet length invalid, %d", value.length);
+                            mLogger.log("Packet length invalid, %d", value.length);
                             throw new BluetoothGattException("Packet length invalid",
                                     GATT_ERROR_INVALID_VALUE);
                         }
@@ -1807,7 +1817,7 @@
                     private boolean verifyAccountKeyToken(byte[] value, boolean ownerOnly)
                             throws BluetoothGattException {
                         if (value.length < ONE_TIME_AUTH_KEY_LENGTH + ONE_TIME_AUTH_KEY_OFFSET) {
-                            logger.log("Packet length invalid, %d", value.length);
+                            mLogger.log("Packet length invalid, %d", value.length);
                             throw new BluetoothGattException(
                                     "Packet length invalid", GATT_ERROR_INVALID_VALUE);
                         }
@@ -1816,27 +1826,28 @@
                                         value,
                                         ONE_TIME_AUTH_KEY_OFFSET,
                                         ONE_TIME_AUTH_KEY_LENGTH + ONE_TIME_AUTH_KEY_OFFSET);
-                        if (lastNonce == null) {
+                        if (mLastNonce == null) {
                             throw new BluetoothGattException(
                                     "Nonce wasn't set", GATT_ERROR_UNAUTHENTICATED);
                         }
                         if (ownerOnly) {
                             ByteString accountKey = getOwnerAccountKey();
                             if (accountKey != null) {
-                                sha256.update(accountKey.toByteArray());
-                                sha256.update(lastNonce);
+                                mSha256.update(accountKey.toByteArray());
+                                mSha256.update(mLastNonce);
                                 return Arrays.equals(
                                         hashedAccountKey,
-                                        Arrays.copyOf(sha256.digest(), ONE_TIME_AUTH_KEY_LENGTH));
+                                        Arrays.copyOf(mSha256.digest(), ONE_TIME_AUTH_KEY_LENGTH));
                             }
                         } else {
                             Set<ByteString> accountKeys = getAccountKeys();
                             for (ByteString accountKey : accountKeys) {
-                                sha256.update(accountKey.toByteArray());
-                                sha256.update(lastNonce);
+                                mSha256.update(accountKey.toByteArray());
+                                mSha256.update(mLastNonce);
                                 if (Arrays.equals(
                                         hashedAccountKey,
-                                        Arrays.copyOf(sha256.digest(), ONE_TIME_AUTH_KEY_LENGTH))) {
+                                        Arrays.copyOf(mSha256.digest(),
+                                                ONE_TIME_AUTH_KEY_LENGTH))) {
                                     return true;
                                 }
                             }
@@ -1889,7 +1900,7 @@
                         if (verifyAccountKeyToken(value, /* ownerOnly= */ true)) {
                             flags |= (byte) (1 << 1);
                         }
-                        if (identityKey == null) {
+                        if (mIdentityKey == null) {
                             sendNotification(
                                     fromBytes(
                                             (byte) BeaconActionType.READ_PROVISIONING_STATE,
@@ -1905,7 +1916,7 @@
                                             flags)
                                             .concat(
                                                     E2eeCalculator.computeE2eeEid(
-                                                            identityKey, /* exponent= */ 10,
+                                                            mIdentityKey, /* exponent= */ 10,
                                                             getBeaconClock()))
                                             .toByteArray());
                         }
@@ -1921,17 +1932,17 @@
                         if (value.length
                                 != ONE_TIME_AUTH_KEY_LENGTH + ONE_TIME_AUTH_KEY_OFFSET
                                 + IDENTITY_KEY_LENGTH) {
-                            logger.log("Packet length invalid, %d", value.length);
+                            mLogger.log("Packet length invalid, %d", value.length);
                             throw new BluetoothGattException("Packet length invalid",
                                     GATT_ERROR_INVALID_VALUE);
                         }
-                        if (identityKey != null) {
+                        if (mIdentityKey != null) {
                             throw new BluetoothGattException(
                                     "Device is already provisioned as Eddystone",
                                     GATT_ERROR_UNAUTHENTICATED);
                         }
-                        identityKey = Crypto.aesEcbNoPaddingDecrypt(
-                                ByteString.copyFrom(ownerAccountKey),
+                        mIdentityKey = Crypto.aesEcbNoPaddingDecrypt(
+                                ByteString.copyFrom(mOwnerAccountKey),
                                 ByteString.copyFrom(value)
                                         .substring(ONE_TIME_AUTH_KEY_LENGTH
                                                 + ONE_TIME_AUTH_KEY_OFFSET));
@@ -1942,10 +1953,10 @@
                 new ServiceConfig()
                         .addCharacteristic(accountKeyServlet)
                         .addCharacteristic(keyBasedPairingServlet)
-                        .addCharacteristic(passkeyServlet)
+                        .addCharacteristic(mPasskeyServlet)
                         .addCharacteristic(firmwareVersionServlet);
-        if (options.getEnableBeaconActionsCharacteristic()) {
-            fastPairServiceConfig.addCharacteristic(beaconActionsServlet);
+        if (mOptions.getEnableBeaconActionsCharacteristic()) {
+            fastPairServiceConfig.addCharacteristic(mBeaconActionsServlet);
         }
 
         BluetoothGattServerConfig config =
@@ -1958,17 +1969,18 @@
                                         .addCharacteristic(bluetoothSigServlet))
                         .addService(
                                 FastPairService.ID,
-                                options.getEnableNameCharacteristic()
-                                        ? fastPairServiceConfig.addCharacteristic(deviceNameServlet)
+                                mOptions.getEnableNameCharacteristic()
+                                        ? fastPairServiceConfig.addCharacteristic(
+                                        mDeviceNameServlet)
                                         : fastPairServiceConfig);
 
-        logger.log(
+        mLogger.log(
                 "Starting GATT server, support name characteristic %b",
-                options.getEnableNameCharacteristic());
+                mOptions.getEnableNameCharacteristic());
         try {
             helper.open(config);
         } catch (BluetoothException e) {
-            logger.log(e, "Error starting GATT server");
+            mLogger.log(e, "Error starting GATT server");
         }
     }
 
@@ -1978,36 +1990,37 @@
     }
 
     public void enterPassKey(int passkey) {
-        logger.log("enterPassKey called with passkey %d.", passkey);
+        mLogger.log("enterPassKey called with passkey %d.", passkey);
         try {
             boolean result =
-                    (Boolean) Reflect.on(pairingDevice).withMethod("setPasskey", int.class).get(
+                    (Boolean) Reflect.on(mPairingDevice).withMethod("setPasskey", int.class).get(
                             passkey);
-            logger.log("enterPassKey called with result %b", result);
+            mLogger.log("enterPassKey called with result %b", result);
         } catch (ReflectionException e) {
-            logger.log("enterPassKey meet Exception %s.", e.getMessage());
+            mLogger.log("enterPassKey meet Exception %s.", e.getMessage());
         }
     }
 
     private void checkPasskey() {
         // There's a race between the PAIRING_REQUEST broadcast from the OS giving us the local
         // passkey, and the remote passkey received over GATT. Skip the check until we have both.
-        if (localPasskey == 0 || remotePasskey == 0) {
-            logger.log(
+        if (mLocalPasskey == 0 || mRemotePasskey == 0) {
+            mLogger.log(
                     "Skipping passkey check, missing local (%s) or remote (%s).",
-                    localPasskey, remotePasskey);
+                    mLocalPasskey, mRemotePasskey);
             return;
         }
 
         // Regardless of whether it matches, send our (encrypted) passkey to the seeker.
-        sendPasskeyToRemoteDevice(localPasskey);
+        sendPasskeyToRemoteDevice(mLocalPasskey);
 
-        logger.log("Checking localPasskey %s == remotePasskey %s", localPasskey, remotePasskey);
-        boolean passkeysMatched = localPasskey == remotePasskey;
-        if (options.getShowsPasskeyConfirmation() && passkeysMatched
-                && passkeyEventCallback != null) {
-            logger.log("callbacks the UI for passkey confirmation.");
-            passkeyEventCallback.onPasskeyConfirmation(localPasskey, this::setPasskeyConfirmation);
+        mLogger.log("Checking localPasskey %s == remotePasskey %s", mLocalPasskey, mRemotePasskey);
+        boolean passkeysMatched = mLocalPasskey == mRemotePasskey;
+        if (mOptions.getShowsPasskeyConfirmation() && passkeysMatched
+                && mPasskeyEventCallback != null) {
+            mLogger.log("callbacks the UI for passkey confirmation.");
+            mPasskeyEventCallback.onPasskeyConfirmation(mLocalPasskey,
+                    this::setPasskeyConfirmation);
         } else {
             setPasskeyConfirmation(passkeysMatched);
         }
@@ -2015,45 +2028,45 @@
 
     private void sendPasskeyToRemoteDevice(int passkey) {
         try {
-            passkeyServlet.sendNotification(
+            mPasskeyServlet.sendNotification(
                     PasskeyCharacteristic.encrypt(
-                            PasskeyCharacteristic.Type.PROVIDER, secret, passkey));
+                            PasskeyCharacteristic.Type.PROVIDER, mSecret, passkey));
         } catch (GeneralSecurityException e) {
-            logger.log(e, "Failed to encrypt passkey response.");
+            mLogger.log(e, "Failed to encrypt passkey response.");
         }
     }
 
     public void setFirmwareVersion(String versionNumber) {
-        deviceFirmwareVersion = versionNumber;
+        mDeviceFirmwareVersion = versionNumber;
     }
 
     public void setDynamicBufferSize(boolean support) {
-        if (supportDynamicBufferSize != support) {
-            supportDynamicBufferSize = support;
+        if (mSupportDynamicBufferSize != support) {
+            mSupportDynamicBufferSize = support;
             sendCapabilitySync();
         }
     }
 
     @VisibleForTesting
     void setPasskeyConfirmationCallback(PasskeyConfirmationCallback callback) {
-        this.passkeyConfirmationCallback = callback;
+        this.mPasskeyConfirmationCallback = callback;
     }
 
     public void setDeviceNameCallback(DeviceNameCallback callback) {
-        this.deviceNameCallback = callback;
+        this.mDeviceNameCallback = callback;
     }
 
     public void setPasskeyEventCallback(PasskeyEventCallback passkeyEventCallback) {
-        this.passkeyEventCallback = passkeyEventCallback;
+        this.mPasskeyEventCallback = passkeyEventCallback;
     }
 
     private void setPasskeyConfirmation(boolean confirm) {
-        pairingDevice.setPairingConfirmation(confirm);
-        if (passkeyConfirmationCallback != null) {
-            passkeyConfirmationCallback.onPasskeyConfirmation(confirm);
+        mPairingDevice.setPairingConfirmation(confirm);
+        if (mPasskeyConfirmationCallback != null) {
+            mPasskeyConfirmationCallback.onPasskeyConfirmation(confirm);
         }
-        localPasskey = 0;
-        remotePasskey = 0;
+        mLocalPasskey = 0;
+        mRemotePasskey = 0;
     }
 
     private void becomeDiscoverable() throws InterruptedException, TimeoutException {
@@ -2066,39 +2079,39 @@
 
     private void setDiscoverable(boolean discoverable)
             throws InterruptedException, TimeoutException {
-        isDiscoverableLatch = new CountDownLatch(1);
+        mIsDiscoverableLatch = new CountDownLatch(1);
         setScanMode(discoverable ? SCAN_MODE_CONNECTABLE_DISCOVERABLE : SCAN_MODE_CONNECTABLE);
         // If we're already discoverable, count down the latch right away. Otherwise,
         // we'll get a broadcast when we successfully become discoverable.
         if (isDiscoverable()) {
-            isDiscoverableLatch.countDown();
+            mIsDiscoverableLatch.countDown();
         }
-        if (isDiscoverableLatch.await(3, TimeUnit.SECONDS)) {
-            logger.log("Successfully became switched discoverable mode %s", discoverable);
+        if (mIsDiscoverableLatch.await(3, TimeUnit.SECONDS)) {
+            mLogger.log("Successfully became switched discoverable mode %s", discoverable);
         } else {
             throw new TimeoutException();
         }
     }
 
     private void setScanMode(int scanMode) {
-        if (revertDiscoverableFuture != null) {
-            revertDiscoverableFuture.cancel(false /* may interrupt if running */);
+        if (mRevertDiscoverableFuture != null) {
+            mRevertDiscoverableFuture.cancel(false /* may interrupt if running */);
         }
 
-        logger.log("Setting scan mode to %s", scanModeToString(scanMode));
+        mLogger.log("Setting scan mode to %s", scanModeToString(scanMode));
         try {
-            Method method = bluetoothAdapter.getClass().getMethod("setScanMode", Integer.TYPE);
-            method.invoke(bluetoothAdapter, scanMode);
+            Method method = mBluetoothAdapter.getClass().getMethod("setScanMode", Integer.TYPE);
+            method.invoke(mBluetoothAdapter, scanMode);
 
             if (scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
-                revertDiscoverableFuture =
-                        executor.schedule(
+                mRevertDiscoverableFuture =
+                        mExecutor.schedule(
                                 () -> setScanMode(SCAN_MODE_CONNECTABLE),
-                                options.getIsMemoryTest() ? 300 : 30,
+                                mOptions.getIsMemoryTest() ? 300 : 30,
                                 TimeUnit.SECONDS);
             }
         } catch (Exception e) {
-            logger.log(e, "Error setting scan mode to %d", scanMode);
+            mLogger.log(e, "Error setting scan mode to %d", scanMode);
         }
     }
 
@@ -2117,21 +2130,21 @@
 
     private ResultCode checkTdsControlPointRequest(byte[] request) {
         if (request.length < 2) {
-            logger.log(
+            mLogger.log(
                     new IllegalArgumentException(), "Expected length >= 2 for %s",
                     base16().encode(request));
             return ResultCode.INVALID_PARAMETER;
         }
         byte opCode = getTdsControlPointOpCode(request);
         if (opCode != ControlPointCharacteristic.ACTIVATE_TRANSPORT_OP_CODE) {
-            logger.log(
+            mLogger.log(
                     new IllegalArgumentException(),
                     "Expected Activate Transport op code (0x01), got %d",
                     opCode);
             return ResultCode.OP_CODE_NOT_SUPPORTED;
         }
         if (request[1] != BLUETOOTH_SIG_ORGANIZATION_ID) {
-            logger.log(
+            mLogger.log(
                     new IllegalArgumentException(),
                     "Expected Bluetooth SIG organization ID (0x01), got %d",
                     request[1]);
@@ -2145,15 +2158,15 @@
     }
 
     private boolean isDiscoverable() {
-        return bluetoothAdapter.getScanMode() == SCAN_MODE_CONNECTABLE_DISCOVERABLE;
+        return mBluetoothAdapter.getScanMode() == SCAN_MODE_CONNECTABLE_DISCOVERABLE;
     }
 
     private byte[] modelIdServiceData(boolean forAdvertising) {
         // Note: This used to be little-endian but is now big-endian. See b/78229467 for details.
         byte[] modelIdPacket =
                 base16().decode(
-                        forAdvertising ? options.getAdvertisingModelId() : options.getModelId());
-        if (!batteryValues.isEmpty()) {
+                        forAdvertising ? mOptions.getAdvertisingModelId() : mOptions.getModelId());
+        if (!mBatteryValues.isEmpty()) {
             // If we are going to advertise battery values with the packet, then switch to the
             // non-3-byte model ID format.
             modelIdPacket = concat(new byte[]{0b00000110}, modelIdPacket);
@@ -2187,22 +2200,22 @@
                 new BloomFilter(
                         new byte[(int) (1.2 * accountKeys.size()) + 3],
                         new FastPairBloomFilterHasher());
-        String address = bleAddress == null ? SIMULATOR_FAKE_BLE_ADDRESS : bleAddress;
+        String address = mBleAddress == null ? SIMULATOR_FAKE_BLE_ADDRESS : mBleAddress;
 
         // Simulator supports Central Address Resolution characteristic, so when paired, the BLE
         // address in Seeker will be resolved to BR/EDR address. This caused Seeker fails on
         // checking the bloom filter due to different address is used for salting. In order to
         // let battery values notification be shown on paired device, we use random salt to
         // workaround it.
-        boolean advertisingBatteryValues = !batteryValues.isEmpty();
+        boolean advertisingBatteryValues = !mBatteryValues.isEmpty();
         byte[] salt;
-        if (options.getUseRandomSaltForAccountKeyRotation() || advertisingBatteryValues) {
+        if (mOptions.getUseRandomSaltForAccountKeyRotation() || advertisingBatteryValues) {
             salt = new byte[1];
             new SecureRandom().nextBytes(salt);
-            logger.log("Using random salt %s for bloom filter", base16().encode(salt));
+            mLogger.log("Using random salt %s for bloom filter", base16().encode(salt));
         } else {
             salt = BluetoothAddress.decode(address);
-            logger.log("Using address %s for bloom filter", address);
+            mLogger.log("Using address %s for bloom filter", address);
         }
 
         // To prevent tampering, account filter shall be slightly modified to include battery data
@@ -2219,7 +2232,7 @@
             bloomFilter.add(concat(accountKey.toByteArray(), saltAndBatteryData));
         }
         byte[] packet = generateAccountKeyData(bloomFilter);
-        return options.getUseRandomSaltForAccountKeyRotation() || advertisingBatteryValues
+        return mOptions.getUseRandomSaltForAccountKeyRotation() || advertisingBatteryValues
                 // Create a header with length 1 and type 1 for a random salt.
                 ? concat(packet, createField((byte) 0x11, salt))
                 // Exclude the salt from the packet, BLE address will be assumed by the client.
@@ -2237,7 +2250,7 @@
     }
 
     public int getTxPower() {
-        return options.getTxPowerLevel();
+        return mOptions.getTxPowerLevel();
     }
 
     @Nullable
@@ -2251,7 +2264,7 @@
 
     @Nullable
     private byte[] addBatteryValues(byte[] packet) {
-        if (batteryValues.isEmpty() || packet == null) {
+        if (mBatteryValues.isEmpty() || packet == null) {
             return packet;
         }
 
@@ -2263,16 +2276,16 @@
         // 4 are the type.
         // Byte 1 - length: Battery values, the first bit is charging status, the remaining bits are
         // the actual value between 0 and 100, or -1 for unknown.
-        byte[] batteryData = new byte[batteryValues.size() + 1];
-        batteryData[0] = (byte) (batteryValues.size() << 4
-                | (suppressBatteryNotification ? 0b0100 : 0b0011));
+        byte[] batteryData = new byte[mBatteryValues.size() + 1];
+        batteryData[0] = (byte) (mBatteryValues.size() << 4
+                | (mSuppressBatteryNotification ? 0b0100 : 0b0011));
 
         int batteryValueIndex = 1;
-        for (BatteryValue batteryValue : batteryValues) {
+        for (BatteryValue batteryValue : mBatteryValues) {
             batteryData[batteryValueIndex++] =
                     (byte)
-                            ((batteryValue.charging ? 0b10000000 : 0b00000000)
-                                    | (0b01111111 & batteryValue.level));
+                            ((batteryValue.mCharging ? 0b10000000 : 0b00000000)
+                                    | (0b01111111 & batteryValue.mLevel));
         }
 
         return batteryData;
@@ -2284,16 +2297,16 @@
         // The following bytes are the data of bloom filter.
         byte[] filterBytes = bloomFilter.asBytes();
         byte lengthAndType = (byte) (filterBytes.length << 4
-                | (suppressSubsequentPairingNotification ? 0b0010 : 0b0000));
-        logger.log(
+                | (mSuppressSubsequentPairingNotification ? 0b0010 : 0b0000));
+        mLogger.log(
                 "Generate bloom filter with suppress subsequent pairing notification:%b",
-                suppressSubsequentPairingNotification);
+                mSuppressSubsequentPairingNotification);
         return createField(lengthAndType, filterBytes);
     }
 
-    /** Disconnects all connected devices. */
-    private void disconnect() {
-        for (BluetoothDevice device : bluetoothAdapter.getBondedDevices()) {
+    /** Disconnects all bonded devices. */
+    public void disconnectAllBondedDevices() {
+        for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) {
             if (device.getBluetoothClass().getMajorDeviceClass() == Major.PHONE) {
                 removeBond(device);
             }
@@ -2304,7 +2317,7 @@
         try {
             Reflect.on(profile).withMethod("disconnect", BluetoothDevice.class).invoke(device);
         } catch (ReflectionException e) {
-            logger.log(e, "Error disconnecting device=%s from profile=%s", device, profile);
+            mLogger.log(e, "Error disconnecting device=%s from profile=%s", device, profile);
         }
     }
 
@@ -2312,16 +2325,16 @@
         try {
             Reflect.on(device).withMethod("removeBond").invoke();
         } catch (ReflectionException e) {
-            logger.log(e, "Error removing bond for device=%s", device);
+            mLogger.log(e, "Error removing bond for device=%s", device);
         }
     }
 
     public void resetAccountKeys() {
-        fastPairSimulatorDatabase.setAccountKeys(new HashSet<>());
-        fastPairSimulatorDatabase.setFastPairSeekerDevices(new HashSet<>());
-        accountKey = null;
-        ownerAccountKey = null;
-        logger.log("Remove all account keys");
+        mFastPairSimulatorDatabase.setAccountKeys(new HashSet<>());
+        mFastPairSimulatorDatabase.setFastPairSeekerDevices(new HashSet<>());
+        mAccountKey = null;
+        mOwnerAccountKey = null;
+        mLogger.log("Remove all account keys");
     }
 
     public void addAccountKey(byte[] key) {
@@ -2329,43 +2342,43 @@
     }
 
     private void addAccountKey(byte[] key, @Nullable BluetoothDevice device) {
-        accountKey = key;
-        if (ownerAccountKey == null) {
-            ownerAccountKey = key;
+        mAccountKey = key;
+        if (mOwnerAccountKey == null) {
+            mOwnerAccountKey = key;
         }
 
-        fastPairSimulatorDatabase.addAccountKey(key);
-        fastPairSimulatorDatabase.addFastPairSeekerDevice(device, key);
-        logger.log("Add account key: key=%s, device=%s", base16().encode(key), device);
+        mFastPairSimulatorDatabase.addAccountKey(key);
+        mFastPairSimulatorDatabase.addFastPairSeekerDevice(device, key);
+        mLogger.log("Add account key: key=%s, device=%s", base16().encode(key), device);
     }
 
     private Set<ByteString> getAccountKeys() {
-        return fastPairSimulatorDatabase.getAccountKeys();
+        return mFastPairSimulatorDatabase.getAccountKeys();
     }
 
     /** Get the latest account key. */
     @Nullable
     public ByteString getAccountKey() {
-        if (accountKey == null) {
+        if (mAccountKey == null) {
             return null;
         }
-        return ByteString.copyFrom(accountKey);
+        return ByteString.copyFrom(mAccountKey);
     }
 
     /** Get the owner account key (the first account key registered). */
     @Nullable
     public ByteString getOwnerAccountKey() {
-        if (ownerAccountKey == null) {
+        if (mOwnerAccountKey == null) {
             return null;
         }
-        return ByteString.copyFrom(ownerAccountKey);
+        return ByteString.copyFrom(mOwnerAccountKey);
     }
 
     public void resetDeviceName() {
-        fastPairSimulatorDatabase.setLocalDeviceName(null);
+        mFastPairSimulatorDatabase.setLocalDeviceName(null);
         // Trigger simulator to update device name text view.
-        if (deviceNameCallback != null) {
-            deviceNameCallback.onNameChanged(getDeviceName());
+        if (mDeviceNameCallback != null) {
+            mDeviceNameCallback.onNameChanged(getDeviceName());
         }
     }
 
@@ -2375,18 +2388,18 @@
     }
 
     private void setDeviceName(@Nullable byte[] deviceName) {
-        fastPairSimulatorDatabase.setLocalDeviceName(deviceName);
+        mFastPairSimulatorDatabase.setLocalDeviceName(deviceName);
 
-        logger.log("Save device name : %s", getDeviceName());
+        mLogger.log("Save device name : %s", getDeviceName());
         // Trigger simulator to update device name text view.
-        if (deviceNameCallback != null) {
-            deviceNameCallback.onNameChanged(getDeviceName());
+        if (mDeviceNameCallback != null) {
+            mDeviceNameCallback.onNameChanged(getDeviceName());
         }
     }
 
     @Nullable
     private byte[] getDeviceNameInBytes() {
-        return fastPairSimulatorDatabase.getLocalDeviceName();
+        return mFastPairSimulatorDatabase.getLocalDeviceName();
     }
 
     @Nullable
@@ -2395,7 +2408,7 @@
                 getDeviceNameInBytes() != null
                         ? new String(getDeviceNameInBytes(), StandardCharsets.UTF_8)
                         : null;
-        logger.log("get device name = %s", providerDeviceName);
+        mLogger.log("get device name = %s", providerDeviceName);
         return providerDeviceName;
     }
 
@@ -2410,19 +2423,19 @@
      * </ul>
      */
     private static byte tdsFlags(TransportState transportState) {
-        return (byte) (0b00000010 & (transportState.byteValue << 3));
+        return (byte) (0b00000010 & (transportState.mByteValue << 3));
     }
 
     /** Detailed information about battery value. */
     public static class BatteryValue {
-        boolean charging;
+        boolean mCharging;
 
         // The range is 0 ~ 100, and -1 represents the battery level is unknown.
-        int level;
+        int mLevel;
 
         public BatteryValue(boolean charging, int level) {
-            this.charging = charging;
-            this.level = level;
+            this.mCharging = charging;
+            this.mLevel = level;
         }
     }
 }
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/FastPairSimulatorDatabase.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulatorDatabase.java
similarity index 84%
rename from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/FastPairSimulatorDatabase.java
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulatorDatabase.java
index 4d2a9e8..254ec51 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/FastPairSimulatorDatabase.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulatorDatabase.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.common.bluetooth.fastpair.testing;
+package android.nearby.fastpair.provider;
 
 import static com.google.common.io.BaseEncoding.base16;
 
@@ -45,15 +45,15 @@
     // [for SASS]
     private static final String KEY_FAST_PAIR_SEEKER_DEVICE = "FAST_PAIR_SEEKER_DEVICE";
 
-    private final SharedPreferences sharedPreferences;
+    private final SharedPreferences mSharedPreferences;
 
     public FastPairSimulatorDatabase(Context context) {
-        sharedPreferences = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
+        mSharedPreferences = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
     }
 
     /** Adds single account key. */
     public void addAccountKey(byte[] accountKey) {
-        if (sharedPreferences == null) {
+        if (mSharedPreferences == null) {
             return;
         }
 
@@ -78,7 +78,7 @@
 
     /** Sets account keys, overrides all. */
     public void setAccountKeys(Set<ByteString> accountKeys) {
-        if (sharedPreferences == null) {
+        if (mSharedPreferences == null) {
             return;
         }
 
@@ -87,16 +87,16 @@
             keys.add(base16().encode(item.toByteArray()));
         }
 
-        sharedPreferences.edit().putStringSet(KEY_ACCOUNT_KEYS, keys).apply();
+        mSharedPreferences.edit().putStringSet(KEY_ACCOUNT_KEYS, keys).apply();
     }
 
     /** Gets all account keys. */
     public Set<ByteString> getAccountKeys() {
-        if (sharedPreferences == null) {
+        if (mSharedPreferences == null) {
             return new HashSet<>();
         }
 
-        Set<String> keys = sharedPreferences.getStringSet(KEY_ACCOUNT_KEYS, new HashSet<>());
+        Set<String> keys = mSharedPreferences.getStringSet(KEY_ACCOUNT_KEYS, new HashSet<>());
         Set<ByteString> accountKeys = new HashSet<>();
         // Add new account keys one by one.
         for (String key : keys) {
@@ -108,26 +108,26 @@
 
     /** Sets local device name. */
     public void setLocalDeviceName(byte[] deviceName) {
-        if (sharedPreferences == null) {
+        if (mSharedPreferences == null) {
             return;
         }
 
         String humanReadableName = deviceName != null ? new String(deviceName, UTF_8) : null;
         if (humanReadableName == null) {
-            sharedPreferences.edit().remove(KEY_DEVICE_NAME).apply();
+            mSharedPreferences.edit().remove(KEY_DEVICE_NAME).apply();
         } else {
-            sharedPreferences.edit().putString(KEY_DEVICE_NAME, humanReadableName).apply();
+            mSharedPreferences.edit().putString(KEY_DEVICE_NAME, humanReadableName).apply();
         }
     }
 
     /** Gets local device name. */
     @Nullable
     public byte[] getLocalDeviceName() {
-        if (sharedPreferences == null) {
+        if (mSharedPreferences == null) {
             return null;
         }
 
-        String deviceName = sharedPreferences.getString(KEY_DEVICE_NAME, null);
+        String deviceName = mSharedPreferences.getString(KEY_DEVICE_NAME, null);
         return deviceName != null ? deviceName.getBytes(UTF_8) : null;
     }
 
@@ -136,7 +136,7 @@
      * href="http://go/smart-audio-source-switching-design">Sass design doc</a>
      */
     public void addFastPairSeekerDevice(@Nullable BluetoothDevice device, byte[] accountKey) {
-        if (sharedPreferences == null) {
+        if (mSharedPreferences == null) {
             return;
         }
 
@@ -164,7 +164,7 @@
 
     /** [for SASS] Sets all seeker device info, overrides all. */
     public void setFastPairSeekerDevices(Set<FastPairSeekerDevice> fastPairSeekerDeviceSet) {
-        if (sharedPreferences == null) {
+        if (mSharedPreferences == null) {
             return;
         }
 
@@ -173,18 +173,18 @@
             rawStringSet.add(item.toRawString());
         }
 
-        sharedPreferences.edit().putStringSet(KEY_FAST_PAIR_SEEKER_DEVICE, rawStringSet).apply();
+        mSharedPreferences.edit().putStringSet(KEY_FAST_PAIR_SEEKER_DEVICE, rawStringSet).apply();
     }
 
     /** [for SASS] Gets all seeker device info. */
     public Set<FastPairSeekerDevice> getFastPairSeekerDevices() {
-        if (sharedPreferences == null) {
+        if (mSharedPreferences == null) {
             return new HashSet<>();
         }
 
         Set<FastPairSeekerDevice> fastPairSeekerDevices = new HashSet<>();
         Set<String> rawStringSet =
-                sharedPreferences.getStringSet(KEY_FAST_PAIR_SEEKER_DEVICE, new HashSet<>());
+                mSharedPreferences.getStringSet(KEY_FAST_PAIR_SEEKER_DEVICE, new HashSet<>());
         for (String rawString : rawStringSet) {
             FastPairSeekerDevice fastPairDevice = FastPairSeekerDevice.fromRawString(rawString);
             if (fastPairDevice == null) {
@@ -201,24 +201,24 @@
         private static final int INDEX_DEVICE = 0;
         private static final int INDEX_ACCOUNT_KEY = 1;
 
-        private final BluetoothDevice device;
-        private final byte[] accountKey;
+        private final BluetoothDevice mDevice;
+        private final byte[] mAccountKey;
 
         private FastPairSeekerDevice(BluetoothDevice device, byte[] accountKey) {
-            this.device = device;
-            this.accountKey = accountKey;
+            this.mDevice = device;
+            this.mAccountKey = accountKey;
         }
 
         public BluetoothDevice getBluetoothDevice() {
-            return device;
+            return mDevice;
         }
 
         public byte[] getAccountKey() {
-            return accountKey;
+            return mAccountKey;
         }
 
         public String toRawString() {
-            return String.format("%s,%s", device, base16().encode(accountKey));
+            return String.format("%s,%s", mDevice, base16().encode(mAccountKey));
         }
 
         /** Decodes the raw string if possible. */
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/HandshakeRequest.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/HandshakeRequest.java
similarity index 84%
rename from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/HandshakeRequest.java
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/HandshakeRequest.java
index 5453a87..9cfffd8 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/HandshakeRequest.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/HandshakeRequest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.common.bluetooth.fastpair.testing;
+package android.nearby.fastpair.provider;
 
 import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.decrypt;
 import static com.android.server.nearby.common.bluetooth.fastpair.Constants.BLUETOOTH_ADDRESS_LENGTH;
@@ -45,7 +45,7 @@
      *
      * @see {go/fast-pair-spec-handshake-message1}
      */
-    private final byte[] decryptedMessage;
+    private final byte[] mDecryptedMessage;
 
     /** Enumerates the handshake message types. */
     public enum Type {
@@ -53,14 +53,14 @@
         ACTION_OVER_BLE(Request.TYPE_ACTION_OVER_BLE),
         UNKNOWN((byte) 0xFF);
 
-        private final byte value;
+        private final byte mValue;
 
         Type(byte type) {
-            value = type;
+            mValue = type;
         }
 
         public byte getValue() {
-            return value;
+            return mValue;
         }
 
         public static Type valueOf(byte value) {
@@ -75,21 +75,24 @@
 
     public HandshakeRequest(byte[] key, byte[] encryptedPairingRequest)
             throws GeneralSecurityException {
-        decryptedMessage = decrypt(key, encryptedPairingRequest);
+        mDecryptedMessage = decrypt(key, encryptedPairingRequest);
     }
 
     /**
-     * Gets the type of this handshake request. Currently, we have 2 types: 0x00 for Key-based Pairing
-     * Request and 0x10 for Action Request.
+     * Gets the type of this handshake request. Currently, we have 2 types: 0x00 for Key-based
+     * Pairing Request and 0x10 for Action Request.
      */
     public Type getType() {
-        return Type.valueOf(decryptedMessage[Request.TYPE_INDEX]);
+        return Type.valueOf(mDecryptedMessage[Request.TYPE_INDEX]);
     }
 
-    /** Gets verification data of this handshake request, currently, we use Provider's BLE address. */
+    /**
+     * Gets verification data of this handshake request.
+     * Currently, we use Provider's BLE address.
+     */
     public byte[] getVerificationData() {
         return Arrays.copyOfRange(
-                decryptedMessage,
+                mDecryptedMessage,
                 Request.VERIFICATION_DATA_INDEX,
                 Request.VERIFICATION_DATA_INDEX + Request.VERIFICATION_DATA_LENGTH);
     }
@@ -97,7 +100,7 @@
     /** Gets Seeker's public address of the handshake request. */
     public byte[] getSeekerPublicAddress() {
         return Arrays.copyOfRange(
-                decryptedMessage,
+                mDecryptedMessage,
                 Request.SEEKER_PUBLIC_ADDRESS_INDEX,
                 Request.SEEKER_PUBLIC_ADDRESS_INDEX + BLUETOOTH_ADDRESS_LENGTH);
     }
@@ -126,7 +129,7 @@
 
     /** Gets the flags of this handshake request. */
     private byte getFlags() {
-        return decryptedMessage[Request.FLAGS_INDEX];
+        return mDecryptedMessage[Request.FLAGS_INDEX];
     }
 
     /** Checks whether the Seeker requests a device action. */
@@ -134,17 +137,21 @@
         return (getFlags() & DEVICE_ACTION) != 0;
     }
 
-    /** Checks whether the Seeker requests an action which will be followed by an additional data. */
+    /**
+     * Checks whether the Seeker requests an action which will be followed by an additional data
+     * .
+     */
     public boolean requestFollowedByAdditionalData() {
         return (getFlags() & ADDITIONAL_DATA_CHARACTERISTIC) != 0;
     }
 
     /** Gets the {@link AdditionalDataType} of this handshake request. */
-    public @AdditionalDataType int getAdditionalDataType() {
+    @AdditionalDataType
+    public int getAdditionalDataType() {
         if (!requestFollowedByAdditionalData()
-                || decryptedMessage.length <= ADDITIONAL_DATA_TYPE_INDEX) {
+                || mDecryptedMessage.length <= ADDITIONAL_DATA_TYPE_INDEX) {
             return AdditionalDataType.UNKNOWN;
         }
-        return decryptedMessage[ADDITIONAL_DATA_TYPE_INDEX];
+        return mDecryptedMessage[ADDITIONAL_DATA_TYPE_INDEX];
     }
 }
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/OreoFastPairAdvertiser.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/OreoFastPairAdvertiser.java
similarity index 73%
rename from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/OreoFastPairAdvertiser.java
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/OreoFastPairAdvertiser.java
index 6913356..dd664ea 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/OreoFastPairAdvertiser.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/OreoFastPairAdvertiser.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.common.bluetooth.fastpair.testing;
+package android.nearby.fastpair.provider;
 
 import static com.google.common.io.BaseEncoding.base16;
 
@@ -26,6 +26,7 @@
 import android.bluetooth.le.AdvertisingSetCallback;
 import android.bluetooth.le.AdvertisingSetParameters;
 import android.bluetooth.le.BluetoothLeAdvertiser;
+import android.nearby.fastpair.provider.utils.Logger;
 import android.os.Build.VERSION_CODES;
 import android.os.ParcelUuid;
 
@@ -43,41 +44,43 @@
 @TargetApi(VERSION_CODES.O)
 public final class OreoFastPairAdvertiser implements FastPairAdvertiser {
     private static final String TAG = "OreoFastPairAdvertiser";
-    private final Logger logger = new Logger(TAG);
+    private final Logger mLogger = new Logger(TAG);
 
-    private final FastPairSimulator simulator;
-    private final BluetoothLeAdvertiser advertiser;
-    private final AdvertisingSetCallback advertisingSetCallback;
-    private AdvertisingSet advertisingSet;
+    private final FastPairSimulator mSimulator;
+    private final BluetoothLeAdvertiser mAdvertiser;
+    private final AdvertisingSetCallback mAdvertisingSetCallback;
+    private AdvertisingSet mAdvertisingSet;
 
     public OreoFastPairAdvertiser(FastPairSimulator simulator) {
-        this.simulator = simulator;
-        this.advertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
-        this.advertisingSetCallback =
+        this.mSimulator = simulator;
+        this.mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
+        this.mAdvertisingSetCallback =
                 new AdvertisingSetCallback() {
                     @Override
-                    public void onAdvertisingSetStarted(AdvertisingSet set, int txPower, int status) {
+                    public void onAdvertisingSetStarted(
+                            AdvertisingSet set, int txPower, int status) {
                         if (status == AdvertisingSetCallback.ADVERTISE_SUCCESS) {
-                            logger.log("Advertising succeeded, advertising at %s dBm", txPower);
+                            mLogger.log("Advertising succeeded, advertising at %s dBm", txPower);
                             simulator.setIsAdvertising(true);
-                            advertisingSet = set;
+                            mAdvertisingSet = set;
 
                             try {
                                 // Requires custom Android build, see callback below.
                                 Reflect.on(set).withMethod("getOwnAddress").invoke();
                             } catch (ReflectionException e) {
-                                logger.log(e, "Error calling getOwnAddress for AdvertisingSet");
+                                mLogger.log(e, "Error calling getOwnAddress for AdvertisingSet");
                             }
                         } else {
-                            logger.log(
-                                    new IllegalStateException(), "Advertising failed, error code=%d", status);
+                            mLogger.log(
+                                    new IllegalStateException(),
+                                    "Advertising failed, error code=%d", status);
                         }
                     }
 
                     @Override
                     public void onAdvertisingDataSet(AdvertisingSet set, int status) {
                         if (status != AdvertisingSetCallback.ADVERTISE_SUCCESS) {
-                            logger.log(
+                            mLogger.log(
                                     new IllegalStateException(),
                                     "Updating advertisement failed, error code=%d",
                                     status);
@@ -86,9 +89,10 @@
                     }
 
                     // Called via reflection with AdvertisingSet.getOwnAddress().
-                    public void onOwnAddressRead(AdvertisingSet set, int addressType, String address) {
+                    public void onOwnAddressRead(
+                            AdvertisingSet set, int addressType, String address) {
                         if (!address.equals(simulator.getBleAddress())) {
-                            logger.log(
+                            mLogger.log(
                                     "Read own BLE address=%s at %s",
                                     address,
                                     new SimpleDateFormat("HH:mm:ss:SSS", Locale.US)
@@ -102,21 +106,21 @@
     @Override
     public void startAdvertising(@Nullable byte[] serviceData) {
         // To be informed that BLE address is rotated, we need to polling query it asynchronously.
-        if (advertisingSet != null) {
+        if (mAdvertisingSet != null) {
             try {
                 // Requires custom Android build, see callback: onOwnAddressRead.
-                Reflect.on(advertisingSet).withMethod("getOwnAddress").invoke();
+                Reflect.on(mAdvertisingSet).withMethod("getOwnAddress").invoke();
             } catch (ReflectionException ignored) {
                 // Ignore it due to user already knows it when setting advertisingSet.
             }
         }
 
-        if (simulator.isDestroyed()) {
+        if (mSimulator.isDestroyed()) {
             return;
         }
 
         if (serviceData == null) {
-            logger.log("Service data is null, stop advertising");
+            mLogger.log("Service data is null, stop advertising");
             stopAdvertising();
             return;
         }
@@ -127,10 +131,10 @@
                         .setIncludeTxPowerLevel(true)
                         .build();
 
-        logger.log("Advertising FE2C service data=%s", base16().encode(serviceData));
+        mLogger.log("Advertising FE2C service data=%s", base16().encode(serviceData));
 
-        if (advertisingSet != null) {
-            advertisingSet.setAdvertisingData(data);
+        if (mAdvertisingSet != null) {
+            mAdvertisingSet.setAdvertisingData(data);
             return;
         }
 
@@ -141,9 +145,10 @@
                         .setConnectable(true)
                         .setScannable(true)
                         .setInterval(AdvertisingSetParameters.INTERVAL_LOW)
-                        .setTxPowerLevel(convertAdvertiseSettingsTxPower(simulator.getTxPower()))
+                        .setTxPowerLevel(convertAdvertiseSettingsTxPower(mSimulator.getTxPower()))
                         .build();
-        advertiser.startAdvertisingSet(parameters, data, null, null, null, advertisingSetCallback);
+        mAdvertiser.startAdvertisingSet(parameters, data, null, null, null,
+                mAdvertisingSetCallback);
     }
 
     private static int convertAdvertiseSettingsTxPower(int txPower) {
@@ -161,12 +166,12 @@
 
     @Override
     public void stopAdvertising() {
-        if (simulator.isDestroyed()) {
+        if (mSimulator.isDestroyed()) {
             return;
         }
 
-        advertiser.stopAdvertisingSet(advertisingSetCallback);
-        advertisingSet = null;
-        simulator.setIsAdvertising(false);
+        mAdvertiser.stopAdvertisingSet(mAdvertisingSetCallback);
+        mAdvertisingSet = null;
+        mSimulator.setIsAdvertising(false);
     }
 }
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/gatt/server/BluetoothGattServerConfig.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConfig.java
similarity index 77%
rename from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/gatt/server/BluetoothGattServerConfig.java
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConfig.java
index 9fa54fe..3cacd55 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/gatt/server/BluetoothGattServerConfig.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConfig.java
@@ -1,5 +1,20 @@
+/*
+ * 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.common.bluetooth.gatt.server;
+package android.nearby.fastpair.provider.bluetooth;
 
 import android.annotation.TargetApi;
 import android.bluetooth.BluetoothGattCharacteristic;
@@ -46,15 +61,16 @@
      * TODO(lingjunl): remove them when b/21587710 is fixed.
      */
     public BluetoothGattServerConfig addSelfDefinedDynamicService() {
-        ServiceConfig serviceConfig = new ServiceConfig().addCharacteristic(new BluetoothGattServlet() {
-            @Override
-            public BluetoothGattCharacteristic getCharacteristic() {
-                return new BluetoothGattCharacteristic(
-                        BluetoothConsts.SERVICE_DYNAMIC_CHARACTERISTIC,
-                        BluetoothGattCharacteristic.PROPERTY_READ,
-                        BluetoothGattCharacteristic.PERMISSION_READ);
-            }
-        });
+        ServiceConfig serviceConfig = new ServiceConfig().addCharacteristic(
+                new BluetoothGattServlet() {
+                    @Override
+                    public BluetoothGattCharacteristic getCharacteristic() {
+                        return new BluetoothGattCharacteristic(
+                                BluetoothConsts.SERVICE_DYNAMIC_CHARACTERISTIC,
+                                BluetoothGattCharacteristic.PROPERTY_READ,
+                                BluetoothGattCharacteristic.PERMISSION_READ);
+                    }
+                });
         return addService(BluetoothConsts.SERVICE_DYNAMIC_SERVICE, serviceConfig);
     }
 
@@ -67,8 +83,8 @@
                 // This is not supposed to happen
                 throw new IllegalStateException();
             }
-            BluetoothGattService gattService =
-                    new BluetoothGattService(serviceUuid, BluetoothGattService.SERVICE_TYPE_PRIMARY);
+            BluetoothGattService gattService = new BluetoothGattService(serviceUuid,
+                    BluetoothGattService.SERVICE_TYPE_PRIMARY);
             for (Entry<BluetoothGattCharacteristic, BluetoothGattServlet> servletEntry :
                     serviceConfig.getServlets().entrySet()) {
                 BluetoothGattCharacteristic characteristic = servletEntry.getKey();
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/gatt/server/BluetoothGattServerConnection.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConnection.java
similarity index 86%
rename from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/gatt/server/BluetoothGattServerConnection.java
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConnection.java
index b67e00e..fae6951 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/gatt/server/BluetoothGattServerConnection.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConnection.java
@@ -1,4 +1,20 @@
-package com.android.server.nearby.common.bluetooth.gatt.server;
+/*
+ * 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.fastpair.provider.bluetooth;
 
 import android.annotation.TargetApi;
 import android.bluetooth.BluetoothGatt;
@@ -12,7 +28,6 @@
 import com.android.server.nearby.common.bluetooth.BluetoothGattException;
 import com.android.server.nearby.common.bluetooth.ReservedUuids;
 import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
-import com.android.server.nearby.common.bluetooth.util.BluetoothGattUtils;
 import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
 import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
 
@@ -53,7 +68,8 @@
     /** Default MTU when value is unknown. */
     public static final int DEFAULT_MTU = 23;
 
-    @VisibleForTesting static final long OPERATION_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
+    @VisibleForTesting
+    static final long OPERATION_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
 
     /** Notification types as defined by the BLE spec vol 4, sec G, part 3.3.3.3 */
     public enum NotificationType {
@@ -72,7 +88,8 @@
     private final BluetoothGattServerHelper mBluetoothGattServerHelper;
     private final BluetoothDevice mBluetoothDevice;
 
-    @VisibleForTesting BluetoothOperationExecutor mBluetoothOperationScheduler =
+    @VisibleForTesting
+    BluetoothOperationExecutor mBluetoothOperationScheduler =
             new BluetoothOperationExecutor(1);
 
     /** Stores pending writes. For each UUID, we store an offset and a byte[] of data. */
@@ -129,12 +146,13 @@
         }
     }
 
-    private final BluetoothGattServlet getServlet(BluetoothGattCharacteristic characteristic)
+    private BluetoothGattServlet getServlet(BluetoothGattCharacteristic characteristic)
             throws BluetoothGattException {
         BluetoothGattServlet servlet = mServlets.get(characteristic);
         if (servlet == null) {
             throw new BluetoothGattException(
-                    String.format("No handler registered for characteristic %s.", characteristic.getUuid()),
+                    String.format("No handler registered for characteristic %s.",
+                            characteristic.getUuid()),
                     BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
         }
         return servlet;
@@ -145,7 +163,8 @@
         return getServlet(characteristic).read(this, offset);
     }
 
-    public void writeCharacteristic(BluetoothGattCharacteristic characteristic, boolean preparedWrite,
+    public void writeCharacteristic(BluetoothGattCharacteristic characteristic,
+            boolean preparedWrite,
             int offset, byte[] value) throws BluetoothGattException {
         Log.d(TAG, String.format(
                 "Received %d bytes at offset %d on %s from device %s, prepareWrite=%s.",
@@ -214,18 +233,21 @@
     }
 
     private void handleCharacteristicConfigurationChange(
-            final BluetoothGattCharacteristic characteristic, BluetoothGattServlet servlet, int offset,
+            final BluetoothGattCharacteristic characteristic, BluetoothGattServlet servlet,
+            int offset,
             byte[] value)
             throws BluetoothGattException {
         if (offset != 0) {
             throw new BluetoothGattException(String.format(
-                    "Offset should be 0 when changing the client characteristic config: %d.", offset),
+                    "Offset should be 0 when changing the client characteristic config: %d.",
+                    offset),
                     BluetoothGatt.GATT_INVALID_OFFSET);
         }
         if (value.length != 2) {
             throw new BluetoothGattException(String.format(
                     "Value 0x%s is undefined for the client characteristic config",
-                    BaseEncoding.base16().encode(value)), BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH);
+                    BaseEncoding.base16().encode(value)),
+                    BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH);
         }
 
         boolean notificationRegistered = mRegisteredNotifications.containsKey(characteristic);
@@ -268,7 +290,8 @@
             default:
                 throw new BluetoothGattException(String.format(
                         "Value 0x%s is undefined for the client characteristic config",
-                        BaseEncoding.base16().encode(value)), BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
+                        BaseEncoding.base16().encode(value)),
+                        BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
         }
     }
 
@@ -305,27 +328,27 @@
      * Assembles the specified queued writes and calls the provided write handler on the assembled
      * chunks. Tries to assemble all the chunks into one write request. For example, if the content
      * of byteChunks is:
-     *  <code>
-     *     offset data_size
-     *       0       10
-     *      10        1
-     *      11        5
-     *  </code>
+     * <code>
+     * offset data_size
+     * 0       10
+     * 10        1
+     * 11        5
+     * </code>
      *
-     *  then this method would call <code>writeHandler.onWrite(0, byte[16])</code>
+     * then this method would call <code>writeHandler.onWrite(0, byte[16])</code>
      *
      * However, if all the chunks cannot be assembled into a continuous byte[], then onWrite() will
      * be called multiple times with the largest continuous chunks. For example, if the content of
      * byteChunks is:
      * <code>
-     *     offset data_size
-     *      10       12
-     *      30        5
-     *      35        9
+     * offset data_size
+     * 10       12
+     * 30        5
+     * 35        9
      * </code>
      *
-     *  then this method would call <code>writeHandler.onWrite(10, byte[12)</code> and
-     *  <code>writeHandler.onWrite(30, byte[14]).
+     * then this method would call <code>writeHandler.onWrite(10, byte[12)</code> and
+     * <code>writeHandler.onWrite(30, byte[14]).
      */
     private void assembleByteChunksAndHandle(BluetoothGattServlet servlet,
             SortedMap<Integer, byte[]> byteChunks) throws BluetoothGattException {
@@ -336,8 +359,9 @@
             Integer offset = byteChunks.firstKey();
 
             if (offset.intValue() < startWritingAtOffset + assembledRequest.size()) {
-                throw new BluetoothGattException("Expected offset of at least " + assembledRequest.size()
-                        + ", but got offset " + offset, BluetoothGatt.GATT_INVALID_OFFSET);
+                throw new BluetoothGattException(
+                        "Expected offset of at least " + assembledRequest.size()
+                                + ", but got offset " + offset, BluetoothGatt.GATT_INVALID_OFFSET);
             }
 
             // If we have a hole, then write what we've already assembled and start assembling a new
@@ -357,7 +381,8 @@
                 }
                 assembledRequest.write(dataChunk);
             } catch (IOException e) {
-                throw new BluetoothGattException("Error assembling request", BluetoothGatt.GATT_FAILURE);
+                throw new BluetoothGattException("Error assembling request",
+                        BluetoothGatt.GATT_FAILURE);
             }
         }
 
@@ -375,8 +400,10 @@
                 new Operation<Void>(OperationType.SEND_NOTIFICATION) {
                     @Override
                     public void run() throws BluetoothException {
-                        mBluetoothGattServerHelper.sendNotification(mBluetoothDevice, characteristic,
-                                data, notificationType == NotificationType.INDICATION ? true : false);
+                        mBluetoothGattServerHelper.sendNotification(mBluetoothDevice,
+                                characteristic,
+                                data,
+                                notificationType == NotificationType.INDICATION ? true : false);
                     }
                 },
                 OPERATION_TIMEOUT);
@@ -409,7 +436,7 @@
         private final Object mScope;
         private final String mKey;
 
-        public ScopedKey(Object scope, String key) {
+        ScopedKey(Object scope, String key) {
             mScope = scope;
             mKey = key;
         }
@@ -430,12 +457,12 @@
     }
 
     /** Listener to be notified when the connection closes. */
-    public static interface Listener {
+    public interface Listener {
         void onClose();
     }
 
     /** Notifier to notify data over notification or indication. */
-    public static interface Notifier {
+    public interface Notifier {
         void notify(byte[] data) throws BluetoothException;
     }
 }
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/gatt/server/BluetoothGattServerHelper.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerHelper.java
similarity index 77%
rename from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/gatt/server/BluetoothGattServerHelper.java
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerHelper.java
index 0ba96c0..9339e14 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/gatt/server/BluetoothGattServerHelper.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerHelper.java
@@ -1,4 +1,20 @@
-package com.android.server.nearby.common.bluetooth.gatt.server;
+/*
+ * 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.fastpair.provider.bluetooth;
 
 import android.annotation.TargetApi;
 import android.bluetooth.BluetoothGatt;
@@ -19,8 +35,6 @@
 import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
 import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattServer;
 import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattServerCallback;
-import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothManager;
-import com.android.server.nearby.common.bluetooth.util.BluetoothGattUtils;
 import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
 import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
 
@@ -37,7 +51,8 @@
 public class BluetoothGattServerHelper {
     private static final String TAG = BluetoothGattServerHelper.class.getSimpleName();
 
-    @VisibleForTesting static final long OPERATION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(1);
+    @VisibleForTesting
+    static final long OPERATION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(1);
     private static final int MAX_PARALLEL_OPERATIONS = 5;
 
     /** BT operation types that can be in flight. */
@@ -48,9 +63,11 @@
     }
 
     private final Object mOperationLock = new Object();
-    @VisibleForTesting final BluetoothGattServerCallback mGattServerCallback =
+    @VisibleForTesting
+    final BluetoothGattServerCallback mGattServerCallback =
             new GattServerCallback();
-    @VisibleForTesting BluetoothOperationExecutor mBluetoothOperationScheduler =
+    @VisibleForTesting
+    BluetoothOperationExecutor mBluetoothOperationScheduler =
             new BluetoothOperationExecutor(MAX_PARALLEL_OPERATIONS);
 
     private final Context mContext;
@@ -58,12 +75,15 @@
     private final VersionProvider mVersionProvider;
 
     @Nullable
-    @VisibleForTesting volatile BluetoothGattServerConfig mServerConfig = null;
+    @VisibleForTesting
+    volatile BluetoothGattServerConfig mServerConfig = null;
 
     @Nullable
-    @VisibleForTesting volatile BluetoothGattServer mBluetoothGattServer = null;
+    @VisibleForTesting
+    volatile BluetoothGattServer mBluetoothGattServer = null;
 
-    @VisibleForTesting final ConcurrentMap<BluetoothDevice, BluetoothGattServerConnection>
+    @VisibleForTesting
+    final ConcurrentMap<BluetoothDevice, BluetoothGattServerConnection>
             mConnections = new ConcurrentHashMap<BluetoothDevice, BluetoothGattServerConnection>();
 
     public BluetoothGattServerHelper(Context context, BluetoothManager bluetoothManager) {
@@ -74,7 +94,8 @@
         );
     }
 
-    @VisibleForTesting BluetoothGattServerHelper(
+    @VisibleForTesting
+    BluetoothGattServerHelper(
             Context context, BluetoothManager bluetoothManager, VersionProvider versionProvider) {
         mContext = context;
         mBluetoothManager = bluetoothManager;
@@ -97,7 +118,8 @@
             }
 
             try {
-                for (final BluetoothGattService service : gattServerConfig.getBluetoothGattServices()) {
+                for (final BluetoothGattService service :
+                        gattServerConfig.getBluetoothGattServices()) {
                     if (service == null) {
                         continue;
                     }
@@ -165,7 +187,8 @@
             if (bluetoothGattServer == null) {
                 throw new BluetoothException("Server is not open.");
             }
-            BluetoothGattCharacteristic clonedCharacteristic = BluetoothGattUtils.clone(characteristic);
+            BluetoothGattCharacteristic clonedCharacteristic =
+                    BluetoothGattUtils.clone(characteristic);
             clonedCharacteristic.setValue(data);
             bluetoothGattServer.notifyCharacteristicChanged(device, clonedCharacteristic, confirm);
         }
@@ -181,12 +204,13 @@
         if (connectionSate != BluetoothGatt.STATE_CONNECTED) {
             return;
         }
-        mBluetoothOperationScheduler.execute(new Operation<Void>(OperationType.CLOSE_CONNECTION) {
-                                                 @Override
-                                                 public void run() throws BluetoothException {
-                                                     bluetoothGattServer.cancelConnection(bluetoothDevice);
-                                                 }
-                                             },
+        mBluetoothOperationScheduler.execute(
+                new Operation<Void>(OperationType.CLOSE_CONNECTION) {
+                    @Override
+                    public void run() throws BluetoothException {
+                        bluetoothGattServer.cancelConnection(bluetoothDevice);
+                    }
+                },
                 OPERATION_TIMEOUT_MILLIS);
     }
 
@@ -214,8 +238,8 @@
                     }
                     Log.i(TAG, String.format("Connected to device %s.", device));
                     if (mConnections.containsKey(device)) {
-                        Log.w(TAG, String.format(
-                                "A connection is already open with device %s. Keeping existing one.", device));
+                        Log.w(TAG, String.format("A connection is already open with device %s. "
+                                + "Keeping existing one.", device));
                         return;
                     }
 
@@ -228,28 +252,35 @@
                     }
                     mConnections.put(device, connection);
 
-                    // By default, Android disconnects active GATT server connection if the advertisement is
-                    // stop (or sometime stopScanning also disconnect, see b/62667394). Asking the server to
+                    // By default, Android disconnects active GATT server connection if the
+                    // advertisement is
+                    // stop (or sometime stopScanning also disconnect, see b/62667394). Asking
+                    // the server to
                     // reverse connect will tell Android to keep the connection open.
                     // Code handling connect() on Android OS is: btif_gatt_server.c
                     // Note: for Android < P, unknown type devices don't connect. See b/62827460.
-                    //           TODO(mfucci): this can be fixed if the GATT server is forced to be LE only.
-                    //       for Android P+, unknown type devices always use LE to connect (see code)
-                    // Note: for Android < N, dual mode devices always connect using BT classic, so connect()
+                    //       for Android P+, unknown type devices always use LE to connect (see
+                    //       code)
+                    // Note: for Android < N, dual mode devices always connect using BT classic,
+                    // so connect()
                     //       should *NOT* be called for those devices. See b/29819614.
                     if (mVersionProvider.getSdkInt() >= VERSION_CODES.N
                             || device.getType() != BluetoothDevice.DEVICE_TYPE_DUAL) {
-                        boolean success = bluetoothGattServer.connect(device, /* autoConnect */false);
+                        boolean success = bluetoothGattServer.connect(device, /* autoConnect */
+                                false);
                         if (!success) {
                             Log.w(TAG, String.format(
-                                    "Keeping connection open on stop advertising failed for device %s.", device));
+                                    "Keeping connection open on stop advertising failed for "
+                                            + "device %s.",
+                                    device));
                         }
                     }
                     break;
                 case BluetoothGattServer.STATE_DISCONNECTED:
                     if (status != BluetoothGatt.GATT_SUCCESS) {
-                        Log.w(TAG, String.format("Disconnection from %s error: %s. Proceeding anyway.", device,
-                                BluetoothGattUtils.getMessageForStatusCode(status)));
+                        Log.w(TAG, String.format(
+                                "Disconnection from %s error: %s. Proceeding anyway.",
+                                device, BluetoothGattUtils.getMessageForStatusCode(status)));
                     }
                     bluetoothLeConnection = mConnections.remove(device);
                     if (bluetoothLeConnection != null) {
@@ -262,7 +293,6 @@
                     break;
                 default:
                     Log.e(TAG, String.format("Unexpected connection state: %d", newState));
-                    return;
             }
         }
 
@@ -274,8 +304,10 @@
                 return;
             }
             try {
-                byte[] value = getConnectionByDevice(device).readCharacteristic(offset, characteristic);
-                bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
+                byte[] value =
+                        getConnectionByDevice(device).readCharacteristic(offset, characteristic);
+                bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,
+                        offset,
                         value);
             } catch (BluetoothGattException e) {
                 Log.e(TAG,
@@ -285,7 +317,8 @@
                                 device,
                                 offset),
                         e);
-                bluetoothGattServer.sendResponse(device, requestId, e.getGattErrorCode(), offset, null);
+                bluetoothGattServer.sendResponse(
+                        device, requestId, e.getGattErrorCode(), offset, null);
             }
         }
 
@@ -307,8 +340,8 @@
                         offset,
                         value);
                 if (responseNeeded) {
-                    bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
-                            null);
+                    bluetoothGattServer.sendResponse(
+                            device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
                 }
             } catch (BluetoothGattException e) {
                 Log.e(TAG,
@@ -318,7 +351,8 @@
                                 device,
                                 offset),
                         e);
-                bluetoothGattServer.sendResponse(device, requestId, e.getGattErrorCode(), offset, null);
+                bluetoothGattServer.sendResponse(
+                        device, requestId, e.getGattErrorCode(), offset, null);
             }
         }
 
@@ -331,8 +365,8 @@
             }
             try {
                 byte[] value = getConnectionByDevice(device).readDescriptor(offset, descriptor);
-                bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
-                        value);
+                bluetoothGattServer.sendResponse(
+                        device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
             } catch (BluetoothGattException e) {
                 Log.e(TAG, String.format(
                                 "Could not read %s on device %s at %d",
@@ -340,7 +374,8 @@
                                 device,
                                 offset),
                         e);
-                bluetoothGattServer.sendResponse(device, requestId, e.getGattErrorCode(), offset, null);
+                bluetoothGattServer.sendResponse(
+                        device, requestId, e.getGattErrorCode(), offset, null);
             }
         }
 
@@ -357,10 +392,11 @@
                 return;
             }
             try {
-                getConnectionByDevice(device).writeDescriptor(descriptor, preparedWrite, offset, value);
+                getConnectionByDevice(device)
+                        .writeDescriptor(descriptor, preparedWrite, offset, value);
                 if (responseNeeded) {
-                    bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
-                            null);
+                    bluetoothGattServer.sendResponse(
+                            device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
                 }
                 Log.d(TAG, "Operation onDescriptorWriteRequest successful.");
             } catch (BluetoothGattException e) {
@@ -371,7 +407,8 @@
                                 device,
                                 offset),
                         e);
-                bluetoothGattServer.sendResponse(device, requestId, e.getGattErrorCode(), offset, null);
+                bluetoothGattServer.sendResponse(
+                        device, requestId, e.getGattErrorCode(), offset, null);
             }
         }
 
@@ -383,7 +420,8 @@
             }
             try {
                 getConnectionByDevice(device).executeWrite(execute);
-                bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null);
+                bluetoothGattServer.sendResponse(
+                        device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null);
             } catch (BluetoothGattException e) {
                 Log.e(TAG, "Could not execute write.", e);
                 bluetoothGattServer.sendResponse(device, requestId, e.getGattErrorCode(), 0, null);
@@ -392,12 +430,13 @@
 
         @Override
         public void onNotificationSent(BluetoothDevice device, int status) {
-            Log.d(TAG, String.format("Received onNotificationSent for device %s with status %s", device,
-                    status));
+            Log.d(TAG,
+                    String.format("Received onNotificationSent for device %s with status %s",
+                            device, status));
             try {
                 getConnectionByDevice(device).notifyNotificationSent(status);
             } catch (BluetoothGattException e) {
-                Log.e(TAG, String.format("An error occurred when receiving onNotificationSent"), e);
+                Log.e(TAG, "An error occurred when receiving onNotificationSent: " + e);
             }
         }
     }
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/gatt/server/BluetoothGattServlet.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServlet.java
similarity index 75%
rename from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/gatt/server/BluetoothGattServlet.java
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServlet.java
index c4a2473..e25e223 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/gatt/server/BluetoothGattServlet.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServlet.java
@@ -1,12 +1,28 @@
-package com.android.server.nearby.common.bluetooth.gatt.server;
+/*
+ * 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.fastpair.provider.bluetooth;
 
 import android.annotation.TargetApi;
 import android.bluetooth.BluetoothGatt;
 import android.bluetooth.BluetoothGattCharacteristic;
 import android.bluetooth.BluetoothGattDescriptor;
+import android.nearby.fastpair.provider.bluetooth.BluetoothGattServerConnection.Notifier;
 
 import com.android.server.nearby.common.bluetooth.BluetoothGattException;
-import com.android.server.nearby.common.bluetooth.gatt.server.BluetoothGattServerConnection.Notifier;
 
 /** Servlet to handle GATT operations on a characteristic. */
 @TargetApi(18)
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtils.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattUtils.java
similarity index 98%
rename from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtils.java
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattUtils.java
index e90b732..7ac26ee 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtils.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattUtils.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.common.bluetooth.util;
+package android.nearby.fastpair.provider.bluetooth;
 
 import android.bluetooth.BluetoothGatt;
 import android.bluetooth.BluetoothGattCharacteristic;
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothManager.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothManager.java
similarity index 83%
rename from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothManager.java
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothManager.java
index 4ccfb59..bf241f1 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothManager.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothManager.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,12 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.common.bluetooth.testability.android.bluetooth;
+package android.nearby.fastpair.provider.bluetooth;
 
 import android.content.Context;
 
 import androidx.annotation.Nullable;
 
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattServer;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattServerCallback;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -72,4 +77,4 @@
     public static BluetoothManager wrap(android.bluetooth.BluetoothManager bluetoothManager) {
         return new BluetoothManager(bluetoothManager);
     }
-}
\ No newline at end of file
+}
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/RfcommServer.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/RfcommServer.java
similarity index 76%
rename from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/RfcommServer.java
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/RfcommServer.java
index 7846226..9ed95ac 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/RfcommServer.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/RfcommServer.java
@@ -14,20 +14,21 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.common.bluetooth.fastpair.testing;
+package android.nearby.fastpair.provider.bluetooth;
 
-import static com.android.server.nearby.common.bluetooth.fastpair.testing.RfcommServer.State.ACCEPTING;
-import static com.android.server.nearby.common.bluetooth.fastpair.testing.RfcommServer.State.CONNECTED;
-import static com.android.server.nearby.common.bluetooth.fastpair.testing.RfcommServer.State.RESTARTING;
-import static com.android.server.nearby.common.bluetooth.fastpair.testing.RfcommServer.State.STARTING;
-import static com.android.server.nearby.common.bluetooth.fastpair.testing.RfcommServer.State.STOPPED;
+import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.ACCEPTING;
+import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.CONNECTED;
+import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.RESTARTING;
+import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.STARTING;
+import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.STOPPED;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothServerSocket;
 import android.bluetooth.BluetoothSocket;
-import android.nearby.multidevices.fastpair.EventStreamProtocol;
+import android.nearby.fastpair.provider.EventStreamProtocol;
+import android.nearby.fastpair.provider.utils.Logger;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
@@ -46,33 +47,33 @@
  */
 public class RfcommServer {
     private static final String TAG = "RfcommServer";
-    private final Logger logger = new Logger(TAG);
+    private final Logger mLogger = new Logger(TAG);
 
     private static final String FAST_PAIR_RFCOMM_SERVICE_NAME = "FastPairServer";
     public static final UUID FAST_PAIR_RFCOMM_UUID =
             UUID.fromString("df21fe2c-2515-4fdb-8886-f12c4d67927c");
 
     /** A single thread executor where all state checks are performed. */
-    private final ExecutorService controllerExecutor = Executors.newSingleThreadExecutor();
+    private final ExecutorService mControllerExecutor = Executors.newSingleThreadExecutor();
 
-    private final ExecutorService sendMessageExecutor = Executors.newSingleThreadExecutor();
-    private final ExecutorService receiveMessageExecutor = Executors.newSingleThreadExecutor();
+    private final ExecutorService mSendMessageExecutor = Executors.newSingleThreadExecutor();
+    private final ExecutorService mReceiveMessageExecutor = Executors.newSingleThreadExecutor();
 
     @Nullable
-    private BluetoothServerSocket serverSocket;
+    private BluetoothServerSocket mServerSocket;
     @Nullable
-    private BluetoothSocket socket;
+    private BluetoothSocket mSocket;
 
-    private State state = STOPPED;
-    private boolean isStopRequested = false;
+    private State mState = STOPPED;
+    private boolean mIsStopRequested = false;
 
     @Nullable
-    private RequestHandler requestHandler;
+    private RequestHandler mRequestHandler;
 
     @Nullable
-    private CountDownLatch countDownLatch;
+    private CountDownLatch mCountDownLatch;
     @Nullable
-    private StateMonitor stateMonitor;
+    private StateMonitor mStateMonitor;
 
     /**
      * Manages RfcommServer status.
@@ -108,12 +109,12 @@
     private void startServer() {
         log("Start RfcommServer");
 
-        if (!state.equals(STOPPED)) {
+        if (!mState.equals(STOPPED)) {
             log("Server is not stopped, skip start request.");
             return;
         }
         updateState(STARTING);
-        isStopRequested = false;
+        mIsStopRequested = false;
 
         startAccept();
     }
@@ -127,7 +128,7 @@
     private void startAccept() {
         try {
             // Gets server socket in controller thread for stop() API.
-            serverSocket =
+            mServerSocket =
                     BluetoothAdapter.getDefaultAdapter()
                             .listenUsingRfcommWithServiceRecord(
                                     FAST_PAIR_RFCOMM_SERVICE_NAME, FAST_PAIR_RFCOMM_UUID);
@@ -138,7 +139,7 @@
         }
 
         updateState(ACCEPTING);
-        new Thread(() -> accept(serverSocket)).start();
+        new Thread(() -> accept(mServerSocket)).start();
     }
 
     private void accept(BluetoothServerSocket serverSocket) {
@@ -156,7 +157,7 @@
     }
 
     private void handleAcceptException(BluetoothServerSocket serverSocket) {
-        if (isStopRequested) {
+        if (mIsStopRequested) {
             stopServer();
         } else {
             closeServerSocket(serverSocket);
@@ -165,7 +166,7 @@
     }
 
     private void startListen(BluetoothSocket bluetoothSocket) {
-        if (isStopRequested) {
+        if (mIsStopRequested) {
             closeSocket(bluetoothSocket);
             stopServer();
             return;
@@ -173,7 +174,7 @@
 
         updateState(CONNECTED);
         // Sets method parameter to global socket for stop() API.
-        this.socket = bluetoothSocket;
+        this.mSocket = bluetoothSocket;
         new Thread(() -> listen(bluetoothSocket)).start();
     }
 
@@ -195,11 +196,12 @@
                     } while (count < additionalLength);
                 }
 
-                if (requestHandler != null) {
-                    // In order not to block listening thread, use different thread to dispatch message.
-                    receiveMessageExecutor.execute(
+                if (mRequestHandler != null) {
+                    // In order not to block listening thread, use different thread to dispatch
+                    // message.
+                    mReceiveMessageExecutor.execute(
                             () -> {
-                                requestHandler.handleRequest(eventGroup, eventCode, data);
+                                mRequestHandler.handleRequest(eventGroup, eventCode, data);
                                 triggerCountdownLatch();
                             });
                 }
@@ -207,13 +209,14 @@
         } catch (IOException e) {
             log(
                     String.format(
-                            "IOException when listening to %s", bluetoothSocket.getRemoteDevice().getAddress()));
+                            "IOException when listening to %s",
+                            bluetoothSocket.getRemoteDevice().getAddress()));
             runInControllerExecutor(() -> handleListenException(bluetoothSocket));
         }
     }
 
     private void handleListenException(BluetoothSocket bluetoothSocket) {
-        if (isStopRequested) {
+        if (mIsStopRequested) {
             stopServer();
         } else {
             closeSocket(bluetoothSocket);
@@ -225,15 +228,18 @@
         switch (eventGroup) {
             case BLUETOOTH:
                 send(EventStreamProtocol.EventGroup.BLUETOOTH_VALUE,
-                        EventStreamProtocol.BluetoothEventCode.BLUETOOTH_ENABLE_SILENCE_MODE_VALUE, new byte[0]);
+                        EventStreamProtocol.BluetoothEventCode.BLUETOOTH_ENABLE_SILENCE_MODE_VALUE,
+                        new byte[0]);
                 break;
             case LOGGING:
-                send(EventStreamProtocol.EventGroup.LOGGING_VALUE, EventStreamProtocol.LoggingEventCode.LOG_FULL_VALUE,
+                send(EventStreamProtocol.EventGroup.LOGGING_VALUE,
+                        EventStreamProtocol.LoggingEventCode.LOG_FULL_VALUE,
                         new byte[0]);
                 break;
             case DEVICE:
                 send(EventStreamProtocol.EventGroup.DEVICE_VALUE,
-                        EventStreamProtocol.DeviceEventCode.DEVICE_BATTERY_INFO_VALUE, new byte[]{0x11, 0x12, 0x13});
+                        EventStreamProtocol.DeviceEventCode.DEVICE_BATTERY_INFO_VALUE,
+                        new byte[]{0x11, 0x12, 0x13});
                 break;
             default: // fall out
         }
@@ -248,12 +254,12 @@
     public void send(int eventGroup, int eventCode, byte[] data) {
         runInControllerExecutor(
                 () -> {
-                    if (!CONNECTED.equals(state)) {
+                    if (!CONNECTED.equals(mState)) {
                         log("Server is not in CONNECTED state, skip send request");
                         return;
                     }
-                    BluetoothSocket bluetoothSocket = this.socket;
-                    sendMessageExecutor.execute(() -> {
+                    BluetoothSocket bluetoothSocket = this.mSocket;
+                    mSendMessageExecutor.execute(() -> {
                         String address = bluetoothSocket.getRemoteDevice().getAddress();
                         try {
                             DataOutputStream dataOutputStream =
@@ -285,23 +291,23 @@
         runInControllerExecutor(() -> {
             log("Stop RfcommServer");
 
-            if (STOPPED.equals(state)) {
+            if (STOPPED.equals(mState)) {
                 log("Server is stopped, skip stop request.");
                 return;
             }
 
-            if (isStopRequested) {
+            if (mIsStopRequested) {
                 log("Stop is already requested, skip stop request.");
                 return;
             }
-            isStopRequested = true;
+            mIsStopRequested = true;
 
-            if (ACCEPTING.equals(state)) {
-                closeServerSocket(serverSocket);
+            if (ACCEPTING.equals(mState)) {
+                closeServerSocket(mServerSocket);
             }
 
-            if (CONNECTED.equals(state)) {
-                closeSocket(socket);
+            if (CONNECTED.equals(mState)) {
+                closeSocket(mSocket);
             }
         });
     }
@@ -312,11 +318,11 @@
     }
 
     private void updateState(State newState) {
-        log(String.format("Change state from %s to %s", state, newState));
-        if (stateMonitor != null) {
-            stateMonitor.onStateChanged(newState);
+        log(String.format("Change state from %s to %s", mState, newState));
+        if (mStateMonitor != null) {
+            mStateMonitor.onStateChanged(newState);
         }
-        state = newState;
+        mState = newState;
     }
 
     private void closeServerSocket(BluetoothServerSocket serverSocket) {
@@ -342,25 +348,26 @@
                 socket.close();
             }
         } catch (IOException e) {
-            log(String.format("IOException when close socket %s", socket.getRemoteDevice().getAddress()));
+            log(String.format("IOException when close socket %s",
+                    socket.getRemoteDevice().getAddress()));
         }
     }
 
     private void runInControllerExecutor(Runnable runnable) {
-        controllerExecutor.execute(runnable);
+        mControllerExecutor.execute(runnable);
     }
 
     private void log(String message) {
-        logger.log("Server=%s, %s", FAST_PAIR_RFCOMM_SERVICE_NAME, message);
+        mLogger.log("Server=%s, %s", FAST_PAIR_RFCOMM_SERVICE_NAME, message);
     }
 
     private void log(String message, Throwable e) {
-        logger.log(e, "Server=%s, %s", FAST_PAIR_RFCOMM_SERVICE_NAME, message);
+        mLogger.log(e, "Server=%s, %s", FAST_PAIR_RFCOMM_SERVICE_NAME, message);
     }
 
     private void triggerCountdownLatch() {
-        if (countDownLatch != null) {
-            countDownLatch.countDown();
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
         }
     }
 
@@ -370,7 +377,7 @@
     }
 
     public void setRequestHandler(@Nullable RequestHandler requestHandler) {
-        this.requestHandler = requestHandler;
+        this.mRequestHandler = requestHandler;
     }
 
     /** A state monitor to send signal when state is changed. */
@@ -379,24 +386,24 @@
     }
 
     public void setStateMonitor(@Nullable StateMonitor stateMonitor) {
-        this.stateMonitor = stateMonitor;
+        this.mStateMonitor = stateMonitor;
     }
 
     @VisibleForTesting
     void setCountDownLatch(@Nullable CountDownLatch countDownLatch) {
-        this.countDownLatch = countDownLatch;
+        this.mCountDownLatch = countDownLatch;
     }
 
     @VisibleForTesting
     void setIsStopRequested(boolean isStopRequested) {
-        this.isStopRequested = isStopRequested;
+        this.mIsStopRequested = isStopRequested;
     }
 
     @VisibleForTesting
     void simulateAcceptIOException() {
         runInControllerExecutor(() -> {
-            if (ACCEPTING.equals(state)) {
-                closeServerSocket(serverSocket);
+            if (ACCEPTING.equals(mState)) {
+                closeServerSocket(mServerSocket);
             }
         });
     }
@@ -404,8 +411,8 @@
     @VisibleForTesting
     void simulateListenIOException() {
         runInControllerExecutor(() -> {
-            if (CONNECTED.equals(state)) {
-                closeSocket(socket);
+            if (CONNECTED.equals(mState)) {
+                closeSocket(mSocket);
             }
         });
     }
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/Crypto.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/Crypto.java
similarity index 97%
rename from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/Crypto.java
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/Crypto.java
index 1543953..0aa4f6e 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/Crypto.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/Crypto.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.common.bluetooth.fastpair.testing;
+package android.nearby.fastpair.provider.crypto;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/E2eeCalculator.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/E2eeCalculator.java
similarity index 89%
rename from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/E2eeCalculator.java
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/E2eeCalculator.java
index 6f213e6..794c19d 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/E2eeCalculator.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/E2eeCalculator.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.common.bluetooth.fastpair.testing;
+package android.nearby.fastpair.provider.crypto;
 
 import com.google.common.base.Preconditions;
 import com.google.common.base.Verify;
@@ -43,18 +43,18 @@
     private static final int E2EE_EID_SIZE = 20;
 
     /**
-     * Computes the E2EE EID value for the given device clock based time. Note that Eddystone beacons
-     * start advertising the new EID at a random time within the window, therefore the currently
-     * advertised EID for beacon time <em>t</em> may be either {@code computeE2eeEid(eik, k, t)} or
-     * {@code computeE2eeEid(eik, k, t - (1 << k))}.
+     * Computes the E2EE EID value for the given device clock based time. Note that Eddystone
+     * beacons start advertising the new EID at a random time within the window, therefore the
+     * currently advertised EID for beacon time <em>t</em> may be either
+     * {@code computeE2eeEid(eik, k, t)} or {@code computeE2eeEid(eik, k, t - (1 << k))}.
      *
      * <p>The E2EE EID computation is based on https://goto.google.com/e2ee-eid-computation.
      *
      * @param identityKey        the beacon's 32-byte Eddystone E2EE identity key
-     * @param exponent           rotation period exponent as configured on the beacon, must be in range the [0,
-     *                           15]
-     * @param deviceClockSeconds the value of the beacon's 32-bit seconds time counter (treated as an
-     *                           unsigned value)
+     * @param exponent           rotation period exponent as configured on the beacon, must be in
+     *                           range the [0,15]
+     * @param deviceClockSeconds the value of the beacon's 32-bit seconds time counter (treated as
+     *                           an unsigned value)
      * @return E2EE EID value.
      */
     public static ByteString computeE2eeEid(
@@ -96,8 +96,8 @@
         byte[] unalignedBytes = point.getAffineX().toByteArray();
 
         // The unalignedBytes may have length < 32 if the leading E2EE EID bytes are zero, or
-        // it may be E2EE_EID_SIZE + 1 if the leading bit is 1, in which case the first byte is always
-        // zero.
+        // it may be E2EE_EID_SIZE + 1 if the leading bit is 1, in which case the first byte is
+        // always zero.
         Verify.verify(
                 unalignedBytes.length <= E2EE_EID_SIZE
                         || (unalignedBytes.length == E2EE_EID_SIZE + 1 && unalignedBytes[0] == 0));
@@ -106,7 +106,8 @@
         if (unalignedBytes.length < E2EE_EID_SIZE) {
             bytes = new byte[E2EE_EID_SIZE];
             System.arraycopy(
-                    unalignedBytes, 0, bytes, bytes.length - unalignedBytes.length, unalignedBytes.length);
+                    unalignedBytes, 0, bytes, bytes.length - unalignedBytes.length,
+                    unalignedBytes.length);
         } else if (unalignedBytes.length == E2EE_EID_SIZE + 1) {
             bytes = new byte[E2EE_EID_SIZE];
             System.arraycopy(unalignedBytes, 1, bytes, 0, E2EE_EID_SIZE);
@@ -161,7 +162,8 @@
                         .subtract(s.getAffineY())
                         .multiply(r.getAffineX().subtract(s.getAffineX()).modInverse(P))
                         .mod(P);
-        BigInteger x = slope.modPow(TWO, P).subtract(r.getAffineX()).subtract(s.getAffineX()).mod(P);
+        BigInteger x =
+                slope.modPow(TWO, P).subtract(r.getAffineX()).subtract(s.getAffineX()).mod(P);
         BigInteger y = s.getAffineY().negate().mod(P);
         y = y.add(slope.multiply(s.getAffineX().subtract(x))).mod(P);
         return new ECPoint(x, y);
@@ -176,7 +178,8 @@
         slope = slope.add(CURVE_SPEC.getCurve().getA());
         slope = slope.multiply(r.getAffineY().multiply(TWO).modInverse(P));
         BigInteger x = slope.pow(2).subtract(r.getAffineX().multiply(TWO)).mod(P);
-        BigInteger y = r.getAffineY().negate().add(slope.multiply(r.getAffineX().subtract(x))).mod(P);
+        BigInteger y =
+                r.getAffineY().negate().add(slope.multiply(r.getAffineX().subtract(x))).mod(P);
         return new ECPoint(x, y);
     }
 
diff --git a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/Logger.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/utils/Logger.java
similarity index 79%
rename from nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/Logger.java
rename to nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/utils/Logger.java
index 37b065f..794f100 100644
--- a/nearby/tests/multidevices/clients/src/com/android/server/nearby/common/bluetooth/fastpair/testing/Logger.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/utils/Logger.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.common.bluetooth.fastpair.testing;
+package android.nearby.fastpair.provider.utils;
 
 import android.util.Log;
 
@@ -26,10 +26,10 @@
  * The base context for a logging statement.
  */
 public class Logger {
-    private final String tag;
+    private final String mString;
 
     public Logger(String tag) {
-        this.tag = tag;
+        this.mString = tag;
     }
 
     @FormatMethod
@@ -41,10 +41,10 @@
     @FormatMethod
     public void log(@Nullable Throwable exception, String message, Object... objects) {
         if (exception == null) {
-            Log.i(tag, String.format(message, objects));
+            Log.i(mString, String.format(message, objects));
         } else {
-            Log.w(tag, String.format(message, objects));
-            Log.w(tag, String.format("Cause: %s", exception));
+            Log.w(mString, String.format(message, objects));
+            Log.w(mString, String.format("Cause: %s", exception));
         }
     }
 }
diff --git a/nearby/tests/multidevices/clients/proto/Android.bp b/nearby/tests/multidevices/clients/test_support/snippet_helper/Android.bp
similarity index 73%
copy from nearby/tests/multidevices/clients/proto/Android.bp
copy to nearby/tests/multidevices/clients/test_support/snippet_helper/Android.bp
index 80e09b4..697c88d 100644
--- a/nearby/tests/multidevices/clients/proto/Android.bp
+++ b/nearby/tests/multidevices/clients/test_support/snippet_helper/Android.bp
@@ -16,15 +16,9 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-java_library {
-    name: "NearbyMultiDevicesClientsFastPairLiteProtos",
-    proto: {
-        type: "lite",
-        canonical_path_from_root: false,
-    },
-    sdk_version: "system_current",
-    min_sdk_version: "30",
-    srcs: ["src/*/*.proto"],
+android_library {
+    name: "MoblySnippetHelperLib",
+    srcs: ["src/**/*.kt"],
+    sdk_version: "test_current",
+    static_libs: ["mobly-snippet-lib",],
 }
-
-
diff --git a/nearby/tests/multidevices/clients/test_support/snippet_helper/AndroidManifest.xml b/nearby/tests/multidevices/clients/test_support/snippet_helper/AndroidManifest.xml
new file mode 100644
index 0000000..4858f46
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/snippet_helper/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.mobly.snippet.util">
+
+</manifest>
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/common/SnippetEventHelper.kt b/nearby/tests/multidevices/clients/test_support/snippet_helper/src/com/google/android/mobly/snippet/util/SnippetEventHelper.kt
similarity index 96%
rename from nearby/tests/multidevices/clients/src/android/nearby/multidevices/common/SnippetEventHelper.kt
rename to nearby/tests/multidevices/clients/test_support/snippet_helper/src/com/google/android/mobly/snippet/util/SnippetEventHelper.kt
index c4816fb..0dbcb57 100644
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/common/SnippetEventHelper.kt
+++ b/nearby/tests/multidevices/clients/test_support/snippet_helper/src/com/google/android/mobly/snippet/util/SnippetEventHelper.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.nearby.multidevices.common
+package com.google.android.mobly.snippet.util
 
 import android.os.Bundle
 import com.google.android.mobly.snippet.event.EventCache
diff --git a/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/Android.bp b/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/Android.bp
new file mode 100644
index 0000000..284d5c2
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/Android.bp
@@ -0,0 +1,38 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Run the tests: atest --host MoblySnippetHelperRoboTest
+android_robolectric_test {
+    name: "MoblySnippetHelperRoboTest",
+    srcs: ["src/**/*.kt"],
+    instrumentation_for: "NearbyMultiDevicesClientsSnippets",
+    java_resources: ["robolectric.properties"],
+
+    static_libs: [
+        "MoblySnippetHelperLib",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "junit",
+        "mobly-snippet-lib",
+        "truth-prebuilt",
+    ],
+    test_options: {
+        // timeout in seconds.
+        timeout: 36000,
+    },
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/AndroidManifest.xml b/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/AndroidManifest.xml
new file mode 100644
index 0000000..f1fef23
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.google.android.mobly.snippet.util"/>
diff --git a/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/robolectric.properties b/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/robolectric.properties
new file mode 100644
index 0000000..2ea03bb
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/robolectric.properties
@@ -0,0 +1,16 @@
+#
+# Copyright (C) 2022 Google Inc.
+#
+# 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.
+#
+sdk=NEWEST_SDK
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/tests/src/com/android/nearby/multidevices/common/SnippetEventHelperTest.kt b/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/src/com/google/android/mobly/snippet/util/SnippetEventHelperTest.kt
similarity index 94%
rename from nearby/tests/multidevices/clients/tests/src/com/android/nearby/multidevices/common/SnippetEventHelperTest.kt
rename to nearby/tests/multidevices/clients/test_support/snippet_helper/tests/src/com/google/android/mobly/snippet/util/SnippetEventHelperTest.kt
index 1fbd352..641ab82 100644
--- a/nearby/tests/multidevices/clients/tests/src/com/android/nearby/multidevices/common/SnippetEventHelperTest.kt
+++ b/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/src/com/google/android/mobly/snippet/util/SnippetEventHelperTest.kt
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-package com.android.nearby.multidevices.common
+package com.google.android.mobly.snippet.util
 
-import android.nearby.multidevices.common.postSnippetEvent
 import androidx.test.platform.app.InstrumentationRegistry
 import com.google.android.mobly.snippet.event.EventSnippet
 import com.google.android.mobly.snippet.util.Log
diff --git a/nearby/tests/multidevices/clients/tests/src/com/android/nearby/multidevices/fastpair/seeker/CompanionAppUtilsTest.kt b/nearby/tests/multidevices/clients/tests/src/com/android/nearby/multidevices/fastpair/seeker/CompanionAppUtilsTest.kt
deleted file mode 100644
index 94c0952..0000000
--- a/nearby/tests/multidevices/clients/tests/src/com/android/nearby/multidevices/fastpair/seeker/CompanionAppUtilsTest.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * 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.nearby.multidevices.fastpair.seeker
-
-import android.nearby.multidevices.fastpair.seeker.generateCompanionAppLaunchIntentUri
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.RobolectricTestRunner
-
-/** Robolectric tests for CompanionAppUtils.kt. */
-@RunWith(RobolectricTestRunner::class)
-class CompanionAppUtilsTest {
-
-    @Test
-    fun testGenerateCompanionAppLaunchIntentUri_defaultNullPackage_returnsEmptyString() {
-        assertThat(generateCompanionAppLaunchIntentUri()).isEmpty()
-    }
-
-    @Test
-    fun testGenerateCompanionAppLaunchIntentUri_emptyPackageName_returnsEmptyString() {
-        assertThat(generateCompanionAppLaunchIntentUri(companionAppPackageName = "")).isEmpty()
-    }
-
-    @Test
-    fun testGenerateCompanionAppLaunchIntentUri_emptyActivityName_returnsEmptyString() {
-        val uriString = generateCompanionAppLaunchIntentUri(
-                companionAppPackageName = COMPANION_APP_PACKAGE_TEST_CONSTANT, activityName = "")
-
-        assertThat(uriString).isEmpty()
-    }
-
-    @Test
-    fun testGenerateCompanionAppLaunchIntentUri_emptyAction_returnsNoActionUriString() {
-        val uriString = generateCompanionAppLaunchIntentUri(
-                companionAppPackageName = COMPANION_APP_PACKAGE_TEST_CONSTANT,
-                activityName = COMPANION_APP_ACTIVITY_TEST_CONSTANT,
-                action = "")
-
-        assertThat(uriString).doesNotContain("action=")
-        assertThat(uriString).contains("package=$COMPANION_APP_PACKAGE_TEST_CONSTANT")
-        assertThat(uriString).contains(COMPANION_APP_ACTIVITY_TEST_CONSTANT)
-    }
-
-    @Test
-    fun testGenerateCompanionAppLaunchIntentUri_nonNullArgs_returnsUriString() {
-        val uriString = generateCompanionAppLaunchIntentUri(
-                companionAppPackageName = COMPANION_APP_PACKAGE_TEST_CONSTANT,
-                activityName = COMPANION_APP_ACTIVITY_TEST_CONSTANT,
-                action = COMPANION_APP_ACTION_TEST_CONSTANT)
-
-        assertThat(uriString).isEqualTo("intent:#Intent;" +
-                "action=android.nearby.SHOW_WELCOME;" +
-                "package=android.nearby.companion;" +
-                "component=android.nearby.companion/android.nearby.companion.MainActivity;" +
-                "end")
-    }
-
-    companion object {
-        private const val COMPANION_APP_PACKAGE_TEST_CONSTANT = "android.nearby.companion"
-        private const val COMPANION_APP_ACTIVITY_TEST_CONSTANT =
-                "android.nearby.companion.MainActivity"
-        private const val COMPANION_APP_ACTION_TEST_CONSTANT = "android.nearby.SHOW_WELCOME"
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
new file mode 100644
index 0000000..2ade5f2
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.pairinghandler;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import androidx.annotation.Nullable;
+
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+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.notification.FastPairNotificationManager;
+import com.android.server.nearby.fastpair.testing.FakeDiscoveryItems;
+
+import com.google.protobuf.ByteString;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.time.Clock;
+
+import service.proto.Cache;
+import service.proto.Rpcs;
+
+public class PairingProgressHandlerBaseTest {
+    @Mock
+    Locator mLocator;
+    @Mock
+    LocatorContextWrapper mContextWrapper;
+    @Mock
+    Clock mClock;
+    @Mock
+    FastPairCacheManager mFastPairCacheManager;
+    @Mock
+    FootprintsDeviceManager mFootprintsDeviceManager;
+    private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
+
+    @Before
+    public void setup() {
+
+        MockitoAnnotations.initMocks(this);
+        when(mContextWrapper.getLocator()).thenReturn(mLocator);
+        mLocator.overrideBindingForTest(FastPairCacheManager.class,
+                mFastPairCacheManager);
+        mLocator.overrideBindingForTest(Clock.class, mClock);
+    }
+
+    @Test
+    public void createHandler_halfSheetSubsequentPairing_notificationPairingHandlerCreated() {
+
+        DiscoveryItem discoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
+        discoveryItem.setStoredItemForTest(
+                discoveryItem.getStoredItemForTest().toBuilder()
+                        .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
+                        .setFastPairInformation(
+                                Cache.FastPairInformation.newBuilder()
+                                        .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
+                        .build());
+
+        PairingProgressHandlerBase progressHandler =
+                createProgressHandler(ACCOUNT_KEY, discoveryItem, /* isRetroactivePair= */ false);
+
+        assertThat(progressHandler).isInstanceOf(NotificationPairingProgressHandler.class);
+    }
+
+    @Test
+    public void createHandler_halfSheetInitialPairing_halfSheetPairingHandlerCreated() {
+        // No account key
+        DiscoveryItem discoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
+        discoveryItem.setStoredItemForTest(
+                discoveryItem.getStoredItemForTest().toBuilder()
+                        .setFastPairInformation(
+                                Cache.FastPairInformation.newBuilder()
+                                        .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
+                        .build());
+
+        PairingProgressHandlerBase progressHandler =
+                createProgressHandler(null, discoveryItem, /* isRetroactivePair= */ false);
+
+        assertThat(progressHandler).isInstanceOf(HalfSheetPairingProgressHandler.class);
+    }
+
+    private PairingProgressHandlerBase createProgressHandler(
+            @Nullable byte[] accountKey, DiscoveryItem fastPairItem, boolean isRetroactivePair) {
+        FastPairNotificationManager fastPairNotificationManager =
+                new FastPairNotificationManager(mContextWrapper, fastPairItem, true);
+        FastPairHalfSheetManager fastPairHalfSheetManager =
+                new FastPairHalfSheetManager(mContextWrapper);
+        mLocator.overrideBindingForTest(FastPairHalfSheetManager.class, fastPairHalfSheetManager);
+        PairingProgressHandlerBase pairingProgressHandlerBase =
+                PairingProgressHandlerBase.create(
+                        mContextWrapper,
+                        fastPairItem,
+                        fastPairItem.getAppPackageName(),
+                        accountKey,
+                        mFootprintsDeviceManager,
+                        fastPairNotificationManager,
+                        fastPairHalfSheetManager,
+                        isRetroactivePair);
+        return pairingProgressHandlerBase;
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java
new file mode 100644
index 0000000..aa7e6f6
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java
@@ -0,0 +1,55 @@
+/*
+ * 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.testing;
+
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+
+import service.proto.Cache;
+
+public class FakeDiscoveryItems {
+    public static final String DEFAULT_MAC_ADDRESS = "00:11:22:33:44:55";
+    public static final long DEFAULT_TIMESTAMP = 1000000000L;
+    public static final String DEFAULT_DESCRIPITON = "description";
+    public static final String TRIGGER_ID = "trigger.id";
+    private static final String FAST_PAIR_ID = "id";
+    private static final int RSSI = -80;
+    private static final int TX_POWER = -10;
+    public static DiscoveryItem newFastPairDiscoveryItem(LocatorContextWrapper contextWrapper) {
+        return new DiscoveryItem(contextWrapper, newFastPairDeviceStoredItem());
+    }
+
+    public static Cache.StoredDiscoveryItem newFastPairDeviceStoredItem() {
+        return newFastPairDeviceStoredItem(TRIGGER_ID);
+    }
+
+    public static Cache.StoredDiscoveryItem newFastPairDeviceStoredItem(String triggerId) {
+        Cache.StoredDiscoveryItem.Builder item = Cache.StoredDiscoveryItem.newBuilder();
+        item.setState(Cache.StoredDiscoveryItem.State.STATE_ENABLED);
+        item.setId(FAST_PAIR_ID);
+        item.setDescription(DEFAULT_DESCRIPITON);
+        item.setType(Cache.NearbyType.NEARBY_DEVICE);
+        item.setTriggerId(triggerId);
+        item.setMacAddress(DEFAULT_MAC_ADDRESS);
+        item.setFirstObservationTimestampMillis(DEFAULT_TIMESTAMP);
+        item.setLastObservationTimestampMillis(DEFAULT_TIMESTAMP);
+        item.setRssi(RSSI);
+        item.setTxPower(TX_POWER);
+        return item.build();
+    }
+
+}