Merge "Update TEST_MAPPING file." am: 8300421bc5 am: fa4f870706
Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/2056266
Change-Id: Id97414ab712cd4a7a0fcc9d7404bc2e9f78224c8
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Tethering/AndroidManifest.xml b/Tethering/AndroidManifest.xml
index 6deb345..b832e16 100644
--- a/Tethering/AndroidManifest.xml
+++ b/Tethering/AndroidManifest.xml
@@ -41,6 +41,7 @@
<uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
<uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<protected-broadcast android:name="com.android.server.connectivity.tethering.DISABLE_TETHERING" />
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 47a163f..d79edb4 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -22,16 +22,16 @@
// different value depending on the branch.
java_defaults {
name: "ConnectivityNextEnableDefaults",
- enabled: false,
+ enabled: true,
}
apex_defaults {
name: "ConnectivityApexDefaults",
// Tethering app to include in the AOSP apex. Branches that disable the "next" targets may use
// a stable tethering app instead, but will generally override the AOSP apex to use updatable
// package names and keys, so that apex will be unused anyway.
- apps: ["Tethering"], // Replace to "Tethering" if ConnectivityNextEnableDefaults is false.
+ apps: ["TetheringNext"], // Replace to "Tethering" if ConnectivityNextEnableDefaults is false.
}
-enable_tethering_next_apex = false
+enable_tethering_next_apex = true
// This is a placeholder comment to avoid merge conflicts
// as the above target may have different "enabled" values
// depending on the branch
@@ -60,8 +60,7 @@
both: {
jni_libs: [
"libframework-connectivity-jni",
- // Changed in sc-mainline-prod only: no framework-connectivity-t
- // "libframework-connectivity-tiramisu-jni"
+ "libframework-connectivity-tiramisu-jni"
],
},
},
@@ -79,6 +78,7 @@
],
apps: [
"ServiceConnectivityResources",
+ "HalfSheetUX",
],
prebuilts: [
"current_sdkinfo",
@@ -111,7 +111,7 @@
name: "com.android.tethering-bootclasspath-fragment",
contents: [
"framework-connectivity",
- // Changed in sc-mainline-prod only: no framework-connectivity-t
+ "framework-connectivity-t",
"framework-tethering",
],
apex_available: ["com.android.tethering"],
@@ -134,18 +134,15 @@
// modified by the Soong or platform compat team.
hidden_api: {
max_target_r_low_priority: [
- // Changed in sc-mainline-prod only: no list for
- // framework-connectivity-t APIs as it is not in the APEX
- ],
+ "hiddenapi/hiddenapi-max-target-r-loprio.txt",
+ ],
max_target_o_low_priority: [
"hiddenapi/hiddenapi-max-target-o-low-priority.txt",
- // Changed in sc-mainline-prod only: no list for
- // framework-connectivity-t APIs as it is not in the APEX
- ],
+ "hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt",
+ ],
unsupported: [
"hiddenapi/hiddenapi-unsupported.txt",
- // Changed in sc-mainline-prod only: no framework-connectivity-t
- // "hiddenapi/hiddenapi-unsupported-tiramisu.txt",
+ "hiddenapi/hiddenapi-unsupported-tiramisu.txt",
],
// The following packages contain classes from other modules on the
@@ -156,6 +153,7 @@
// API.
split_packages: [
"android.app.usage",
+ "android.nearby",
"android.net",
"android.net.netstats",
"android.net.util",
@@ -168,6 +166,7 @@
// classes into an API surface, e.g. public, system, etc.. Doing so will
// result in a build failure due to inconsistent flags.
package_prefixes: [
+ "android.nearby.aidl",
"android.net.apf",
"android.net.connectivity",
"android.net.netstats.provider",
diff --git a/Tethering/apex/manifest.json b/Tethering/apex/manifest.json
index 88f13b2..dcc8493 100644
--- a/Tethering/apex/manifest.json
+++ b/Tethering/apex/manifest.json
@@ -1,4 +1,4 @@
{
"name": "com.android.tethering",
- "version": 319999900
+ "version": 330000000
}
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 25489ff..9ca3f14 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -21,7 +21,6 @@
name: "framework-tethering",
defaults: ["framework-module-defaults"],
impl_library_visibility: [
- "//frameworks/base/packages/Tethering:__subpackages__",
"//packages/modules/Connectivity/Tethering:__subpackages__",
// Using for test only
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index d1b8380..37b1bc8 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -67,6 +67,7 @@
"ext",
"framework-minus-apex",
"framework-res",
+ "framework-bluetooth.stubs.module_lib",
"framework-connectivity.impl",
"framework-connectivity-t.impl",
"framework-tethering.impl",
diff --git a/buildstubs-t/Android.bp b/buildstubs-t/Android.bp
deleted file mode 100644
index 9ca3fd2..0000000
--- a/buildstubs-t/Android.bp
+++ /dev/null
@@ -1,80 +0,0 @@
-//
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-package {
- // See: http://go/android-license-faq
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-// Placeholder empty filegroups to avoid merge conflicts on build rules
-// on a branch that does not have the filegroups
-
-filegroup {
- name: "framework-connectivity-tiramisu-updatable-sources",
- srcs: [],
-}
-
-filegroup {
- name: "services.connectivity-tiramisu-updatable-sources",
- srcs: ["stubs-src/**/*.java"],
-}
-
-filegroup {
- name: "framework-connectivity-api-shared-srcs",
- srcs: [],
-}
-
-filegroup {
- name: "ethernet-service-updatable-sources",
- srcs: [],
-}
-
-filegroup {
- name: "services.connectivity-netstats-jni-sources",
- srcs: [
- "stubs-src-jni/mock_com_android_server_net_NetworkStatsFactory.cpp",
- "stubs-src-jni/mock_com_android_server_net_NetworkStatsService.cpp",
- ],
- visibility: [
- "//packages/modules/Connectivity:__subpackages__",
- ],
-}
-
-filegroup {
- name: "framework-connectivity-tiramisu-jni-sources",
- srcs: [
- "stubs-src-jni/mock_android_net_TrafficStats.cpp",
- ],
- visibility: [
- "//packages/modules/Connectivity:__subpackages__",
- ],
-}
-
-// Empty replacement for framework-connectivity-t.impl and stubs,
-// as framework-connectivity is disabled in the branch
-java_library {
- name: "framework-connectivity-t.impl",
- min_sdk_version: "Tiramisu",
- sdk_version: "module_current",
- srcs: [],
-}
-
-java_library {
- name: "framework-connectivity-t.stubs.module_lib",
- min_sdk_version: "Tiramisu",
- sdk_version: "module_current",
- srcs: [],
-}
diff --git a/buildstubs-t/stubs-src-jni/mock_android_net_TrafficStats.cpp b/buildstubs-t/stubs-src-jni/mock_android_net_TrafficStats.cpp
deleted file mode 100644
index ef5d874..0000000
--- a/buildstubs-t/stubs-src-jni/mock_android_net_TrafficStats.cpp
+++ /dev/null
@@ -1,25 +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.
- */
-
-#include <nativehelper/JNIHelp.h>
-
-namespace android {
-
-int register_android_net_TrafficStats(JNIEnv* env) {
- return JNI_ERR;
-}
-
-}; // namespace android
diff --git a/buildstubs-t/stubs-src-jni/mock_com_android_server_net_NetworkStatsFactory.cpp b/buildstubs-t/stubs-src-jni/mock_com_android_server_net_NetworkStatsFactory.cpp
deleted file mode 100644
index 594a174..0000000
--- a/buildstubs-t/stubs-src-jni/mock_com_android_server_net_NetworkStatsFactory.cpp
+++ /dev/null
@@ -1,25 +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.
- */
-
-#include <nativehelper/JNIHelp.h>
-
-namespace android {
-
-int register_android_server_net_NetworkStatsFactory(JNIEnv* env) {
- return JNI_ERR;
-}
-
-}; // namespace android
diff --git a/buildstubs-t/stubs-src-jni/mock_com_android_server_net_NetworkStatsService.cpp b/buildstubs-t/stubs-src-jni/mock_com_android_server_net_NetworkStatsService.cpp
deleted file mode 100644
index b0c42b0..0000000
--- a/buildstubs-t/stubs-src-jni/mock_com_android_server_net_NetworkStatsService.cpp
+++ /dev/null
@@ -1,25 +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.
- */
-
-#include <nativehelper/JNIHelp.h>
-
-namespace android {
-
-int register_android_server_net_NetworkStatsService(JNIEnv* env) {
- return JNI_ERR;
-}
-
-}; // namespace android
diff --git a/buildstubs-t/stubs-src/android/net/TrafficStats.java b/buildstubs-t/stubs-src/android/net/TrafficStats.java
deleted file mode 100644
index 0b208ac..0000000
--- a/buildstubs-t/stubs-src/android/net/TrafficStats.java
+++ /dev/null
@@ -1,31 +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.net;
-
-import android.content.Context;
-
-/**
- * Fake TrafficStats class for sc-mainline-prod,
- * to allow building the T service-connectivity before sources
- * are moved to the branch.
- */
-public final class TrafficStats {
- /** Init */
- public static void init(Context context) {
- throw new RuntimeException("This is a stub class");
- }
-}
diff --git a/buildstubs-t/stubs-src/com/android/server/EthernetService.java b/buildstubs-t/stubs-src/com/android/server/EthernetService.java
deleted file mode 100644
index 4a06e1e..0000000
--- a/buildstubs-t/stubs-src/com/android/server/EthernetService.java
+++ /dev/null
@@ -1,31 +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.server.ethernet;
-
-import android.content.Context;
-
-/**
- * Fake EthernetService class for branches that do not have the updatable EthernetService yet,
- * to allow building the T service-connectivity before sources are moved to the branch.
- */
-public final class EthernetService {
- /** Create instance */
- public static EthernetServiceImpl create(Context ctx) {
- throw new RuntimeException("This is a stub class");
- }
-}
-
diff --git a/buildstubs-t/stubs-src/com/android/server/EthernetServiceImpl.java b/buildstubs-t/stubs-src/com/android/server/EthernetServiceImpl.java
deleted file mode 100644
index eb3bfa0..0000000
--- a/buildstubs-t/stubs-src/com/android/server/EthernetServiceImpl.java
+++ /dev/null
@@ -1,28 +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.server.ethernet;
-
-import android.os.Binder;
-
-/** Stub class for EthernetServiceImpl */
-public class EthernetServiceImpl extends Binder {
- /** Start service */
- public void start() {
- throw new RuntimeException("This is a stub class");
- }
-}
-
diff --git a/buildstubs-t/stubs-src/com/android/server/IpSecService.java b/buildstubs-t/stubs-src/com/android/server/IpSecService.java
deleted file mode 100644
index bb48c14..0000000
--- a/buildstubs-t/stubs-src/com/android/server/IpSecService.java
+++ /dev/null
@@ -1,31 +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.server;
-
-import android.content.Context;
-import android.os.Binder;
-
-/**
- * Fake IpSecManager class for sc-mainline-prod,
- * to allow building the T service-connectivity before sources
- * are moved to the branch
- */
-public final class IpSecService extends Binder {
- public IpSecService(Context ctx) {
- throw new RuntimeException("This is a stub class");
- }
-}
diff --git a/buildstubs-t/stubs-src/com/android/server/NsdService.java b/buildstubs-t/stubs-src/com/android/server/NsdService.java
deleted file mode 100644
index 0c625f0..0000000
--- a/buildstubs-t/stubs-src/com/android/server/NsdService.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server;
-
-import android.content.Context;
-import android.os.Binder;
-
-/**
- * Fake NsdService class for sc-mainline-prod,
- * to allow building the T service-connectivity before sources
- * are moved to the branch
- */
-public final class NsdService extends Binder {
- /** Create instance */
- public static NsdService create(Context ctx) {
- throw new RuntimeException("This is a stub class");
- }
-}
diff --git a/buildstubs-t/stubs-src/com/android/server/net/NetworkStatsService.java b/buildstubs-t/stubs-src/com/android/server/net/NetworkStatsService.java
deleted file mode 100644
index 8568e2a..0000000
--- a/buildstubs-t/stubs-src/com/android/server/net/NetworkStatsService.java
+++ /dev/null
@@ -1,37 +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.server.net;
-
-import android.content.Context;
-import android.os.Binder;
-
-/**
- * Fake NetworkStatsService class for sc-mainline-prod,
- * to allow building the T service-connectivity before sources
- * are moved to the branch
- */
-public final class NetworkStatsService extends Binder {
- /** Create instance */
- public static NetworkStatsService create(Context ctx) {
- throw new RuntimeException("This is a stub class");
- }
-
- /** System Ready */
- public void systemReady() {
- throw new RuntimeException("This is a stub class");
- }
-}
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index 292dc3c..9c8b359 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -19,9 +19,12 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
+// Include build rules from Sources.bp
+build = ["Sources.bp"]
+
java_defaults {
name: "enable-framework-connectivity-t-targets",
- enabled: false,
+ enabled: true,
}
// The above defaults can be used to disable framework-connectivity t
// targets while minimizing merge conflicts in the build rules.
@@ -114,7 +117,7 @@
// In preparation for future move
"//packages/modules/Connectivity/apex",
"//packages/modules/Connectivity/service-t",
- "//packages/modules/Nearby/service",
+ "//packages/modules/Connectivity/nearby/service",
"//frameworks/base",
// Tests using hidden APIs
@@ -131,10 +134,10 @@
"//frameworks/opt/telephony/tests/telephonytests",
"//packages/modules/CaptivePortalLogin/tests",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
+ "//packages/modules/Connectivity/nearby/tests:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
"//packages/modules/NetworkStack/tests:__subpackages__",
- "//packages/modules/Nearby/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
],
}
diff --git a/framework-t/api/module-lib-current.txt b/framework-t/api/module-lib-current.txt
index c1f7b39..5a8d47b 100644
--- a/framework-t/api/module-lib-current.txt
+++ b/framework-t/api/module-lib-current.txt
@@ -27,6 +27,14 @@
}
+package android.nearby {
+
+ public final class NearbyFrameworkInitializer {
+ method public static void registerServiceWrappers();
+ }
+
+}
+
package android.net {
public final class ConnectivityFrameworkInitializerTiramisu {
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 6460fed..c2d245c 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -8,6 +8,214 @@
}
+package android.nearby {
+
+ public interface BroadcastCallback {
+ method public void onStatusChanged(int);
+ field public static final int STATUS_FAILURE = 1; // 0x1
+ field public static final int STATUS_FAILURE_ALREADY_REGISTERED = 2; // 0x2
+ field public static final int STATUS_FAILURE_MISSING_PERMISSIONS = 4; // 0x4
+ field public static final int STATUS_FAILURE_SIZE_EXCEED_LIMIT = 3; // 0x3
+ field public static final int STATUS_OK = 0; // 0x0
+ }
+
+ public abstract class BroadcastRequest {
+ method @NonNull public java.util.List<java.lang.Integer> getMediums();
+ method @IntRange(from=0xffffff81, to=126) public int getTxPower();
+ method public int getType();
+ method public int getVersion();
+ field public static final int BROADCAST_TYPE_NEARBY_PRESENCE = 3; // 0x3
+ field public static final int BROADCAST_TYPE_UNKNOWN = -1; // 0xffffffff
+ field public static final int MEDIUM_BLE = 1; // 0x1
+ field public static final int PRESENCE_VERSION_UNKNOWN = -1; // 0xffffffff
+ field public static final int PRESENCE_VERSION_V0 = 0; // 0x0
+ field public static final int PRESENCE_VERSION_V1 = 1; // 0x1
+ field public static final int UNKNOWN_TX_POWER = -127; // 0xffffff81
+ }
+
+ public final class CredentialElement implements android.os.Parcelable {
+ ctor public CredentialElement(@NonNull String, @NonNull byte[]);
+ method public int describeContents();
+ method @NonNull public String getKey();
+ method @NonNull public byte[] getValue();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.nearby.CredentialElement> CREATOR;
+ }
+
+ public final class DataElement implements android.os.Parcelable {
+ ctor public DataElement(int, @NonNull byte[]);
+ method public int describeContents();
+ method public int getKey();
+ method @NonNull public byte[] getValue();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.nearby.DataElement> CREATOR;
+ }
+
+ public abstract class NearbyDevice {
+ method @NonNull public java.util.List<java.lang.Integer> getMediums();
+ method @Nullable public String getName();
+ method @IntRange(from=0xffffff81, to=126) public int getRssi();
+ method public static boolean isValidMedium(int);
+ }
+
+ public class NearbyManager {
+ method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void startBroadcast(@NonNull android.nearby.BroadcastRequest, @NonNull java.util.concurrent.Executor, @NonNull android.nearby.BroadcastCallback);
+ method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int startScan(@NonNull android.nearby.ScanRequest, @NonNull java.util.concurrent.Executor, @NonNull android.nearby.ScanCallback);
+ method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void stopBroadcast(@NonNull android.nearby.BroadcastCallback);
+ method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void stopScan(@NonNull android.nearby.ScanCallback);
+ }
+
+ public final class PresenceBroadcastRequest extends android.nearby.BroadcastRequest implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public java.util.List<java.lang.Integer> getActions();
+ method @NonNull public android.nearby.PrivateCredential getCredential();
+ method @NonNull public java.util.List<android.nearby.DataElement> getExtendedProperties();
+ method @NonNull public byte[] getSalt();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.nearby.PresenceBroadcastRequest> CREATOR;
+ }
+
+ public static final class PresenceBroadcastRequest.Builder {
+ ctor public PresenceBroadcastRequest.Builder(@NonNull java.util.List<java.lang.Integer>, @NonNull byte[], @NonNull android.nearby.PrivateCredential);
+ method @NonNull public android.nearby.PresenceBroadcastRequest.Builder addAction(@IntRange(from=1, to=255) int);
+ method @NonNull public android.nearby.PresenceBroadcastRequest.Builder addExtendedProperty(@NonNull android.nearby.DataElement);
+ method @NonNull public android.nearby.PresenceBroadcastRequest build();
+ method @NonNull public android.nearby.PresenceBroadcastRequest.Builder setTxPower(@IntRange(from=0xffffff81, to=126) int);
+ method @NonNull public android.nearby.PresenceBroadcastRequest.Builder setVersion(int);
+ }
+
+ public abstract class PresenceCredential {
+ method @NonNull public byte[] getAuthenticityKey();
+ method @NonNull public java.util.List<android.nearby.CredentialElement> getCredentialElements();
+ method public int getIdentityType();
+ method @NonNull public byte[] getSecretId();
+ method public int getType();
+ field public static final int CREDENTIAL_TYPE_PRIVATE = 0; // 0x0
+ field public static final int CREDENTIAL_TYPE_PUBLIC = 1; // 0x1
+ field public static final int IDENTITY_TYPE_PRIVATE = 1; // 0x1
+ field public static final int IDENTITY_TYPE_PROVISIONED = 2; // 0x2
+ field public static final int IDENTITY_TYPE_TRUSTED = 3; // 0x3
+ field public static final int IDENTITY_TYPE_UNKNOWN = 0; // 0x0
+ }
+
+ public final class PresenceDevice extends android.nearby.NearbyDevice implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public String getDeviceId();
+ method @Nullable public String getDeviceImageUrl();
+ method public int getDeviceType();
+ method public long getDiscoveryTimestampMillis();
+ method @NonNull public byte[] getEncryptedIdentity();
+ method @NonNull public java.util.List<android.nearby.DataElement> getExtendedProperties();
+ method @NonNull public byte[] getSalt();
+ method @NonNull public byte[] getSecretId();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.nearby.PresenceDevice> CREATOR;
+ }
+
+ public static final class PresenceDevice.Builder {
+ ctor public PresenceDevice.Builder(@NonNull String, @NonNull byte[], @NonNull byte[], @NonNull byte[]);
+ method @NonNull public android.nearby.PresenceDevice.Builder addExtendedProperty(@NonNull android.nearby.DataElement);
+ method @NonNull public android.nearby.PresenceDevice.Builder addMedium(int);
+ method @NonNull public android.nearby.PresenceDevice build();
+ method @NonNull public android.nearby.PresenceDevice.Builder setDeviceImageUrl(@Nullable String);
+ method @NonNull public android.nearby.PresenceDevice.Builder setDeviceType(int);
+ method @NonNull public android.nearby.PresenceDevice.Builder setDiscoveryTimestampMillis(long);
+ method @NonNull public android.nearby.PresenceDevice.Builder setName(@Nullable String);
+ method @NonNull public android.nearby.PresenceDevice.Builder setRssi(int);
+ }
+
+ public final class PresenceScanFilter extends android.nearby.ScanFilter implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public java.util.List<android.nearby.PublicCredential> getCredentials();
+ method @NonNull public java.util.List<android.nearby.DataElement> getExtendedProperties();
+ method @NonNull public java.util.List<java.lang.Integer> getPresenceActions();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.nearby.PresenceScanFilter> CREATOR;
+ }
+
+ public static final class PresenceScanFilter.Builder {
+ ctor public PresenceScanFilter.Builder();
+ method @NonNull public android.nearby.PresenceScanFilter.Builder addCredential(@NonNull android.nearby.PublicCredential);
+ method @NonNull public android.nearby.PresenceScanFilter.Builder addExtendedProperty(@NonNull android.nearby.DataElement);
+ method @NonNull public android.nearby.PresenceScanFilter.Builder addPresenceAction(@IntRange(from=1, to=255) int);
+ method @NonNull public android.nearby.PresenceScanFilter build();
+ method @NonNull public android.nearby.PresenceScanFilter.Builder setMaxPathLoss(@IntRange(from=0, to=127) int);
+ }
+
+ public final class PrivateCredential extends android.nearby.PresenceCredential implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public String getDeviceName();
+ method @NonNull public byte[] getMetadataEncryptionKey();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.nearby.PrivateCredential> CREATOR;
+ }
+
+ public static final class PrivateCredential.Builder {
+ ctor public PrivateCredential.Builder(@NonNull byte[], @NonNull byte[], @NonNull byte[], @NonNull String);
+ method @NonNull public android.nearby.PrivateCredential.Builder addCredentialElement(@NonNull android.nearby.CredentialElement);
+ method @NonNull public android.nearby.PrivateCredential build();
+ method @NonNull public android.nearby.PrivateCredential.Builder setIdentityType(int);
+ }
+
+ public final class PublicCredential extends android.nearby.PresenceCredential implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public byte[] getEncryptedMetadata();
+ method @NonNull public byte[] getEncryptedMetadataKeyTag();
+ method @NonNull public byte[] getPublicKey();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.nearby.PublicCredential> CREATOR;
+ }
+
+ public static final class PublicCredential.Builder {
+ ctor public PublicCredential.Builder(@NonNull byte[], @NonNull byte[], @NonNull byte[], @NonNull byte[], @NonNull byte[]);
+ method @NonNull public android.nearby.PublicCredential.Builder addCredentialElement(@NonNull android.nearby.CredentialElement);
+ method @NonNull public android.nearby.PublicCredential build();
+ method @NonNull public android.nearby.PublicCredential.Builder setIdentityType(int);
+ }
+
+ public interface ScanCallback {
+ method public void onDiscovered(@NonNull android.nearby.NearbyDevice);
+ method public void onLost(@NonNull android.nearby.NearbyDevice);
+ method public void onUpdated(@NonNull android.nearby.NearbyDevice);
+ }
+
+ public abstract class ScanFilter {
+ method @IntRange(from=0, to=127) public int getMaxPathLoss();
+ method public int getType();
+ }
+
+ public final class ScanRequest implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public java.util.List<android.nearby.ScanFilter> getScanFilters();
+ method public int getScanMode();
+ method public int getScanType();
+ method @NonNull public android.os.WorkSource getWorkSource();
+ method public boolean isBleEnabled();
+ method public static boolean isValidScanMode(int);
+ method public static boolean isValidScanType(int);
+ method @NonNull public static String scanModeToString(int);
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.nearby.ScanRequest> CREATOR;
+ field public static final int SCAN_MODE_BALANCED = 1; // 0x1
+ field public static final int SCAN_MODE_LOW_LATENCY = 2; // 0x2
+ field public static final int SCAN_MODE_LOW_POWER = 0; // 0x0
+ field public static final int SCAN_MODE_NO_POWER = -1; // 0xffffffff
+ field public static final int SCAN_TYPE_FAST_PAIR = 1; // 0x1
+ field public static final int SCAN_TYPE_NEARBY_PRESENCE = 2; // 0x2
+ }
+
+ public static final class ScanRequest.Builder {
+ ctor public ScanRequest.Builder();
+ method @NonNull public android.nearby.ScanRequest.Builder addScanFilter(@NonNull android.nearby.ScanFilter);
+ method @NonNull public android.nearby.ScanRequest build();
+ method @NonNull public android.nearby.ScanRequest.Builder setBleEnabled(boolean);
+ method @NonNull public android.nearby.ScanRequest.Builder setScanMode(int);
+ method @NonNull public android.nearby.ScanRequest.Builder setScanType(int);
+ method @NonNull @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public android.nearby.ScanRequest.Builder setWorkSource(@Nullable android.os.WorkSource);
+ }
+
+}
+
package android.net {
public class EthernetManager {
diff --git a/framework-t/src/android/net/DataUsageRequest.java b/framework-t/src/android/net/DataUsageRequest.java
index b06d515..f0ff465 100644
--- a/framework-t/src/android/net/DataUsageRequest.java
+++ b/framework-t/src/android/net/DataUsageRequest.java
@@ -75,7 +75,7 @@
@Override
public DataUsageRequest createFromParcel(Parcel in) {
int requestId = in.readInt();
- NetworkTemplate template = in.readParcelable(null);
+ NetworkTemplate template = in.readParcelable(null, android.net.NetworkTemplate.class);
long thresholdInBytes = in.readLong();
DataUsageRequest result = new DataUsageRequest(requestId, template,
thresholdInBytes);
diff --git a/framework-t/src/android/net/IpSecConfig.java b/framework-t/src/android/net/IpSecConfig.java
index 575c5ed..03bb187 100644
--- a/framework-t/src/android/net/IpSecConfig.java
+++ b/framework-t/src/android/net/IpSecConfig.java
@@ -267,14 +267,14 @@
mMode = in.readInt();
mSourceAddress = in.readString();
mDestinationAddress = in.readString();
- mNetwork = (Network) in.readParcelable(Network.class.getClassLoader());
+ mNetwork = (Network) in.readParcelable(Network.class.getClassLoader(), android.net.Network.class);
mSpiResourceId = in.readInt();
mEncryption =
- (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
+ (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader(), android.net.IpSecAlgorithm.class);
mAuthentication =
- (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
+ (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader(), android.net.IpSecAlgorithm.class);
mAuthenticatedEncryption =
- (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
+ (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader(), android.net.IpSecAlgorithm.class);
mEncapType = in.readInt();
mEncapSocketResourceId = in.readInt();
mEncapRemotePort = in.readInt();
diff --git a/framework-t/src/android/net/IpSecUdpEncapResponse.java b/framework-t/src/android/net/IpSecUdpEncapResponse.java
index 732cf19..390af82 100644
--- a/framework-t/src/android/net/IpSecUdpEncapResponse.java
+++ b/framework-t/src/android/net/IpSecUdpEncapResponse.java
@@ -81,7 +81,7 @@
status = in.readInt();
resourceId = in.readInt();
port = in.readInt();
- fileDescriptor = in.readParcelable(ParcelFileDescriptor.class.getClassLoader());
+ fileDescriptor = in.readParcelable(ParcelFileDescriptor.class.getClassLoader(), android.os.ParcelFileDescriptor.class);
}
@android.annotation.NonNull
diff --git a/framework-t/src/android/net/NetworkStateSnapshot.java b/framework-t/src/android/net/NetworkStateSnapshot.java
index d3f785a..c018e91 100644
--- a/framework-t/src/android/net/NetworkStateSnapshot.java
+++ b/framework-t/src/android/net/NetworkStateSnapshot.java
@@ -75,9 +75,9 @@
/** @hide */
public NetworkStateSnapshot(@NonNull Parcel in) {
- mNetwork = in.readParcelable(null);
- mNetworkCapabilities = in.readParcelable(null);
- mLinkProperties = in.readParcelable(null);
+ mNetwork = in.readParcelable(null, android.net.Network.class);
+ mNetworkCapabilities = in.readParcelable(null, android.net.NetworkCapabilities.class);
+ mLinkProperties = in.readParcelable(null, android.net.LinkProperties.class);
mSubscriberId = in.readString();
mLegacyType = in.readInt();
}
diff --git a/framework-t/src/android/net/UnderlyingNetworkInfo.java b/framework-t/src/android/net/UnderlyingNetworkInfo.java
index 33f9375..7ab53b1 100644
--- a/framework-t/src/android/net/UnderlyingNetworkInfo.java
+++ b/framework-t/src/android/net/UnderlyingNetworkInfo.java
@@ -60,7 +60,7 @@
mOwnerUid = in.readInt();
mIface = in.readString();
List<String> underlyingIfaces = new ArrayList<>();
- in.readList(underlyingIfaces, null /*classLoader*/);
+ in.readList(underlyingIfaces, null /*classLoader*/, java.lang.String.class);
mUnderlyingIfaces = Collections.unmodifiableList(underlyingIfaces);
}
diff --git a/framework/aidl-export/android/net/NetworkStats.aidl b/framework/aidl-export/android/net/NetworkStats.aidl
new file mode 100644
index 0000000..d06ca65
--- /dev/null
+++ b/framework/aidl-export/android/net/NetworkStats.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2011, 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.net;
+
+parcelable NetworkStats;
diff --git a/framework/aidl-export/android/net/NetworkTemplate.aidl b/framework/aidl-export/android/net/NetworkTemplate.aidl
new file mode 100644
index 0000000..3d37488
--- /dev/null
+++ b/framework/aidl-export/android/net/NetworkTemplate.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2011, 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.net;
+
+parcelable NetworkTemplate;
diff --git a/framework/aidl-export/android/net/nsd/NsdServiceInfo.aidl b/framework/aidl-export/android/net/nsd/NsdServiceInfo.aidl
new file mode 100644
index 0000000..657bdd1
--- /dev/null
+++ b/framework/aidl-export/android/net/nsd/NsdServiceInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd;
+
+@JavaOnlyStableParcelable parcelable NsdServiceInfo;
\ No newline at end of file
diff --git a/nearby/.gitignore b/nearby/.gitignore
new file mode 100644
index 0000000..4402b3d
--- /dev/null
+++ b/nearby/.gitignore
@@ -0,0 +1,8 @@
+# Eclipse project
+**/.classpath
+**/.project
+
+# IntelliJ project
+**/.idea
+**/*.iml
+**/*.ipr
\ No newline at end of file
diff --git a/nearby/Android.bp b/nearby/Android.bp
deleted file mode 100644
index fb4e3cd..0000000
--- a/nearby/Android.bp
+++ /dev/null
@@ -1,39 +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 {
- // See: http://go/android-license-faq
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-// Empty sources and libraries to avoid merge conflicts with downstream
-// branches
-// TODO: remove once the Nearby sources are available in this branch
-filegroup {
- name: "framework-nearby-java-sources",
- srcs: [],
- visibility: ["//packages/modules/Connectivity:__subpackages__"],
-}
-
-
-java_library {
- name: "service-nearby-pre-jarjar",
- srcs: ["service-src/**/*.java"],
- sdk_version: "module_current",
- min_sdk_version: "30",
- apex_available: ["com.android.tethering"],
- visibility: ["//packages/modules/Connectivity:__subpackages__"],
-}
diff --git a/nearby/PREUPLOAD.cfg b/nearby/PREUPLOAD.cfg
new file mode 100644
index 0000000..048ddb6
--- /dev/null
+++ b/nearby/PREUPLOAD.cfg
@@ -0,0 +1,10 @@
+[Builtin Hooks]
+xmllint = true
+clang_format = true
+commit_msg_changeid_field = true
+
+[Builtin Hooks Options]
+clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp
+
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
\ No newline at end of file
diff --git a/nearby/README.md b/nearby/README.md
new file mode 100644
index 0000000..6925dc4
--- /dev/null
+++ b/nearby/README.md
@@ -0,0 +1,42 @@
+# Nearby Mainline Module
+This directory contains code for the AOSP Nearby mainline module.
+
+##Directory Structure
+
+`apex`
+ - Files associated with the Nearby mainline module APEX.
+
+`framework`
+ - Contains client side APIs and AIDL files.
+
+`jni`
+ - JNI wrapper for invoking Android APIs from native code.
+
+`native`
+ - Native code implementation for nearby module services.
+
+`service`
+ - Server side implementation for nearby module services.
+
+`tests`
+ - Unit/Multi devices tests for Nearby module (both Java and native code).
+
+## IDE setup
+
+```sh
+$ source build/envsetup.sh && lunch <TARGET>
+$ cd packages/modules/Nearby
+$ aidegen .
+# This will launch Intellij project for Nearby module.
+```
+
+## Build and Install
+
+```sh
+$ source build/envsetup.sh && lunch <TARGET>
+$ m com.google.android.tethering.next deapexer
+$ $ANDROID_BUILD_TOP/out/host/linux-x86/bin/deapexer decompress --input \
+ ${ANDROID_PRODUCT_OUT}/system/apex/com.google.android.tethering.next.capex \
+ --output /tmp/tethering.apex
+$ adb install -r /tmp/tethering.apex
+```
diff --git a/nearby/TEST_MAPPING b/nearby/TEST_MAPPING
new file mode 100644
index 0000000..dbaca33
--- /dev/null
+++ b/nearby/TEST_MAPPING
@@ -0,0 +1,24 @@
+{
+ "presubmit": [
+ {
+ "name": "NearbyUnitTests"
+ },
+ {
+ "name": "NearbyIntegrationPrivilegedTests"
+ },
+ {
+ "name": "NearbyIntegrationUntrustedTests"
+ }
+ ],
+ "postsubmit": [
+ {
+ "name": "NearbyUnitTests"
+ }
+ ]
+ // TODO(b/193602229): uncomment once it's supported.
+ //"mainline-presubmit": [
+ // {
+ // "name": "NearbyUnitTests[com.google.android.nearby.apex]"
+ // }
+ //]
+}
diff --git a/nearby/apex/Android.bp b/nearby/apex/Android.bp
new file mode 100644
index 0000000..d7f063a
--- /dev/null
+++ b/nearby/apex/Android.bp
@@ -0,0 +1,21 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+ name: "nearby-jarjar-rules",
+ srcs: ["jarjar-rules.txt"],
+}
diff --git a/nearby/apex/jarjar-rules.txt b/nearby/apex/jarjar-rules.txt
new file mode 100644
index 0000000..826f54f
--- /dev/null
+++ b/nearby/apex/jarjar-rules.txt
@@ -0,0 +1 @@
+rule com.android.internal.** com.android.nearby.jarjar.@0
diff --git a/nearby/apex/manifest.json b/nearby/apex/manifest.json
new file mode 100644
index 0000000..b91d259
--- /dev/null
+++ b/nearby/apex/manifest.json
@@ -0,0 +1,4 @@
+{
+ "name": "com.android.nearby",
+ "version": 1
+}
diff --git a/nearby/framework/Android.bp b/nearby/framework/Android.bp
new file mode 100644
index 0000000..e223b54
--- /dev/null
+++ b/nearby/framework/Android.bp
@@ -0,0 +1,55 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Sources included in the framework-connectivity-t jar
+// TODO: consider moving files to packages/modules/Connectivity
+filegroup {
+ name: "framework-nearby-java-sources",
+ srcs: [
+ "java/**/*.java",
+ "java/**/*.aidl",
+ ],
+ path: "java",
+ visibility: [
+ "//packages/modules/Connectivity/framework-t:__subpackages__",
+ ],
+}
+
+filegroup {
+ name: "framework-nearby-sources",
+ srcs: [
+ ":framework-nearby-java-sources",
+ ],
+ visibility: ["//frameworks/base"],
+}
+
+// Build of only framework-nearby (not as part of connectivity) for
+// unit tests
+java_library {
+ name: "framework-nearby-static",
+ srcs: [":framework-nearby-java-sources"],
+ sdk_version: "module_current",
+ libs: [
+ "framework-annotations-lib",
+ "framework-bluetooth",
+ ],
+ static_libs: [
+ "modules-utils-preconditions",
+ ],
+ visibility: ["//packages/modules/Connectivity/nearby/tests:__subpackages__"],
+}
diff --git a/nearby/framework/java/android/nearby/BroadcastCallback.java b/nearby/framework/java/android/nearby/BroadcastCallback.java
new file mode 100644
index 0000000..cc94308
--- /dev/null
+++ b/nearby/framework/java/android/nearby/BroadcastCallback.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Callback when broadcasting request using nearby specification.
+ *
+ * @hide
+ */
+@SystemApi
+public interface BroadcastCallback {
+ /** Broadcast was successful. */
+ int STATUS_OK = 0;
+
+ /** General status code when broadcast failed. */
+ int STATUS_FAILURE = 1;
+
+ /**
+ * Broadcast failed as the callback was already registered.
+ */
+ int STATUS_FAILURE_ALREADY_REGISTERED = 2;
+
+ /**
+ * Broadcast failed as the request contains excessive data.
+ */
+ int STATUS_FAILURE_SIZE_EXCEED_LIMIT = 3;
+
+ /**
+ * Broadcast failed as the client doesn't hold required permissions.
+ */
+ int STATUS_FAILURE_MISSING_PERMISSIONS = 4;
+
+ /** @hide **/
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({STATUS_OK, STATUS_FAILURE, STATUS_FAILURE_ALREADY_REGISTERED,
+ STATUS_FAILURE_SIZE_EXCEED_LIMIT, STATUS_FAILURE_MISSING_PERMISSIONS})
+ @interface BroadcastStatus {
+ }
+
+ /**
+ * Called when broadcast status changes.
+ */
+ void onStatusChanged(@BroadcastStatus int status);
+}
diff --git a/nearby/framework/java/android/nearby/BroadcastRequest.java b/nearby/framework/java/android/nearby/BroadcastRequest.java
new file mode 100644
index 0000000..90f4d0f
--- /dev/null
+++ b/nearby/framework/java/android/nearby/BroadcastRequest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a {@link BroadcastRequest}.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class BroadcastRequest {
+
+ /** An unknown nearby broadcast request type. */
+ public static final int BROADCAST_TYPE_UNKNOWN = -1;
+
+ /** Broadcast type for advertising using nearby presence protocol. */
+ public static final int BROADCAST_TYPE_NEARBY_PRESENCE = 3;
+
+ /** @hide **/
+ // Currently, only Nearby Presence broadcast is supported, in the future
+ // broadcasting using other nearby specifications will be added.
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({BROADCAST_TYPE_UNKNOWN, BROADCAST_TYPE_NEARBY_PRESENCE})
+ public @interface BroadcastType {
+ }
+
+ /**
+ * Tx Power when the value is not set in the broadcast.
+ */
+ public static final int UNKNOWN_TX_POWER = -127;
+
+ /**
+ * An unknown version of presence broadcast request.
+ */
+ public static final int PRESENCE_VERSION_UNKNOWN = -1;
+
+ /**
+ * A legacy presence version that is only suitable for legacy (31 bytes) BLE advertisements.
+ * This exists to support legacy presence version, and not recommended for use.
+ */
+ public static final int PRESENCE_VERSION_V0 = 0;
+
+ /**
+ * V1 of Nearby Presence Protocol. This version supports both legacy (31 bytes) BLE
+ * advertisements, and extended BLE advertisements.
+ */
+ public static final int PRESENCE_VERSION_V1 = 1;
+
+ /** @hide **/
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({PRESENCE_VERSION_UNKNOWN, PRESENCE_VERSION_V0, PRESENCE_VERSION_V1})
+ public @interface BroadcastVersion {
+ }
+
+ /**
+ * Broadcast the request using the Bluetooth Low Energy (BLE) medium.
+ */
+ public static final int MEDIUM_BLE = 1;
+
+ /**
+ * The medium where the broadcast request should be sent.
+ *
+ * @hide
+ */
+ @IntDef({MEDIUM_BLE})
+ public @interface Medium {}
+
+ /**
+ * Creates a {@link BroadcastRequest} from parcel.
+ *
+ * @hide
+ */
+ @NonNull
+ public static BroadcastRequest createFromParcel(Parcel in) {
+ int type = in.readInt();
+ switch (type) {
+ case BroadcastRequest.BROADCAST_TYPE_NEARBY_PRESENCE:
+ return PresenceBroadcastRequest.createFromParcelBody(in);
+ default:
+ throw new IllegalStateException(
+ "Unexpected broadcast type (value " + type + ") in parcel.");
+ }
+ }
+
+ private final @BroadcastType int mType;
+ private final @BroadcastVersion int mVersion;
+ private final int mTxPower;
+ private final @Medium List<Integer> mMediums;
+
+ BroadcastRequest(@BroadcastType int type, @BroadcastVersion int version, int txPower,
+ @Medium List<Integer> mediums) {
+ this.mType = type;
+ this.mVersion = version;
+ this.mTxPower = txPower;
+ this.mMediums = mediums;
+ }
+
+ BroadcastRequest(@BroadcastType int type, Parcel in) {
+ mType = type;
+ mVersion = in.readInt();
+ mTxPower = in.readInt();
+ mMediums = new ArrayList<>();
+ in.readList(mMediums, Integer.class.getClassLoader(), Integer.class);
+ }
+
+ /**
+ * Returns the type of the broadcast.
+ */
+ public @BroadcastType int getType() {
+ return mType;
+ }
+
+ /**
+ * Returns the version of the broadcast.
+ */
+ public @BroadcastVersion int getVersion() {
+ return mVersion;
+ }
+
+ /**
+ * Returns the calibrated TX power when this request is broadcast.
+ */
+ @IntRange(from = -127, to = 126)
+ public int getTxPower() {
+ return mTxPower;
+ }
+
+ /**
+ * Returns the list of broadcast mediums. A medium represents the channel on which the broadcast
+ * request is sent.
+ */
+ @NonNull
+ @Medium
+ public List<Integer> getMediums() {
+ return mMediums;
+ }
+
+ /**
+ * Writes the BroadcastRequest to the parcel.
+ *
+ * @hide
+ */
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mType);
+ dest.writeInt(mVersion);
+ dest.writeInt(mTxPower);
+ dest.writeList(mMediums);
+ }
+}
diff --git a/nearby/framework/java/android/nearby/BroadcastRequestParcelable.aidl b/nearby/framework/java/android/nearby/BroadcastRequestParcelable.aidl
new file mode 100644
index 0000000..818f8d5
--- /dev/null
+++ b/nearby/framework/java/android/nearby/BroadcastRequestParcelable.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+parcelable BroadcastRequestParcelable;
diff --git a/nearby/framework/java/android/nearby/BroadcastRequestParcelable.java b/nearby/framework/java/android/nearby/BroadcastRequestParcelable.java
new file mode 100644
index 0000000..4a2ff6d
--- /dev/null
+++ b/nearby/framework/java/android/nearby/BroadcastRequestParcelable.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A wrapper of {@link BroadcastRequest} that is parcelable.
+ *
+ * @hide
+ */
+public class BroadcastRequestParcelable implements Parcelable {
+ private final BroadcastRequest mBroadcastRequest;
+
+ public static final Creator<BroadcastRequestParcelable> CREATOR =
+ new Creator<BroadcastRequestParcelable>() {
+ @Override
+ public BroadcastRequestParcelable createFromParcel(Parcel in) {
+ return new BroadcastRequestParcelable(BroadcastRequest.createFromParcel(in));
+ }
+
+ @Override
+ public BroadcastRequestParcelable[] newArray(int size) {
+ return new BroadcastRequestParcelable[size];
+ }
+ };
+
+ BroadcastRequestParcelable(BroadcastRequest broadcastRequest) {
+ mBroadcastRequest = broadcastRequest;
+ }
+
+ /**
+ * Returns the broadcastRequest.
+ */
+ public BroadcastRequest getBroadcastRequest() {
+ return mBroadcastRequest;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ mBroadcastRequest.writeToParcel(dest, flags);
+ }
+}
diff --git a/nearby/framework/java/android/nearby/CredentialElement.java b/nearby/framework/java/android/nearby/CredentialElement.java
new file mode 100644
index 0000000..7a43b01
--- /dev/null
+++ b/nearby/framework/java/android/nearby/CredentialElement.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Represents an element in {@link PresenceCredential}.
+ *
+ * @hide
+ */
+@SystemApi
+public final class CredentialElement implements Parcelable {
+ private final String mKey;
+ private final byte[] mValue;
+
+ /** Constructs a {@link CredentialElement}. */
+ public CredentialElement(@NonNull String key, @NonNull byte[] value) {
+ Preconditions.checkState(key != null && value != null, "neither key or value can be null");
+ mKey = key;
+ mValue = value;
+ }
+
+ @NonNull
+ public static final Parcelable.Creator<CredentialElement> CREATOR =
+ new Parcelable.Creator<CredentialElement>() {
+ @Override
+ public CredentialElement createFromParcel(Parcel in) {
+ String key = in.readString();
+ byte[] value = new byte[in.readInt()];
+ in.readByteArray(value);
+ return new CredentialElement(key, value);
+ }
+
+ @Override
+ public CredentialElement[] newArray(int size) {
+ return new CredentialElement[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString(mKey);
+ dest.writeInt(mValue.length);
+ dest.writeByteArray(mValue);
+ }
+
+ /** Returns the key of the credential element. */
+ @NonNull
+ public String getKey() {
+ return mKey;
+ }
+
+ /** Returns the value of the credential element. */
+ @NonNull
+ public byte[] getValue() {
+ return mValue;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj instanceof CredentialElement) {
+ CredentialElement that = (CredentialElement) obj;
+ return mKey.equals(that.mKey) && Arrays.equals(mValue, that.mValue);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mKey.hashCode(), Arrays.hashCode(mValue));
+ }
+}
diff --git a/nearby/framework/java/android/nearby/DataElement.java b/nearby/framework/java/android/nearby/DataElement.java
new file mode 100644
index 0000000..6fa5fb5
--- /dev/null
+++ b/nearby/framework/java/android/nearby/DataElement.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+
+/**
+ * Represents a data element in Nearby Presence.
+ *
+ * @hide
+ */
+@SystemApi
+public final class DataElement implements Parcelable {
+
+ private final int mKey;
+ private final byte[] mValue;
+
+ /**
+ * Constructs a {@link DataElement}.
+ */
+ public DataElement(int key, @NonNull byte[] value) {
+ Preconditions.checkState(value != null, "value cannot be null");
+ mKey = key;
+ mValue = value;
+ }
+
+ @NonNull
+ public static final Creator<DataElement> CREATOR = new Creator<DataElement>() {
+ @Override
+ public DataElement createFromParcel(Parcel in) {
+ int key = in.readInt();
+ byte[] value = new byte[in.readInt()];
+ in.readByteArray(value);
+ return new DataElement(key, value);
+ }
+
+ @Override
+ public DataElement[] newArray(int size) {
+ return new DataElement[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mKey);
+ dest.writeInt(mValue.length);
+ dest.writeByteArray(mValue);
+ }
+
+ /**
+ * Returns the key of the data element, as defined in the nearby presence specification.
+ */
+ public int getKey() {
+ return mKey;
+ }
+
+ /**
+ * Returns the value of the data element.
+ */
+ @NonNull
+ public byte[] getValue() {
+ return mValue;
+ }
+}
diff --git a/nearby/framework/java/android/nearby/FastPairAccountKeyDeviceMetadata.java b/nearby/framework/java/android/nearby/FastPairAccountKeyDeviceMetadata.java
new file mode 100644
index 0000000..d42fbf4
--- /dev/null
+++ b/nearby/framework/java/android/nearby/FastPairAccountKeyDeviceMetadata.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
+
+/**
+ * Class for metadata of a Fast Pair device associated with an account.
+ *
+ * @hide
+ */
+public class FastPairAccountKeyDeviceMetadata {
+
+ FastPairAccountKeyDeviceMetadataParcel mMetadataParcel;
+
+ FastPairAccountKeyDeviceMetadata(FastPairAccountKeyDeviceMetadataParcel metadataParcel) {
+ this.mMetadataParcel = metadataParcel;
+ }
+
+ /**
+ * Get Device Account Key, which uniquely identifies a Fast Pair device associated with an
+ * account. AccountKey is 16 bytes: first byte is 0x04. Other 15 bytes are randomly generated.
+ *
+ * @return 16-byte Account Key.
+ * @hide
+ */
+ @Nullable
+ public byte[] getDeviceAccountKey() {
+ return mMetadataParcel.deviceAccountKey;
+ }
+
+ /**
+ * Get a hash value of device's account key and public bluetooth address without revealing the
+ * public bluetooth address. Sha256 hash value is 32 bytes.
+ *
+ * @return 32-byte Sha256 hash value.
+ * @hide
+ */
+ @Nullable
+ public byte[] getSha256DeviceAccountKeyPublicAddress() {
+ return mMetadataParcel.sha256DeviceAccountKeyPublicAddress;
+ }
+
+ /**
+ * Get metadata of a Fast Pair device type.
+ *
+ * @hide
+ */
+ @Nullable
+ public FastPairDeviceMetadata getFastPairDeviceMetadata() {
+ if (mMetadataParcel.metadata == null) {
+ return null;
+ }
+ return new FastPairDeviceMetadata(mMetadataParcel.metadata);
+ }
+
+ /**
+ * Get Fast Pair discovery item, which is tied to both the device type and the account.
+ *
+ * @hide
+ */
+ @Nullable
+ public FastPairDiscoveryItem getFastPairDiscoveryItem() {
+ if (mMetadataParcel.discoveryItem == null) {
+ return null;
+ }
+ return new FastPairDiscoveryItem(mMetadataParcel.discoveryItem);
+ }
+
+ /**
+ * Builder used to create FastPairAccountKeyDeviceMetadata.
+ *
+ * @hide
+ */
+ public static final class Builder {
+
+ private final FastPairAccountKeyDeviceMetadataParcel mBuilderParcel;
+
+ /**
+ * Default constructor of Builder.
+ *
+ * @hide
+ */
+ public Builder() {
+ mBuilderParcel = new FastPairAccountKeyDeviceMetadataParcel();
+ mBuilderParcel.deviceAccountKey = null;
+ mBuilderParcel.sha256DeviceAccountKeyPublicAddress = null;
+ mBuilderParcel.metadata = null;
+ mBuilderParcel.discoveryItem = null;
+ }
+
+ /**
+ * Set Account Key.
+ *
+ * @param deviceAccountKey Fast Pair device account key, which is 16 bytes: first byte is
+ * 0x04. Next 15 bytes are randomly generated.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setDeviceAccountKey(@Nullable byte[] deviceAccountKey) {
+ mBuilderParcel.deviceAccountKey = deviceAccountKey;
+ return this;
+ }
+
+ /**
+ * Set sha256 hash value of account key and public bluetooth address.
+ *
+ * @param sha256DeviceAccountKeyPublicAddress 32-byte sha256 hash value of account key and
+ * public bluetooth address.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setSha256DeviceAccountKeyPublicAddress(
+ @Nullable byte[] sha256DeviceAccountKeyPublicAddress) {
+ mBuilderParcel.sha256DeviceAccountKeyPublicAddress =
+ sha256DeviceAccountKeyPublicAddress;
+ return this;
+ }
+
+
+ /**
+ * Set Fast Pair metadata.
+ *
+ * @param metadata Fast Pair metadata.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setFastPairDeviceMetadata(@Nullable FastPairDeviceMetadata metadata) {
+ if (metadata == null) {
+ mBuilderParcel.metadata = null;
+ } else {
+ mBuilderParcel.metadata = metadata.mMetadataParcel;
+ }
+ return this;
+ }
+
+ /**
+ * Set Fast Pair discovery item.
+ *
+ * @param discoveryItem Fast Pair discovery item.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setFastPairDiscoveryItem(@Nullable FastPairDiscoveryItem discoveryItem) {
+ if (discoveryItem == null) {
+ mBuilderParcel.discoveryItem = null;
+ } else {
+ mBuilderParcel.discoveryItem = discoveryItem.mMetadataParcel;
+ }
+ return this;
+ }
+
+ /**
+ * Build {@link FastPairAccountKeyDeviceMetadata} with the currently set configuration.
+ *
+ * @hide
+ */
+ @NonNull
+ public FastPairAccountKeyDeviceMetadata build() {
+ return new FastPairAccountKeyDeviceMetadata(mBuilderParcel);
+ }
+ }
+}
diff --git a/nearby/framework/java/android/nearby/FastPairAntispoofKeyDeviceMetadata.java b/nearby/framework/java/android/nearby/FastPairAntispoofKeyDeviceMetadata.java
new file mode 100644
index 0000000..74831d5
--- /dev/null
+++ b/nearby/framework/java/android/nearby/FastPairAntispoofKeyDeviceMetadata.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.nearby;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
+
+/**
+ * Class for a type of registered Fast Pair device keyed by modelID, or antispoofKey.
+ *
+ * @hide
+ */
+public class FastPairAntispoofKeyDeviceMetadata {
+
+ FastPairAntispoofKeyDeviceMetadataParcel mMetadataParcel;
+ FastPairAntispoofKeyDeviceMetadata(
+ FastPairAntispoofKeyDeviceMetadataParcel metadataParcel) {
+ this.mMetadataParcel = metadataParcel;
+ }
+
+ /**
+ * Get Antispoof public key.
+ *
+ * @hide
+ */
+ @Nullable
+ public byte[] getAntispoofPublicKey() {
+ return this.mMetadataParcel.antispoofPublicKey;
+ }
+
+ /**
+ * Get metadata of a Fast Pair device type.
+ *
+ * @hide
+ */
+ @Nullable
+ public FastPairDeviceMetadata getFastPairDeviceMetadata() {
+ if (this.mMetadataParcel.deviceMetadata == null) {
+ return null;
+ }
+ return new FastPairDeviceMetadata(this.mMetadataParcel.deviceMetadata);
+ }
+
+ /**
+ * Builder used to create FastPairAntispoofkeyDeviceMetadata.
+ *
+ * @hide
+ */
+ public static final class Builder {
+
+ private final FastPairAntispoofKeyDeviceMetadataParcel mBuilderParcel;
+
+ /**
+ * Default constructor of Builder.
+ *
+ * @hide
+ */
+ public Builder() {
+ mBuilderParcel = new FastPairAntispoofKeyDeviceMetadataParcel();
+ mBuilderParcel.antispoofPublicKey = null;
+ mBuilderParcel.deviceMetadata = null;
+ }
+
+ /**
+ * Set AntiSpoof public key, which uniquely identify a Fast Pair device type.
+ *
+ * @param antispoofPublicKey is 64 bytes, see <a href="https://developers.google.com/nearby/fast-pair/spec#data_format">Data Format</a>.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setAntispoofPublicKey(@Nullable byte[] antispoofPublicKey) {
+ mBuilderParcel.antispoofPublicKey = antispoofPublicKey;
+ return this;
+ }
+
+ /**
+ * Set Fast Pair metadata, which is the property of a Fast Pair device type, including
+ * device images and strings.
+ *
+ * @param metadata Fast Pair device meta data.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setFastPairDeviceMetadata(@Nullable FastPairDeviceMetadata metadata) {
+ if (metadata != null) {
+ mBuilderParcel.deviceMetadata = metadata.mMetadataParcel;
+ } else {
+ mBuilderParcel.deviceMetadata = null;
+ }
+ return this;
+ }
+
+ /**
+ * Build {@link FastPairAntispoofKeyDeviceMetadata} with the currently set configuration.
+ *
+ * @hide
+ */
+ @NonNull
+ public FastPairAntispoofKeyDeviceMetadata build() {
+ return new FastPairAntispoofKeyDeviceMetadata(mBuilderParcel);
+ }
+ }
+}
diff --git a/nearby/framework/java/android/nearby/FastPairDataProviderService.java b/nearby/framework/java/android/nearby/FastPairDataProviderService.java
new file mode 100644
index 0000000..f1d5074
--- /dev/null
+++ b/nearby/framework/java/android/nearby/FastPairDataProviderService.java
@@ -0,0 +1,714 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.accounts.Account;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Service;
+import android.content.Intent;
+import android.nearby.aidl.ByteArrayParcel;
+import android.nearby.aidl.FastPairAccountDevicesMetadataRequestParcel;
+import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataRequestParcel;
+import android.nearby.aidl.FastPairEligibleAccountParcel;
+import android.nearby.aidl.FastPairEligibleAccountsRequestParcel;
+import android.nearby.aidl.FastPairManageAccountDeviceRequestParcel;
+import android.nearby.aidl.FastPairManageAccountRequestParcel;
+import android.nearby.aidl.IFastPairAccountDevicesMetadataCallback;
+import android.nearby.aidl.IFastPairAntispoofKeyDeviceMetadataCallback;
+import android.nearby.aidl.IFastPairDataProvider;
+import android.nearby.aidl.IFastPairEligibleAccountsCallback;
+import android.nearby.aidl.IFastPairManageAccountCallback;
+import android.nearby.aidl.IFastPairManageAccountDeviceCallback;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A service class for fast pair data providers outside the system server.
+ *
+ * Fast pair providers should be wrapped in a non-exported service which returns the result of
+ * {@link #getBinder()} from the service's {@link android.app.Service#onBind(Intent)} method. The
+ * service should not be exported so that components other than the system server cannot bind to it.
+ * Alternatively, the service may be guarded by a permission that only system server can obtain.
+ *
+ * <p>Fast Pair providers are identified by their UID / package name.
+ *
+ * @hide
+ */
+public abstract class FastPairDataProviderService extends Service {
+ /**
+ * The action the wrapping service should have in its intent filter to implement the
+ * {@link android.nearby.FastPairDataProviderBase}.
+ *
+ * @hide
+ */
+ public static final String ACTION_FAST_PAIR_DATA_PROVIDER =
+ "android.nearby.action.FAST_PAIR_DATA_PROVIDER";
+
+ /**
+ * Manage request type to add, or opt-in.
+ *
+ * @hide
+ */
+ public static final int MANAGE_REQUEST_ADD = 0;
+
+ /**
+ * Manage request type to remove, or opt-out.
+ *
+ * @hide
+ */
+ public static final int MANAGE_REQUEST_REMOVE = 1;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ MANAGE_REQUEST_ADD,
+ MANAGE_REQUEST_REMOVE})
+ @interface ManageRequestType {}
+
+ /**
+ * Error code for bad request.
+ *
+ * @hide
+ */
+ public static final int ERROR_CODE_BAD_REQUEST = 0;
+
+ /**
+ * Error code for internal error.
+ *
+ * @hide
+ */
+ public static final int ERROR_CODE_INTERNAL_ERROR = 1;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ ERROR_CODE_BAD_REQUEST,
+ ERROR_CODE_INTERNAL_ERROR})
+ @interface ErrorCode {}
+
+ private final IBinder mBinder;
+ private final String mTag;
+
+ /**
+ * Constructor of FastPairDataProviderService.
+ *
+ * @param tag TAG for on device logging.
+ * @hide
+ */
+ public FastPairDataProviderService(@NonNull String tag) {
+ mBinder = new Service();
+ mTag = tag;
+ }
+
+ @Override
+ @NonNull
+ public final IBinder onBind(@NonNull Intent intent) {
+ return mBinder;
+ }
+
+ /**
+ * Callback to be invoked when an AntispoofKeyed device metadata is loaded.
+ *
+ * @hide
+ */
+ public interface FastPairAntispoofKeyDeviceMetadataCallback {
+
+ /**
+ * Invoked once the meta data is loaded.
+ *
+ * @hide
+ */
+ void onFastPairAntispoofKeyDeviceMetadataReceived(
+ @NonNull FastPairAntispoofKeyDeviceMetadata metadata);
+
+ /** Invoked in case of error.
+ *
+ * @hide
+ */
+ void onError(@ErrorCode int code, @Nullable String message);
+ }
+
+ /**
+ * Callback to be invoked when Fast Pair devices of a given account is loaded.
+ *
+ * @hide
+ */
+ public interface FastPairAccountDevicesMetadataCallback {
+
+ /**
+ * Should be invoked once the metadatas are loaded.
+ *
+ * @hide
+ */
+ void onFastPairAccountDevicesMetadataReceived(
+ @NonNull Collection<FastPairAccountKeyDeviceMetadata> metadatas);
+ /**
+ * Invoked in case of error.
+ *
+ * @hide
+ */
+ void onError(@ErrorCode int code, @Nullable String message);
+ }
+
+ /**
+ * Callback to be invoked when FastPair eligible accounts are loaded.
+ *
+ * @hide
+ */
+ public interface FastPairEligibleAccountsCallback {
+
+ /**
+ * Should be invoked once the eligible accounts are loaded.
+ *
+ * @hide
+ */
+ void onFastPairEligibleAccountsReceived(
+ @NonNull Collection<FastPairEligibleAccount> accounts);
+ /**
+ * Invoked in case of error.
+ *
+ * @hide
+ */
+ void onError(@ErrorCode int code, @Nullable String message);
+ }
+
+ /**
+ * Callback to be invoked when a management action is finished.
+ *
+ * @hide
+ */
+ public interface FastPairManageActionCallback {
+
+ /**
+ * Should be invoked once the manage action is successful.
+ *
+ * @hide
+ */
+ void onSuccess();
+ /**
+ * Invoked in case of error.
+ *
+ * @hide
+ */
+ void onError(@ErrorCode int code, @Nullable String message);
+ }
+
+ /**
+ * Fulfills the Fast Pair device metadata request by using callback to send back the
+ * device meta data of a given modelId.
+ *
+ * @hide
+ */
+ public abstract void onLoadFastPairAntispoofKeyDeviceMetadata(
+ @NonNull FastPairAntispoofKeyDeviceMetadataRequest request,
+ @NonNull FastPairAntispoofKeyDeviceMetadataCallback callback);
+
+ /**
+ * Fulfills the account tied Fast Pair devices metadata request by using callback to send back
+ * all Fast Pair device's metadata of a given account.
+ *
+ * @hide
+ */
+ public abstract void onLoadFastPairAccountDevicesMetadata(
+ @NonNull FastPairAccountDevicesMetadataRequest request,
+ @NonNull FastPairAccountDevicesMetadataCallback callback);
+
+ /**
+ * Fulfills the Fast Pair eligible accounts request by using callback to send back Fast Pair
+ * eligible accounts.
+ *
+ * @hide
+ */
+ public abstract void onLoadFastPairEligibleAccounts(
+ @NonNull FastPairEligibleAccountsRequest request,
+ @NonNull FastPairEligibleAccountsCallback callback);
+
+ /**
+ * Fulfills the Fast Pair account management request by using callback to send back result.
+ *
+ * @hide
+ */
+ public abstract void onManageFastPairAccount(
+ @NonNull FastPairManageAccountRequest request,
+ @NonNull FastPairManageActionCallback callback);
+
+ /**
+ * Fulfills the request to manage device-account mapping by using callback to send back result.
+ *
+ * @hide
+ */
+ public abstract void onManageFastPairAccountDevice(
+ @NonNull FastPairManageAccountDeviceRequest request,
+ @NonNull FastPairManageActionCallback callback);
+
+ /**
+ * Class for reading FastPairAntispoofKeyDeviceMetadataRequest, which specifies the model ID of
+ * a Fast Pair device. To fulfill this request, corresponding
+ * {@link FastPairAntispoofKeyDeviceMetadata} should be fetched and returned.
+ *
+ * @hide
+ */
+ public static class FastPairAntispoofKeyDeviceMetadataRequest {
+
+ private final FastPairAntispoofKeyDeviceMetadataRequestParcel mMetadataRequestParcel;
+
+ private FastPairAntispoofKeyDeviceMetadataRequest(
+ final FastPairAntispoofKeyDeviceMetadataRequestParcel metaDataRequestParcel) {
+ this.mMetadataRequestParcel = metaDataRequestParcel;
+ }
+
+ /**
+ * Get modelId (24 bit), the key for FastPairAntispoofKeyDeviceMetadata in the same format
+ * returned by Google at device registration time.
+ *
+ * ModelId format is defined at device registration time, see
+ * <a href="https://developers.google.com/nearby/fast-pair/spec#model_id">Model ID</a>.
+ * @return raw bytes of modelId in the same format returned by Google at device registration
+ * time.
+ * @hide
+ */
+ public @NonNull byte[] getModelId() {
+ return this.mMetadataRequestParcel.modelId;
+ }
+ }
+
+ /**
+ * Class for reading FastPairAccountDevicesMetadataRequest, which specifies the Fast Pair
+ * account and the allow list of the FastPair device keys saved to the account (i.e., FastPair
+ * accountKeys).
+ *
+ * A Fast Pair accountKey is created when a Fast Pair device is saved to an account. It is per
+ * Fast Pair device per account.
+ *
+ * To retrieve all Fast Pair accountKeys saved to an account, the caller needs to set
+ * account with an empty allow list.
+ *
+ * To retrieve metadata of a selected list of Fast Pair devices saved to an account, the caller
+ * needs to set account with a non-empty allow list.
+ * @hide
+ */
+ public static class FastPairAccountDevicesMetadataRequest {
+
+ private final FastPairAccountDevicesMetadataRequestParcel mMetadataRequestParcel;
+
+ private FastPairAccountDevicesMetadataRequest(
+ final FastPairAccountDevicesMetadataRequestParcel metaDataRequestParcel) {
+ this.mMetadataRequestParcel = metaDataRequestParcel;
+ }
+
+ /**
+ * Get FastPair account, whose Fast Pair devices' metadata is requested.
+ *
+ * @return a FastPair account.
+ * @hide
+ */
+ public @NonNull Account getAccount() {
+ return this.mMetadataRequestParcel.account;
+ }
+
+ /**
+ * Get allowlist of Fast Pair devices using a collection of deviceAccountKeys.
+ * Note that as a special case, empty list actually means all FastPair devices under the
+ * account instead of none.
+ *
+ * DeviceAccountKey is 16 bytes: first byte is 0x04. Other 15 bytes are randomly generated.
+ *
+ * @return allowlist of Fast Pair devices using a collection of deviceAccountKeys.
+ * @hide
+ */
+ public @NonNull Collection<byte[]> getDeviceAccountKeys() {
+ if (this.mMetadataRequestParcel.deviceAccountKeys == null) {
+ return new ArrayList<byte[]>(0);
+ }
+ List<byte[]> deviceAccountKeys =
+ new ArrayList<>(this.mMetadataRequestParcel.deviceAccountKeys.length);
+ for (ByteArrayParcel deviceAccountKey : this.mMetadataRequestParcel.deviceAccountKeys) {
+ deviceAccountKeys.add(deviceAccountKey.byteArray);
+ }
+ return deviceAccountKeys;
+ }
+ }
+
+ /**
+ * Class for reading FastPairEligibleAccountsRequest. Upon receiving this request, Fast Pair
+ * eligible accounts should be returned to bind Fast Pair devices.
+ *
+ * @hide
+ */
+ public static class FastPairEligibleAccountsRequest {
+ @SuppressWarnings("UnusedVariable")
+ private final FastPairEligibleAccountsRequestParcel mAccountsRequestParcel;
+
+ private FastPairEligibleAccountsRequest(
+ final FastPairEligibleAccountsRequestParcel accountsRequestParcel) {
+ this.mAccountsRequestParcel = accountsRequestParcel;
+ }
+ }
+
+ /**
+ * Class for reading FastPairManageAccountRequest. If the request type is MANAGE_REQUEST_ADD,
+ * the account is enabled to bind Fast Pair devices; If the request type is
+ * MANAGE_REQUEST_REMOVE, the account is disabled to bind more Fast Pair devices. Furthermore,
+ * all existing bounded Fast Pair devices are unbounded.
+ *
+ * @hide
+ */
+ public static class FastPairManageAccountRequest {
+
+ private final FastPairManageAccountRequestParcel mAccountRequestParcel;
+
+ private FastPairManageAccountRequest(
+ final FastPairManageAccountRequestParcel accountRequestParcel) {
+ this.mAccountRequestParcel = accountRequestParcel;
+ }
+
+ /**
+ * Get request type: MANAGE_REQUEST_ADD, or MANAGE_REQUEST_REMOVE.
+ *
+ * @hide
+ */
+ public @ManageRequestType int getRequestType() {
+ return this.mAccountRequestParcel.requestType;
+ }
+ /**
+ * Get account.
+ *
+ * @hide
+ */
+ public @NonNull Account getAccount() {
+ return this.mAccountRequestParcel.account;
+ }
+ }
+
+ /**
+ * Class for reading FastPairManageAccountDeviceRequest. If the request type is
+ * MANAGE_REQUEST_ADD, then a Fast Pair device is bounded to a Fast Pair account. If the
+ * request type is MANAGE_REQUEST_REMOVE, then a Fast Pair device is removed from a Fast Pair
+ * account.
+ *
+ * @hide
+ */
+ public static class FastPairManageAccountDeviceRequest {
+
+ private final FastPairManageAccountDeviceRequestParcel mRequestParcel;
+
+ private FastPairManageAccountDeviceRequest(
+ final FastPairManageAccountDeviceRequestParcel requestParcel) {
+ this.mRequestParcel = requestParcel;
+ }
+
+ /**
+ * Get request type: MANAGE_REQUEST_ADD, or MANAGE_REQUEST_REMOVE.
+ *
+ * @hide
+ */
+ public @ManageRequestType int getRequestType() {
+ return this.mRequestParcel.requestType;
+ }
+ /**
+ * Get account.
+ *
+ * @hide
+ */
+ public @NonNull Account getAccount() {
+ return this.mRequestParcel.account;
+ }
+ /**
+ * Get account key device metadata.
+ *
+ * @hide
+ */
+ public @NonNull FastPairAccountKeyDeviceMetadata getAccountKeyDeviceMetadata() {
+ return new FastPairAccountKeyDeviceMetadata(
+ this.mRequestParcel.accountKeyDeviceMetadata);
+ }
+ }
+
+ /**
+ * Callback class that sends back FastPairAntispoofKeyDeviceMetadata.
+ */
+ private final class WrapperFastPairAntispoofKeyDeviceMetadataCallback implements
+ FastPairAntispoofKeyDeviceMetadataCallback {
+
+ private IFastPairAntispoofKeyDeviceMetadataCallback mCallback;
+
+ private WrapperFastPairAntispoofKeyDeviceMetadataCallback(
+ IFastPairAntispoofKeyDeviceMetadataCallback callback) {
+ mCallback = callback;
+ }
+
+ /**
+ * Sends back FastPairAntispoofKeyDeviceMetadata.
+ */
+ @Override
+ public void onFastPairAntispoofKeyDeviceMetadataReceived(
+ @NonNull FastPairAntispoofKeyDeviceMetadata metadata) {
+ try {
+ mCallback.onFastPairAntispoofKeyDeviceMetadataReceived(metadata.mMetadataParcel);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (RuntimeException e) {
+ Log.w(mTag, e);
+ }
+ }
+
+ @Override
+ public void onError(@ErrorCode int code, @Nullable String message) {
+ try {
+ mCallback.onError(code, message);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (RuntimeException e) {
+ Log.w(mTag, e);
+ }
+ }
+ }
+
+ /**
+ * Callback class that sends back collection of FastPairAccountKeyDeviceMetadata.
+ */
+ private final class WrapperFastPairAccountDevicesMetadataCallback implements
+ FastPairAccountDevicesMetadataCallback {
+
+ private IFastPairAccountDevicesMetadataCallback mCallback;
+
+ private WrapperFastPairAccountDevicesMetadataCallback(
+ IFastPairAccountDevicesMetadataCallback callback) {
+ mCallback = callback;
+ }
+
+ /**
+ * Sends back collection of FastPairAccountKeyDeviceMetadata.
+ */
+ @Override
+ public void onFastPairAccountDevicesMetadataReceived(
+ @NonNull Collection<FastPairAccountKeyDeviceMetadata> metadatas) {
+ FastPairAccountKeyDeviceMetadataParcel[] metadataParcels =
+ new FastPairAccountKeyDeviceMetadataParcel[metadatas.size()];
+ int i = 0;
+ for (FastPairAccountKeyDeviceMetadata metadata : metadatas) {
+ metadataParcels[i] = metadata.mMetadataParcel;
+ i = i + 1;
+ }
+ try {
+ mCallback.onFastPairAccountDevicesMetadataReceived(metadataParcels);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (RuntimeException e) {
+ Log.w(mTag, e);
+ }
+ }
+
+ @Override
+ public void onError(@ErrorCode int code, @Nullable String message) {
+ try {
+ mCallback.onError(code, message);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (RuntimeException e) {
+ Log.w(mTag, e);
+ }
+ }
+ }
+
+ /**
+ * Callback class that sends back eligible Fast Pair accounts.
+ */
+ private final class WrapperFastPairEligibleAccountsCallback implements
+ FastPairEligibleAccountsCallback {
+
+ private IFastPairEligibleAccountsCallback mCallback;
+
+ private WrapperFastPairEligibleAccountsCallback(
+ IFastPairEligibleAccountsCallback callback) {
+ mCallback = callback;
+ }
+
+ /**
+ * Sends back the eligible Fast Pair accounts.
+ */
+ @Override
+ public void onFastPairEligibleAccountsReceived(
+ @NonNull Collection<FastPairEligibleAccount> accounts) {
+ int i = 0;
+ FastPairEligibleAccountParcel[] accountParcels =
+ new FastPairEligibleAccountParcel[accounts.size()];
+ for (FastPairEligibleAccount account: accounts) {
+ accountParcels[i] = account.mAccountParcel;
+ i = i + 1;
+ }
+ try {
+ mCallback.onFastPairEligibleAccountsReceived(accountParcels);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (RuntimeException e) {
+ Log.w(mTag, e);
+ }
+ }
+
+ @Override
+ public void onError(@ErrorCode int code, @Nullable String message) {
+ try {
+ mCallback.onError(code, message);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (RuntimeException e) {
+ Log.w(mTag, e);
+ }
+ }
+ }
+
+ /**
+ * Callback class that sends back Fast Pair account management result.
+ */
+ private final class WrapperFastPairManageAccountCallback implements
+ FastPairManageActionCallback {
+
+ private IFastPairManageAccountCallback mCallback;
+
+ private WrapperFastPairManageAccountCallback(
+ IFastPairManageAccountCallback callback) {
+ mCallback = callback;
+ }
+
+ /**
+ * Sends back Fast Pair account opt in result.
+ */
+ @Override
+ public void onSuccess() {
+ try {
+ mCallback.onSuccess();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (RuntimeException e) {
+ Log.w(mTag, e);
+ }
+ }
+
+ @Override
+ public void onError(@ErrorCode int code, @Nullable String message) {
+ try {
+ mCallback.onError(code, message);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (RuntimeException e) {
+ Log.w(mTag, e);
+ }
+ }
+ }
+
+ /**
+ * Call back class that sends back account-device mapping management result.
+ */
+ private final class WrapperFastPairManageAccountDeviceCallback implements
+ FastPairManageActionCallback {
+
+ private IFastPairManageAccountDeviceCallback mCallback;
+
+ private WrapperFastPairManageAccountDeviceCallback(
+ IFastPairManageAccountDeviceCallback callback) {
+ mCallback = callback;
+ }
+
+ /**
+ * Sends back the account-device mapping management result.
+ */
+ @Override
+ public void onSuccess() {
+ try {
+ mCallback.onSuccess();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (RuntimeException e) {
+ Log.w(mTag, e);
+ }
+ }
+
+ @Override
+ public void onError(@ErrorCode int code, @Nullable String message) {
+ try {
+ mCallback.onError(code, message);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (RuntimeException e) {
+ Log.w(mTag, e);
+ }
+ }
+ }
+
+ private final class Service extends IFastPairDataProvider.Stub {
+
+ Service() {
+ }
+
+ @Override
+ public void loadFastPairAntispoofKeyDeviceMetadata(
+ @NonNull FastPairAntispoofKeyDeviceMetadataRequestParcel requestParcel,
+ IFastPairAntispoofKeyDeviceMetadataCallback callback) {
+ onLoadFastPairAntispoofKeyDeviceMetadata(
+ new FastPairAntispoofKeyDeviceMetadataRequest(requestParcel),
+ new WrapperFastPairAntispoofKeyDeviceMetadataCallback(callback));
+ }
+
+ @Override
+ public void loadFastPairAccountDevicesMetadata(
+ @NonNull FastPairAccountDevicesMetadataRequestParcel requestParcel,
+ IFastPairAccountDevicesMetadataCallback callback) {
+ onLoadFastPairAccountDevicesMetadata(
+ new FastPairAccountDevicesMetadataRequest(requestParcel),
+ new WrapperFastPairAccountDevicesMetadataCallback(callback));
+ }
+
+ @Override
+ public void loadFastPairEligibleAccounts(
+ @NonNull FastPairEligibleAccountsRequestParcel requestParcel,
+ IFastPairEligibleAccountsCallback callback) {
+ onLoadFastPairEligibleAccounts(new FastPairEligibleAccountsRequest(requestParcel),
+ new WrapperFastPairEligibleAccountsCallback(callback));
+ }
+
+ @Override
+ public void manageFastPairAccount(
+ @NonNull FastPairManageAccountRequestParcel requestParcel,
+ IFastPairManageAccountCallback callback) {
+ onManageFastPairAccount(new FastPairManageAccountRequest(requestParcel),
+ new WrapperFastPairManageAccountCallback(callback));
+ }
+
+ @Override
+ public void manageFastPairAccountDevice(
+ @NonNull FastPairManageAccountDeviceRequestParcel requestParcel,
+ IFastPairManageAccountDeviceCallback callback) {
+ onManageFastPairAccountDevice(new FastPairManageAccountDeviceRequest(requestParcel),
+ new WrapperFastPairManageAccountDeviceCallback(callback));
+ }
+ }
+}
diff --git a/nearby/framework/java/android/nearby/FastPairDevice.aidl b/nearby/framework/java/android/nearby/FastPairDevice.aidl
new file mode 100644
index 0000000..5942966
--- /dev/null
+++ b/nearby/framework/java/android/nearby/FastPairDevice.aidl
@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+/**
+ * A class represents a Fast Pair device that can be discovered by multiple mediums.
+ *
+ * {@hide}
+ */
+parcelable FastPairDevice;
diff --git a/nearby/framework/java/android/nearby/FastPairDevice.java b/nearby/framework/java/android/nearby/FastPairDevice.java
new file mode 100644
index 0000000..7160533
--- /dev/null
+++ b/nearby/framework/java/android/nearby/FastPairDevice.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A class represents a Fast Pair device that can be discovered by multiple mediums.
+ *
+ * @hide
+ */
+public class FastPairDevice extends NearbyDevice implements Parcelable {
+ /**
+ * Used to read a FastPairDevice from a Parcel.
+ */
+ public static final Creator<FastPairDevice> CREATOR = new Creator<FastPairDevice>() {
+ @Override
+ public FastPairDevice createFromParcel(Parcel in) {
+ FastPairDevice.Builder builder = new FastPairDevice.Builder();
+ if (in.readInt() == 1) {
+ builder.setName(in.readString());
+ }
+ int size = in.readInt();
+ for (int i = 0; i < size; i++) {
+ builder.addMedium(in.readInt());
+ }
+ builder.setRssi(in.readInt());
+ builder.setTxPower(in.readInt());
+ if (in.readInt() == 1) {
+ builder.setModelId(in.readString());
+ }
+ builder.setBluetoothAddress(in.readString());
+ if (in.readInt() == 1) {
+ int dataLength = in.readInt();
+ byte[] data = new byte[dataLength];
+ in.readByteArray(data);
+ builder.setData(data);
+ }
+ return builder.build();
+ }
+
+ @Override
+ public FastPairDevice[] newArray(int size) {
+ return new FastPairDevice[size];
+ }
+ };
+
+ // The transmit power in dBm. Valid range is [-127, 126]. a
+ // See android.bluetooth.le.ScanResult#getTxPower
+ private int mTxPower;
+
+ // Some OEM devices devices don't have model Id.
+ @Nullable private final String mModelId;
+
+ // Bluetooth hardware address as string. Can be read from BLE ScanResult.
+ private final String mBluetoothAddress;
+
+ @Nullable
+ private final byte[] mData;
+
+ /**
+ * Creates a new FastPairDevice.
+ *
+ * @param name Name of the FastPairDevice. Can be {@code null} if there is no name.
+ * @param mediums The {@link Medium}s over which the device is discovered.
+ * @param rssi The received signal strength in dBm.
+ * @param txPower The transmit power in dBm. Valid range is [-127, 126].
+ * @param modelId The identifier of the Fast Pair device.
+ * Can be {@code null} if there is no Model ID.
+ * @param bluetoothAddress The hardware address of this BluetoothDevice.
+ * @param data Extra data for a Fast Pair device.
+ */
+ public FastPairDevice(@Nullable String name,
+ List<Integer> mediums,
+ int rssi,
+ int txPower,
+ @Nullable String modelId,
+ @NonNull String bluetoothAddress,
+ @Nullable byte[] data) {
+ super(name, mediums, rssi);
+ this.mTxPower = txPower;
+ this.mModelId = modelId;
+ this.mBluetoothAddress = bluetoothAddress;
+ this.mData = data;
+ }
+
+ /**
+ * Gets the transmit power in dBm. A value of
+ * android.bluetooth.le.ScanResult#TX_POWER_NOT_PRESENT
+ * indicates that the TX power is not present.
+ */
+ @IntRange(from = -127, to = 126)
+ public int getTxPower() {
+ return mTxPower;
+ }
+
+ /**
+ * Gets the identifier of the Fast Pair device. Can be {@code null} if there is no Model ID.
+ */
+ @Nullable
+ public String getModelId() {
+ return this.mModelId;
+ }
+
+ /**
+ * Gets the hardware address of this BluetoothDevice.
+ */
+ @NonNull
+ public String getBluetoothAddress() {
+ return mBluetoothAddress;
+ }
+
+ /**
+ * Gets the extra data for a Fast Pair device. Can be {@code null} if there is extra data.
+ *
+ * @hide
+ */
+ @Nullable
+ public byte[] getData() {
+ return mData;
+ }
+
+ /**
+ * No special parcel contents.
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Returns a string representation of this FastPairDevice.
+ */
+ @Override
+ public String toString() {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("FastPairDevice [");
+ String name = getName();
+ if (getName() != null && !name.isEmpty()) {
+ stringBuilder.append("name=").append(name).append(", ");
+ }
+ stringBuilder.append("medium={");
+ for (int medium: getMediums()) {
+ stringBuilder.append(mediumToString(medium));
+ }
+ stringBuilder.append("} rssi=").append(getRssi());
+ stringBuilder.append(" txPower=").append(mTxPower);
+ stringBuilder.append(" modelId=").append(mModelId);
+ stringBuilder.append(" bluetoothAddress=").append(mBluetoothAddress);
+ stringBuilder.append("]");
+ return stringBuilder.toString();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof FastPairDevice) {
+ FastPairDevice otherDevice = (FastPairDevice) other;
+ if (!super.equals(other)) {
+ return false;
+ }
+ return mTxPower == otherDevice.mTxPower
+ && Objects.equals(mModelId, otherDevice.mModelId)
+ && Objects.equals(mBluetoothAddress, otherDevice.mBluetoothAddress)
+ && Arrays.equals(mData, otherDevice.mData);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ getName(), getMediums(), getRssi(), mTxPower, mModelId, mBluetoothAddress,
+ Arrays.hashCode(mData));
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ String name = getName();
+ dest.writeInt(name == null ? 0 : 1);
+ if (name != null) {
+ dest.writeString(name);
+ }
+ List<Integer> mediums = getMediums();
+ dest.writeInt(mediums.size());
+ for (int medium : mediums) {
+ dest.writeInt(medium);
+ }
+ dest.writeInt(getRssi());
+ dest.writeInt(mTxPower);
+ dest.writeInt(mModelId == null ? 0 : 1);
+ if (mModelId != null) {
+ dest.writeString(mModelId);
+ }
+ dest.writeString(mBluetoothAddress);
+ dest.writeInt(mData == null ? 0 : 1);
+ if (mData != null) {
+ dest.writeInt(mData.length);
+ dest.writeByteArray(mData);
+ }
+ }
+
+ /**
+ * A builder class for {@link FastPairDevice}
+ *
+ * @hide
+ */
+ public static final class Builder {
+ private final List<Integer> mMediums;
+
+ @Nullable private String mName;
+ private int mRssi;
+ private int mTxPower;
+ @Nullable private String mModelId;
+ private String mBluetoothAddress;
+ @Nullable private byte[] mData;
+
+ public Builder() {
+ mMediums = new ArrayList<>();
+ }
+
+ /**
+ * Sets the name of the Fast Pair device.
+ *
+ * @param name Name of the FastPairDevice. Can be {@code null} if there is no name.
+ */
+ @NonNull
+ public Builder setName(@Nullable String name) {
+ mName = name;
+ return this;
+ }
+
+ /**
+ * Sets the medium over which the Fast Pair device is discovered.
+ *
+ * @param medium The {@link Medium} over which the device is discovered.
+ */
+ @NonNull
+ public Builder addMedium(@Medium int medium) {
+ mMediums.add(medium);
+ return this;
+ }
+
+ /**
+ * Sets the RSSI between the scan device and the discovered Fast Pair device.
+ *
+ * @param rssi The received signal strength in dBm.
+ */
+ @NonNull
+ public Builder setRssi(@IntRange(from = -127, to = 126) int rssi) {
+ mRssi = rssi;
+ return this;
+ }
+
+ /**
+ * Sets the txPower.
+ *
+ * @param txPower The transmit power in dBm
+ */
+ @NonNull
+ public Builder setTxPower(@IntRange(from = -127, to = 126) int txPower) {
+ mTxPower = txPower;
+ return this;
+ }
+
+ /**
+ * Sets the model Id of this Fast Pair device.
+ *
+ * @param modelId The identifier of the Fast Pair device. Can be {@code null}
+ * if there is no Model ID.
+ */
+ @NonNull
+ public Builder setModelId(@Nullable String modelId) {
+ mModelId = modelId;
+ return this;
+ }
+
+ /**
+ * Sets the hardware address of this BluetoothDevice.
+ *
+ * @param bluetoothAddress The hardware address of this BluetoothDevice.
+ */
+ @NonNull
+ public Builder setBluetoothAddress(@NonNull String bluetoothAddress) {
+ Objects.requireNonNull(bluetoothAddress);
+ mBluetoothAddress = bluetoothAddress;
+ return this;
+ }
+
+ /**
+ * Sets the raw data for a FastPairDevice. Can be {@code null} if there is no extra data.
+ *
+ * @hide
+ */
+ @NonNull
+ public Builder setData(@Nullable byte[] data) {
+ mData = data;
+ return this;
+ }
+
+ /**
+ * Builds a FastPairDevice and return it.
+ */
+ @NonNull
+ public FastPairDevice build() {
+ return new FastPairDevice(mName, mMediums, mRssi, mTxPower, mModelId,
+ mBluetoothAddress, mData);
+ }
+ }
+}
diff --git a/nearby/framework/java/android/nearby/FastPairDeviceMetadata.java b/nearby/framework/java/android/nearby/FastPairDeviceMetadata.java
new file mode 100644
index 0000000..0e2e79d
--- /dev/null
+++ b/nearby/framework/java/android/nearby/FastPairDeviceMetadata.java
@@ -0,0 +1,683 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.nearby.aidl.FastPairDeviceMetadataParcel;
+
+/**
+ * Class for the properties of a given type of Fast Pair device, including images and text.
+ *
+ * @hide
+ */
+public class FastPairDeviceMetadata {
+
+ FastPairDeviceMetadataParcel mMetadataParcel;
+
+ FastPairDeviceMetadata(
+ FastPairDeviceMetadataParcel metadataParcel) {
+ this.mMetadataParcel = metadataParcel;
+ }
+
+ /**
+ * Get ImageUrl, which will be displayed in notification.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getImageUrl() {
+ return mMetadataParcel.imageUrl;
+ }
+
+ /**
+ * Get IntentUri, which will be launched to install companion app.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getIntentUri() {
+ return mMetadataParcel.intentUri;
+ }
+
+ /**
+ * Get BLE transmit power, as described in Fast Pair spec, see
+ * <a href="https://developers.google.com/nearby/fast-pair/spec#transmit_power">Transmit Power</a>
+ *
+ * @hide
+ */
+ public int getBleTxPower() {
+ return mMetadataParcel.bleTxPower;
+ }
+
+ /**
+ * Get Fast Pair Half Sheet trigger distance in meters.
+ *
+ * @hide
+ */
+ public float getTriggerDistance() {
+ return mMetadataParcel.triggerDistance;
+ }
+
+ /**
+ * Get Fast Pair device image, which is submitted at device registration time to display on
+ * notification. It is a 32-bit PNG with dimensions of 512px by 512px.
+ *
+ * @return Fast Pair device image in 32-bit PNG with dimensions of 512px by 512px.
+ * @hide
+ */
+ @Nullable
+ public byte[] getImage() {
+ return mMetadataParcel.image;
+ }
+
+ /**
+ * Get Fast Pair device type.
+ * DEVICE_TYPE_UNSPECIFIED = 0;
+ * HEADPHONES = 1;
+ * TRUE_WIRELESS_HEADPHONES = 7;
+ * @hide
+ */
+ public int getDeviceType() {
+ return mMetadataParcel.deviceType;
+ }
+
+ /**
+ * Get Fast Pair device name. e.g., "Pixel Buds A-Series".
+ *
+ * @hide
+ */
+ @Nullable
+ public String getName() {
+ return mMetadataParcel.name;
+ }
+
+ /**
+ * Get true wireless image url for left bud.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getTrueWirelessImageUrlLeftBud() {
+ return mMetadataParcel.trueWirelessImageUrlLeftBud;
+ }
+
+ /**
+ * Get true wireless image url for right bud.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getTrueWirelessImageUrlRightBud() {
+ return mMetadataParcel.trueWirelessImageUrlRightBud;
+ }
+
+ /**
+ * Get true wireless image url for case.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getTrueWirelessImageUrlCase() {
+ return mMetadataParcel.trueWirelessImageUrlCase;
+ }
+
+ /**
+ * Get InitialNotificationDescription, which is a translated string of
+ * "Tap to pair. Earbuds will be tied to %s" based on locale.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getInitialNotificationDescription() {
+ return mMetadataParcel.initialNotificationDescription;
+ }
+
+ /**
+ * Get InitialNotificationDescriptionNoAccount, which is a translated string of
+ * "Tap to pair with this device" based on locale.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getInitialNotificationDescriptionNoAccount() {
+ return mMetadataParcel.initialNotificationDescriptionNoAccount;
+ }
+
+ /**
+ * Get OpenCompanionAppDescription, which is a translated string of
+ * "Tap to finish setup" based on locale.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getOpenCompanionAppDescription() {
+ return mMetadataParcel.openCompanionAppDescription;
+ }
+
+ /**
+ * Get UpdateCompanionAppDescription, which is a translated string of
+ * "Tap to update device settings and finish setup" based on locale.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getUpdateCompanionAppDescription() {
+ return mMetadataParcel.updateCompanionAppDescription;
+ }
+
+ /**
+ * Get DownloadCompanionAppDescription, which is a translated string of
+ * "Tap to download device app on Google Play and see all features" based on locale.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getDownloadCompanionAppDescription() {
+ return mMetadataParcel.downloadCompanionAppDescription;
+ }
+
+ /**
+ * Get UnableToConnectTitle, which is a translated string of
+ * "Unable to connect" based on locale.
+ */
+ @Nullable
+ public String getUnableToConnectTitle() {
+ return mMetadataParcel.unableToConnectTitle;
+ }
+
+ /**
+ * Get UnableToConnectDescription, which is a translated string of
+ * "Try manually pairing to the device" based on locale.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getUnableToConnectDescription() {
+ return mMetadataParcel.unableToConnectDescription;
+ }
+
+ /**
+ * Get InitialPairingDescription, which is a translated string of
+ * "%s will appear on devices linked with %s" based on locale.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getInitialPairingDescription() {
+ return mMetadataParcel.initialPairingDescription;
+ }
+
+ /**
+ * Get ConnectSuccessCompanionAppInstalled, which is a translated string of
+ * "Your device is ready to be set up" based on locale.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getConnectSuccessCompanionAppInstalled() {
+ return mMetadataParcel.connectSuccessCompanionAppInstalled;
+ }
+
+ /**
+ * Get ConnectSuccessCompanionAppNotInstalled, which is a translated string of
+ * "Download the device app on Google Play to see all available features" based on locale.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getConnectSuccessCompanionAppNotInstalled() {
+ return mMetadataParcel.connectSuccessCompanionAppNotInstalled;
+ }
+
+ /**
+ * Get SubsequentPairingDescription, which is a translated string of
+ * "Connect %s to this phone" based on locale.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getSubsequentPairingDescription() {
+ return mMetadataParcel.subsequentPairingDescription;
+ }
+
+ /**
+ * Get RetroactivePairingDescription, which is a translated string of
+ * "Save device to %s for faster pairing to your other devices" based on locale.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getRetroactivePairingDescription() {
+ return mMetadataParcel.retroactivePairingDescription;
+ }
+
+ /**
+ * Get WaitLaunchCompanionAppDescription, which is a translated string of
+ * "This will take a few moments" based on locale.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getWaitLaunchCompanionAppDescription() {
+ return mMetadataParcel.waitLaunchCompanionAppDescription;
+ }
+
+ /**
+ * Get FailConnectGoToSettingsDescription, which is a translated string of
+ * "Try manually pairing to the device by going to Settings" based on locale.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getFailConnectGoToSettingsDescription() {
+ return mMetadataParcel.failConnectGoToSettingsDescription;
+ }
+
+ /**
+ * Builder used to create FastPairDeviceMetadata.
+ *
+ * @hide
+ */
+ public static final class Builder {
+
+ private final FastPairDeviceMetadataParcel mBuilderParcel;
+
+ /**
+ * Default constructor of Builder.
+ *
+ * @hide
+ */
+ public Builder() {
+ mBuilderParcel = new FastPairDeviceMetadataParcel();
+ mBuilderParcel.imageUrl = null;
+ mBuilderParcel.intentUri = null;
+ mBuilderParcel.name = null;
+ mBuilderParcel.bleTxPower = 0;
+ mBuilderParcel.triggerDistance = 0;
+ mBuilderParcel.image = null;
+ mBuilderParcel.deviceType = 0; // DEVICE_TYPE_UNSPECIFIED
+ mBuilderParcel.trueWirelessImageUrlLeftBud = null;
+ mBuilderParcel.trueWirelessImageUrlRightBud = null;
+ mBuilderParcel.trueWirelessImageUrlCase = null;
+ mBuilderParcel.initialNotificationDescription = null;
+ mBuilderParcel.initialNotificationDescriptionNoAccount = null;
+ mBuilderParcel.openCompanionAppDescription = null;
+ mBuilderParcel.updateCompanionAppDescription = null;
+ mBuilderParcel.downloadCompanionAppDescription = null;
+ mBuilderParcel.unableToConnectTitle = null;
+ mBuilderParcel.unableToConnectDescription = null;
+ mBuilderParcel.initialPairingDescription = null;
+ mBuilderParcel.connectSuccessCompanionAppInstalled = null;
+ mBuilderParcel.connectSuccessCompanionAppNotInstalled = null;
+ mBuilderParcel.subsequentPairingDescription = null;
+ mBuilderParcel.retroactivePairingDescription = null;
+ mBuilderParcel.waitLaunchCompanionAppDescription = null;
+ mBuilderParcel.failConnectGoToSettingsDescription = null;
+ }
+
+ /**
+ * Set ImageUlr.
+ *
+ * @param imageUrl Image Ulr.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setImageUrl(@Nullable String imageUrl) {
+ mBuilderParcel.imageUrl = imageUrl;
+ return this;
+ }
+
+ /**
+ * Set IntentUri.
+ *
+ * @param intentUri Intent uri.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setIntentUri(@Nullable String intentUri) {
+ mBuilderParcel.intentUri = intentUri;
+ return this;
+ }
+
+ /**
+ * Set device name.
+ *
+ * @param name Device name.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setName(@Nullable String name) {
+ mBuilderParcel.name = name;
+ return this;
+ }
+
+ /**
+ * Set ble transmission power.
+ *
+ * @param bleTxPower Ble transmission power.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setBleTxPower(int bleTxPower) {
+ mBuilderParcel.bleTxPower = bleTxPower;
+ return this;
+ }
+
+ /**
+ * Set trigger distance.
+ *
+ * @param triggerDistance Fast Pair trigger distance.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setTriggerDistance(float triggerDistance) {
+ mBuilderParcel.triggerDistance = triggerDistance;
+ return this;
+ }
+
+ /**
+ * Set image.
+ *
+ * @param image Fast Pair device image, which is submitted at device registration time to
+ * display on notification. It is a 32-bit PNG with dimensions of
+ * 512px by 512px.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setImage(@Nullable byte[] image) {
+ mBuilderParcel.image = image;
+ return this;
+ }
+
+ /**
+ * Set device type.
+ *
+ * @param deviceType Fast Pair device type.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setDeviceType(int deviceType) {
+ mBuilderParcel.deviceType = deviceType;
+ return this;
+ }
+
+ /**
+ * Set true wireless image url for left bud.
+ *
+ * @param trueWirelessImageUrlLeftBud True wireless image url for left bud.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setTrueWirelessImageUrlLeftBud(
+ @Nullable String trueWirelessImageUrlLeftBud) {
+ mBuilderParcel.trueWirelessImageUrlLeftBud = trueWirelessImageUrlLeftBud;
+ return this;
+ }
+
+ /**
+ * Set true wireless image url for right bud.
+ *
+ * @param trueWirelessImageUrlRightBud True wireless image url for right bud.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setTrueWirelessImageUrlRightBud(
+ @Nullable String trueWirelessImageUrlRightBud) {
+ mBuilderParcel.trueWirelessImageUrlRightBud = trueWirelessImageUrlRightBud;
+ return this;
+ }
+
+ /**
+ * Set true wireless image url for case.
+ *
+ * @param trueWirelessImageUrlCase True wireless image url for case.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setTrueWirelessImageUrlCase(@Nullable String trueWirelessImageUrlCase) {
+ mBuilderParcel.trueWirelessImageUrlCase = trueWirelessImageUrlCase;
+ return this;
+ }
+
+ /**
+ * Set InitialNotificationDescription.
+ *
+ * @param initialNotificationDescription Initial notification description.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setInitialNotificationDescription(
+ @Nullable String initialNotificationDescription) {
+ mBuilderParcel.initialNotificationDescription = initialNotificationDescription;
+ return this;
+ }
+
+ /**
+ * Set InitialNotificationDescriptionNoAccount.
+ *
+ * @param initialNotificationDescriptionNoAccount Initial notification description when
+ * account is not present.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setInitialNotificationDescriptionNoAccount(
+ @Nullable String initialNotificationDescriptionNoAccount) {
+ mBuilderParcel.initialNotificationDescriptionNoAccount =
+ initialNotificationDescriptionNoAccount;
+ return this;
+ }
+
+ /**
+ * Set OpenCompanionAppDescription.
+ *
+ * @param openCompanionAppDescription Description for opening companion app.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setOpenCompanionAppDescription(
+ @Nullable String openCompanionAppDescription) {
+ mBuilderParcel.openCompanionAppDescription = openCompanionAppDescription;
+ return this;
+ }
+
+ /**
+ * Set UpdateCompanionAppDescription.
+ *
+ * @param updateCompanionAppDescription Description for updating companion app.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setUpdateCompanionAppDescription(
+ @Nullable String updateCompanionAppDescription) {
+ mBuilderParcel.updateCompanionAppDescription = updateCompanionAppDescription;
+ return this;
+ }
+
+ /**
+ * Set DownloadCompanionAppDescription.
+ *
+ * @param downloadCompanionAppDescription Description for downloading companion app.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setDownloadCompanionAppDescription(
+ @Nullable String downloadCompanionAppDescription) {
+ mBuilderParcel.downloadCompanionAppDescription = downloadCompanionAppDescription;
+ return this;
+ }
+
+ /**
+ * Set UnableToConnectTitle.
+ *
+ * @param unableToConnectTitle Title when Fast Pair device is unable to be connected to.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setUnableToConnectTitle(@Nullable String unableToConnectTitle) {
+ mBuilderParcel.unableToConnectTitle = unableToConnectTitle;
+ return this;
+ }
+
+ /**
+ * Set UnableToConnectDescription.
+ *
+ * @param unableToConnectDescription Description when Fast Pair device is unable to be
+ * connected to.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setUnableToConnectDescription(
+ @Nullable String unableToConnectDescription) {
+ mBuilderParcel.unableToConnectDescription = unableToConnectDescription;
+ return this;
+ }
+
+ /**
+ * Set InitialPairingDescription.
+ *
+ * @param initialPairingDescription Description for Fast Pair initial pairing.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setInitialPairingDescription(@Nullable String initialPairingDescription) {
+ mBuilderParcel.initialPairingDescription = initialPairingDescription;
+ return this;
+ }
+
+ /**
+ * Set ConnectSuccessCompanionAppInstalled.
+ *
+ * @param connectSuccessCompanionAppInstalled Description that let user open the companion
+ * app.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setConnectSuccessCompanionAppInstalled(
+ @Nullable String connectSuccessCompanionAppInstalled) {
+ mBuilderParcel.connectSuccessCompanionAppInstalled =
+ connectSuccessCompanionAppInstalled;
+ return this;
+ }
+
+ /**
+ * Set ConnectSuccessCompanionAppNotInstalled.
+ *
+ * @param connectSuccessCompanionAppNotInstalled Description that let user download the
+ * companion app.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setConnectSuccessCompanionAppNotInstalled(
+ @Nullable String connectSuccessCompanionAppNotInstalled) {
+ mBuilderParcel.connectSuccessCompanionAppNotInstalled =
+ connectSuccessCompanionAppNotInstalled;
+ return this;
+ }
+
+ /**
+ * Set SubsequentPairingDescription.
+ *
+ * @param subsequentPairingDescription Description that reminds user there is a paired
+ * device nearby.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setSubsequentPairingDescription(
+ @Nullable String subsequentPairingDescription) {
+ mBuilderParcel.subsequentPairingDescription = subsequentPairingDescription;
+ return this;
+ }
+
+ /**
+ * Set RetroactivePairingDescription.
+ *
+ * @param retroactivePairingDescription Description that reminds users opt in their device.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setRetroactivePairingDescription(
+ @Nullable String retroactivePairingDescription) {
+ mBuilderParcel.retroactivePairingDescription = retroactivePairingDescription;
+ return this;
+ }
+
+ /**
+ * Set WaitLaunchCompanionAppDescription.
+ *
+ * @param waitLaunchCompanionAppDescription Description that indicates companion app is
+ * about to launch.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setWaitLaunchCompanionAppDescription(
+ @Nullable String waitLaunchCompanionAppDescription) {
+ mBuilderParcel.waitLaunchCompanionAppDescription =
+ waitLaunchCompanionAppDescription;
+ return this;
+ }
+
+ /**
+ * Set FailConnectGoToSettingsDescription.
+ *
+ * @param failConnectGoToSettingsDescription Description that indicates go to bluetooth
+ * settings when connection fail.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setFailConnectGoToSettingsDescription(
+ @Nullable String failConnectGoToSettingsDescription) {
+ mBuilderParcel.failConnectGoToSettingsDescription =
+ failConnectGoToSettingsDescription;
+ return this;
+ }
+
+ /**
+ * Build {@link FastPairDeviceMetadata} with the currently set configuration.
+ *
+ * @hide
+ */
+ @NonNull
+ public FastPairDeviceMetadata build() {
+ return new FastPairDeviceMetadata(mBuilderParcel);
+ }
+ }
+}
diff --git a/nearby/framework/java/android/nearby/FastPairDiscoveryItem.java b/nearby/framework/java/android/nearby/FastPairDiscoveryItem.java
new file mode 100644
index 0000000..d8dfe29
--- /dev/null
+++ b/nearby/framework/java/android/nearby/FastPairDiscoveryItem.java
@@ -0,0 +1,529 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.nearby.aidl.FastPairDiscoveryItemParcel;
+
+/**
+ * Class for FastPairDiscoveryItem and its builder.
+ *
+ * @hide
+ */
+public class FastPairDiscoveryItem {
+
+ FastPairDiscoveryItemParcel mMetadataParcel;
+
+ FastPairDiscoveryItem(
+ FastPairDiscoveryItemParcel metadataParcel) {
+ this.mMetadataParcel = metadataParcel;
+ }
+
+ /**
+ * Get Id.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getId() {
+ return mMetadataParcel.id;
+ }
+
+ /**
+ * Get MacAddress.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getMacAddress() {
+ return mMetadataParcel.macAddress;
+ }
+
+ /**
+ * Get ActionUrl.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getActionUrl() {
+ return mMetadataParcel.actionUrl;
+ }
+
+ /**
+ * Get DeviceName.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getDeviceName() {
+ return mMetadataParcel.deviceName;
+ }
+
+ /**
+ * Get Title.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getTitle() {
+ return mMetadataParcel.title;
+ }
+
+ /**
+ * Get Description.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getDescription() {
+ return mMetadataParcel.description;
+ }
+
+ /**
+ * Get DisplayUrl.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getDisplayUrl() {
+ return mMetadataParcel.displayUrl;
+ }
+
+ /**
+ * Get LastObservationTimestampMillis.
+ *
+ * @hide
+ */
+ public long getLastObservationTimestampMillis() {
+ return mMetadataParcel.lastObservationTimestampMillis;
+ }
+
+ /**
+ * Get FirstObservationTimestampMillis.
+ *
+ * @hide
+ */
+ public long getFirstObservationTimestampMillis() {
+ return mMetadataParcel.firstObservationTimestampMillis;
+ }
+
+ /**
+ * Get State.
+ *
+ * @hide
+ */
+ public int getState() {
+ return mMetadataParcel.state;
+ }
+
+ /**
+ * Get ActionUrlType.
+ *
+ * @hide
+ */
+ public int getActionUrlType() {
+ return mMetadataParcel.actionUrlType;
+ }
+
+ /**
+ * Get Rssi.
+ *
+ * @hide
+ */
+ public int getRssi() {
+ return mMetadataParcel.rssi;
+ }
+
+ /**
+ * Get PendingAppInstallTimestampMillis.
+ *
+ * @hide
+ */
+ public long getPendingAppInstallTimestampMillis() {
+ return mMetadataParcel.pendingAppInstallTimestampMillis;
+ }
+
+ /**
+ * Get TxPower.
+ *
+ * @hide
+ */
+ public int getTxPower() {
+ return mMetadataParcel.txPower;
+ }
+
+ /**
+ * Get AppName.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getAppName() {
+ return mMetadataParcel.appName;
+ }
+
+ /**
+ * Get PackageName.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getPackageName() {
+ return mMetadataParcel.packageName;
+ }
+
+ /**
+ * Get TriggerId.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getTriggerId() {
+ return mMetadataParcel.triggerId;
+ }
+
+ /**
+ * Get IconPng, which is submitted at device registration time to display on notification. It is
+ * a 32-bit PNG with dimensions of 512px by 512px.
+ *
+ * @return IconPng in 32-bit PNG with dimensions of 512px by 512px.
+ * @hide
+ */
+ @Nullable
+ public byte[] getIconPng() {
+ return mMetadataParcel.iconPng;
+ }
+
+ /**
+ * Get IconFifeUrl.
+ *
+ * @hide
+ */
+ @Nullable
+ public String getIconFfeUrl() {
+ return mMetadataParcel.iconFifeUrl;
+ }
+
+ /**
+ * Get authenticationPublicKeySecp256r1, which is same as AntiSpoof public key, see
+ * <a href="https://developers.google.com/nearby/fast-pair/spec#data_format">Data Format</a>.
+ *
+ * @return 64-byte authenticationPublicKeySecp256r1.
+ * @hide
+ */
+ @Nullable
+ public byte[] getAuthenticationPublicKeySecp256r1() {
+ return mMetadataParcel.authenticationPublicKeySecp256r1;
+ }
+
+ /**
+ * Builder used to create FastPairDiscoveryItem.
+ *
+ * @hide
+ */
+ public static final class Builder {
+
+ private final FastPairDiscoveryItemParcel mBuilderParcel;
+
+ /**
+ * Default constructor of Builder.
+ *
+ * @hide
+ */
+ public Builder() {
+ mBuilderParcel = new FastPairDiscoveryItemParcel();
+ }
+
+ /**
+ * Set Id.
+ *
+ * @param id Unique id.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ *
+ * @hide
+ */
+ @NonNull
+ public Builder setId(@Nullable String id) {
+ mBuilderParcel.id = id;
+ return this;
+ }
+
+ /**
+ * Set MacAddress.
+ *
+ * @param macAddress Fast Pair device rotating mac address.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setMacAddress(@Nullable String macAddress) {
+ mBuilderParcel.macAddress = macAddress;
+ return this;
+ }
+
+ /**
+ * Set ActionUrl.
+ *
+ * @param actionUrl Action Url of Fast Pair device.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setActionUrl(@Nullable String actionUrl) {
+ mBuilderParcel.actionUrl = actionUrl;
+ return this;
+ }
+
+ /**
+ * Set DeviceName.
+ * @param deviceName Fast Pair device name.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setDeviceName(@Nullable String deviceName) {
+ mBuilderParcel.deviceName = deviceName;
+ return this;
+ }
+
+ /**
+ * Set Title.
+ *
+ * @param title Title of Fast Pair device.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setTitle(@Nullable String title) {
+ mBuilderParcel.title = title;
+ return this;
+ }
+
+ /**
+ * Set Description.
+ *
+ * @param description Description of Fast Pair device.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setDescription(@Nullable String description) {
+ mBuilderParcel.description = description;
+ return this;
+ }
+
+ /**
+ * Set DisplayUrl.
+ *
+ * @param displayUrl Display Url of Fast Pair device.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setDisplayUrl(@Nullable String displayUrl) {
+ mBuilderParcel.displayUrl = displayUrl;
+ return this;
+ }
+
+ /**
+ * Set LastObservationTimestampMillis.
+ *
+ * @param lastObservationTimestampMillis Last observed timestamp of Fast Pair device, keyed
+ * by a rotating id.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setLastObservationTimestampMillis(
+ long lastObservationTimestampMillis) {
+ mBuilderParcel.lastObservationTimestampMillis = lastObservationTimestampMillis;
+ return this;
+ }
+
+ /**
+ * Set FirstObservationTimestampMillis.
+ *
+ * @param firstObservationTimestampMillis First observed timestamp of Fast Pair device,
+ * keyed by a rotating id.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setFirstObservationTimestampMillis(
+ long firstObservationTimestampMillis) {
+ mBuilderParcel.firstObservationTimestampMillis = firstObservationTimestampMillis;
+ return this;
+ }
+
+ /**
+ * Set State.
+ *
+ * @param state Item's current state. e.g. if the item is blocked.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setState(int state) {
+ mBuilderParcel.state = state;
+ return this;
+ }
+
+ /**
+ * Set ActionUrlType.
+ *
+ * @param actionUrlType The resolved url type for the action_url.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setActionUrlType(int actionUrlType) {
+ mBuilderParcel.actionUrlType = actionUrlType;
+ return this;
+ }
+
+ /**
+ * Set Rssi.
+ *
+ * @param rssi Beacon's RSSI value.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setRssi(int rssi) {
+ mBuilderParcel.rssi = rssi;
+ return this;
+ }
+
+ /**
+ * Set PendingAppInstallTimestampMillis.
+ *
+ * @param pendingAppInstallTimestampMillis The timestamp when the user is redirected to App
+ * Store after clicking on the item.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setPendingAppInstallTimestampMillis(long pendingAppInstallTimestampMillis) {
+ mBuilderParcel.pendingAppInstallTimestampMillis = pendingAppInstallTimestampMillis;
+ return this;
+ }
+
+ /**
+ * Set TxPower.
+ *
+ * @param txPower Beacon's tx power.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setTxPower(int txPower) {
+ mBuilderParcel.txPower = txPower;
+ return this;
+ }
+
+ /**
+ * Set AppName.
+ *
+ * @param appName Human readable name of the app designated to open the uri.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setAppName(@Nullable String appName) {
+ mBuilderParcel.appName = appName;
+ return this;
+ }
+
+ /**
+ * Set PackageName.
+ *
+ * @param packageName Package name of the App that owns this item.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setPackageName(@Nullable String packageName) {
+ mBuilderParcel.packageName = packageName;
+ return this;
+ }
+
+ /**
+ * Set TriggerId.
+ *
+ * @param triggerId TriggerId identifies the trigger/beacon that is attached with a message.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setTriggerId(@Nullable String triggerId) {
+ mBuilderParcel.triggerId = triggerId;
+ return this;
+ }
+
+ /**
+ * Set IconPng.
+ *
+ * @param iconPng Bytes of item icon in PNG format displayed in Discovery item list.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setIconPng(@Nullable byte[] iconPng) {
+ mBuilderParcel.iconPng = iconPng;
+ return this;
+ }
+
+ /**
+ * Set IconFifeUrl.
+ *
+ * @param iconFifeUrl A FIFE URL of the item icon displayed in Discovery item list.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setIconFfeUrl(@Nullable String iconFifeUrl) {
+ mBuilderParcel.iconFifeUrl = iconFifeUrl;
+ return this;
+ }
+
+ /**
+ * Set authenticationPublicKeySecp256r1, which is same as AntiSpoof public key, see
+ * <a href="https://developers.google.com/nearby/fast-pair/spec#data_format">Data Format</a>
+ *
+ * @param authenticationPublicKeySecp256r1 64-byte Fast Pair device public key.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setAuthenticationPublicKeySecp256r1(
+ @Nullable byte[] authenticationPublicKeySecp256r1) {
+ mBuilderParcel.authenticationPublicKeySecp256r1 = authenticationPublicKeySecp256r1;
+ return this;
+ }
+
+ /**
+ * Build {@link FastPairDiscoveryItem} with the currently set configuration.
+ *
+ * @hide
+ */
+ @NonNull
+ public FastPairDiscoveryItem build() {
+ return new FastPairDiscoveryItem(mBuilderParcel);
+ }
+ }
+}
diff --git a/nearby/framework/java/android/nearby/FastPairEligibleAccount.java b/nearby/framework/java/android/nearby/FastPairEligibleAccount.java
new file mode 100644
index 0000000..8be4cca
--- /dev/null
+++ b/nearby/framework/java/android/nearby/FastPairEligibleAccount.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.accounts.Account;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.nearby.aidl.FastPairEligibleAccountParcel;
+
+/**
+ * Class for FastPairEligibleAccount and its builder.
+ *
+ * @hide
+ */
+public class FastPairEligibleAccount {
+
+ FastPairEligibleAccountParcel mAccountParcel;
+
+ FastPairEligibleAccount(FastPairEligibleAccountParcel accountParcel) {
+ this.mAccountParcel = accountParcel;
+ }
+
+ /**
+ * Get Account.
+ *
+ * @hide
+ */
+ @Nullable
+ public Account getAccount() {
+ return this.mAccountParcel.account;
+ }
+
+ /**
+ * Get OptIn Status.
+ *
+ * @hide
+ */
+ public boolean isOptIn() {
+ return this.mAccountParcel.optIn;
+ }
+
+ /**
+ * Builder used to create FastPairEligibleAccount.
+ *
+ * @hide
+ */
+ public static final class Builder {
+
+ private final FastPairEligibleAccountParcel mBuilderParcel;
+
+ /**
+ * Default constructor of Builder.
+ *
+ * @hide
+ */
+ public Builder() {
+ mBuilderParcel = new FastPairEligibleAccountParcel();
+ mBuilderParcel.account = null;
+ mBuilderParcel.optIn = false;
+ }
+
+ /**
+ * Set Account.
+ *
+ * @param account Fast Pair eligible account.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setAccount(@Nullable Account account) {
+ mBuilderParcel.account = account;
+ return this;
+ }
+
+ /**
+ * Set whether the account is opt into Fast Pair.
+ *
+ * @param optIn Whether the Fast Pair eligible account opts into Fast Pair.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ * @hide
+ */
+ @NonNull
+ public Builder setOptIn(boolean optIn) {
+ mBuilderParcel.optIn = optIn;
+ return this;
+ }
+
+ /**
+ * Build {@link FastPairEligibleAccount} with the currently set configuration.
+ *
+ * @hide
+ */
+ @NonNull
+ public FastPairEligibleAccount build() {
+ return new FastPairEligibleAccount(mBuilderParcel);
+ }
+ }
+}
diff --git a/nearby/framework/java/android/nearby/FastPairStatusCallback.java b/nearby/framework/java/android/nearby/FastPairStatusCallback.java
new file mode 100644
index 0000000..1567828
--- /dev/null
+++ b/nearby/framework/java/android/nearby/FastPairStatusCallback.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.NonNull;
+
+/**
+ * Reports the pair status for an ongoing pair with a {@link FastPairDevice}.
+ * @hide
+ */
+public interface FastPairStatusCallback {
+
+ /** Reports a pair status related metadata associated with a {@link FastPairDevice} */
+ void onPairUpdate(@NonNull FastPairDevice fastPairDevice,
+ PairStatusMetadata pairStatusMetadata);
+}
diff --git a/nearby/framework/java/android/nearby/IBroadcastListener.aidl b/nearby/framework/java/android/nearby/IBroadcastListener.aidl
new file mode 100644
index 0000000..98c7e17
--- /dev/null
+++ b/nearby/framework/java/android/nearby/IBroadcastListener.aidl
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+/**
+ * Callback when brodacast status changes.
+ *
+ * {@hide}
+ */
+oneway interface IBroadcastListener {
+ /** Called when the broadcast status changes. */
+ void onStatusChanged(int status);
+}
diff --git a/nearby/framework/java/android/nearby/IFastPairHalfSheetCallback.aidl b/nearby/framework/java/android/nearby/IFastPairHalfSheetCallback.aidl
new file mode 100644
index 0000000..2e6fc87
--- /dev/null
+++ b/nearby/framework/java/android/nearby/IFastPairHalfSheetCallback.aidl
@@ -0,0 +1,25 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package android.nearby;
+
+import android.content.Intent;
+/**
+ * Provides callback interface for halfsheet to send FastPair call back.
+ *
+ * {@hide}
+ */
+interface IFastPairHalfSheetCallback {
+ void onHalfSheetConnectionConfirm(in Intent intent);
+ }
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/INearbyManager.aidl b/nearby/framework/java/android/nearby/INearbyManager.aidl
new file mode 100644
index 0000000..62e109e
--- /dev/null
+++ b/nearby/framework/java/android/nearby/INearbyManager.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.nearby.IBroadcastListener;
+import android.nearby.IScanListener;
+import android.nearby.BroadcastRequestParcelable;
+import android.nearby.ScanRequest;
+
+/**
+ * Interface for communicating with the nearby services.
+ *
+ * @hide
+ */
+interface INearbyManager {
+
+ int registerScanListener(in ScanRequest scanRequest, in IScanListener listener);
+
+ void unregisterScanListener(in IScanListener listener);
+
+ void startBroadcast(in BroadcastRequestParcelable broadcastRequest,
+ in IBroadcastListener callback);
+
+ void stopBroadcast(in IBroadcastListener callback);
+}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/IScanListener.aidl b/nearby/framework/java/android/nearby/IScanListener.aidl
new file mode 100644
index 0000000..54033aa
--- /dev/null
+++ b/nearby/framework/java/android/nearby/IScanListener.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.nearby.NearbyDeviceParcelable;
+
+/**
+ * Binder callback for ScanCallback.
+ *
+ * {@hide}
+ */
+oneway interface IScanListener {
+ /** Reports a {@link NearbyDevice} being discovered. */
+ void onDiscovered(in NearbyDeviceParcelable nearbyDeviceParcelable);
+
+ /** Reports a {@link NearbyDevice} information(distance, packet, and etc) changed. */
+ void onUpdated(in NearbyDeviceParcelable nearbyDeviceParcelable);
+
+ /** Reports a {@link NearbyDevice} is no longer within range. */
+ void onLost(in NearbyDeviceParcelable nearbyDeviceParcelable);
+}
diff --git a/nearby/framework/java/android/nearby/NearbyDevice.java b/nearby/framework/java/android/nearby/NearbyDevice.java
new file mode 100644
index 0000000..538940c
--- /dev/null
+++ b/nearby/framework/java/android/nearby/NearbyDevice.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A class represents a device that can be discovered by multiple mediums.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class NearbyDevice {
+
+ @Nullable
+ private final String mName;
+
+ @Medium
+ private final List<Integer> mMediums;
+
+ private final int mRssi;
+
+ /**
+ * Creates a new NearbyDevice.
+ *
+ * @param name Local device name. Can be {@code null} if there is no name.
+ * @param mediums The {@link Medium}s over which the device is discovered.
+ * @param rssi The received signal strength in dBm.
+ * @hide
+ */
+ public NearbyDevice(@Nullable String name, List<Integer> mediums, int rssi) {
+ for (int medium : mediums) {
+ Preconditions.checkState(isValidMedium(medium),
+ "Not supported medium: " + medium
+ + ", scan medium must be one of NearbyDevice#Medium.");
+ }
+ mName = name;
+ mMediums = mediums;
+ mRssi = rssi;
+ }
+
+ static String mediumToString(@Medium int medium) {
+ switch (medium) {
+ case Medium.BLE:
+ return "BLE";
+ case Medium.BLUETOOTH:
+ return "Bluetooth Classic";
+ default:
+ return "Unknown";
+ }
+ }
+
+ /**
+ * True if the medium is defined in {@link Medium}.
+ *
+ * @param medium Integer that may represent a medium type.
+ */
+ public static boolean isValidMedium(@Medium int medium) {
+ return medium == Medium.BLE
+ || medium == Medium.BLUETOOTH;
+ }
+
+ /**
+ * The name of the device, or null if not available.
+ */
+ @Nullable
+ public String getName() {
+ return mName;
+ }
+
+ /** The medium over which this device was discovered. */
+ @NonNull
+ @Medium public List<Integer> getMediums() {
+ return mMediums;
+ }
+
+ /**
+ * Returns the received signal strength in dBm.
+ */
+ @IntRange(from = -127, to = 126)
+ public int getRssi() {
+ return mRssi;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("NearbyDevice [");
+ if (mName != null && !mName.isEmpty()) {
+ stringBuilder.append("name=").append(mName).append(", ");
+ }
+ stringBuilder.append("medium={");
+ for (int medium : mMediums) {
+ stringBuilder.append(mediumToString(medium));
+ }
+ stringBuilder.append("} rssi=").append(mRssi);
+ stringBuilder.append("]");
+ return stringBuilder.toString();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof NearbyDevice) {
+ NearbyDevice otherDevice = (NearbyDevice) other;
+ return Objects.equals(mName, otherDevice.mName)
+ && mMediums == otherDevice.mMediums
+ && mRssi == otherDevice.mRssi;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mName, mMediums, mRssi);
+ }
+
+ /**
+ * The medium where a NearbyDevice was discovered on.
+ *
+ * @hide
+ */
+ @IntDef({Medium.BLE, Medium.BLUETOOTH})
+ public @interface Medium {
+ int BLE = 1;
+ int BLUETOOTH = 2;
+ }
+}
+
diff --git a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.aidl b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.aidl
new file mode 100644
index 0000000..1a88181
--- /dev/null
+++ b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2012, 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;
+
+parcelable NearbyDeviceParcelable;
+
diff --git a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
new file mode 100644
index 0000000..1ad3571
--- /dev/null
+++ b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
@@ -0,0 +1,437 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A data class representing scan result from Nearby Service. Scan result can come from multiple
+ * mediums like BLE, Wi-Fi Aware, and etc. A scan result consists of An encapsulation of various
+ * parameters for requesting nearby scans.
+ *
+ * <p>All scan results generated through {@link NearbyManager} are guaranteed to have a valid
+ * medium, identifier, timestamp (both UTC time and elapsed real-time since boot), and accuracy. All
+ * other parameters are optional.
+ *
+ * @hide
+ */
+public final class NearbyDeviceParcelable implements Parcelable {
+
+ /** Used to read a NearbyDeviceParcelable from a Parcel. */
+ @NonNull
+ public static final Creator<NearbyDeviceParcelable> CREATOR =
+ new Creator<NearbyDeviceParcelable>() {
+ @Override
+ public NearbyDeviceParcelable createFromParcel(Parcel in) {
+ Builder builder = new Builder();
+ if (in.readInt() == 1) {
+ builder.setName(in.readString());
+ }
+ builder.setMedium(in.readInt());
+ builder.setTxPower(in.readInt());
+ builder.setRssi(in.readInt());
+ builder.setAction(in.readInt());
+ builder.setPublicCredential(
+ in.readParcelable(
+ PublicCredential.class.getClassLoader(),
+ PublicCredential.class));
+ if (in.readInt() == 1) {
+ builder.setFastPairModelId(in.readString());
+ }
+ if (in.readInt() == 1) {
+ builder.setBluetoothAddress(in.readString());
+ }
+ if (in.readInt() == 1) {
+ int dataLength = in.readInt();
+ byte[] data = new byte[dataLength];
+ in.readByteArray(data);
+ builder.setData(data);
+ }
+ return builder.build();
+ }
+
+ @Override
+ public NearbyDeviceParcelable[] newArray(int size) {
+ return new NearbyDeviceParcelable[size];
+ }
+ };
+
+ @ScanRequest.ScanType int mScanType;
+ @Nullable private final String mName;
+ @NearbyDevice.Medium private final int mMedium;
+ private final int mTxPower;
+ private final int mRssi;
+ private final int mAction;
+ private final PublicCredential mPublicCredential;
+ @Nullable private final String mBluetoothAddress;
+ @Nullable private final String mFastPairModelId;
+ @Nullable private final byte[] mData;
+
+ private NearbyDeviceParcelable(
+ @ScanRequest.ScanType int scanType,
+ @Nullable String name,
+ int medium,
+ int TxPower,
+ int rssi,
+ int action,
+ PublicCredential publicCredential,
+ @Nullable String fastPairModelId,
+ @Nullable String bluetoothAddress,
+ @Nullable byte[] data) {
+ mScanType = scanType;
+ mName = name;
+ mMedium = medium;
+ mTxPower = TxPower;
+ mRssi = rssi;
+ mAction = action;
+ mPublicCredential = publicCredential;
+ mFastPairModelId = fastPairModelId;
+ mBluetoothAddress = bluetoothAddress;
+ mData = data;
+ }
+
+ /** No special parcel contents. */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Flatten this NearbyDeviceParcelable in to a Parcel.
+ *
+ * @param dest The Parcel in which the object should be written.
+ * @param flags Additional flags about how the object should be written.
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mName == null ? 0 : 1);
+ if (mName != null) {
+ dest.writeString(mName);
+ }
+ dest.writeInt(mMedium);
+ dest.writeInt(mTxPower);
+ dest.writeInt(mRssi);
+ dest.writeInt(mAction);
+ dest.writeParcelable(mPublicCredential, flags);
+ dest.writeInt(mFastPairModelId == null ? 0 : 1);
+ if (mFastPairModelId != null) {
+ dest.writeString(mFastPairModelId);
+ }
+ dest.writeInt(mBluetoothAddress == null ? 0 : 1);
+ if (mBluetoothAddress != null) {
+ dest.writeString(mBluetoothAddress);
+ }
+ dest.writeInt(mData == null ? 0 : 1);
+ if (mData != null) {
+ dest.writeInt(mData.length);
+ dest.writeByteArray(mData);
+ }
+ }
+
+ /** Returns a string representation of this ScanRequest. */
+ @Override
+ public String toString() {
+ return "NearbyDeviceParcelable["
+ + "name="
+ + mName
+ + ", medium="
+ + NearbyDevice.mediumToString(mMedium)
+ + ", txPower="
+ + mTxPower
+ + ", rssi="
+ + mRssi
+ + ", action="
+ + mAction
+ + ", bluetoothAddress="
+ + mBluetoothAddress
+ + ", fastPairModelId="
+ + mFastPairModelId
+ + ", data="
+ + Arrays.toString(mData)
+ + "]";
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof NearbyDeviceParcelable) {
+ NearbyDeviceParcelable otherNearbyDeviceParcelable = (NearbyDeviceParcelable) other;
+ return Objects.equals(mName, otherNearbyDeviceParcelable.mName)
+ && (mMedium == otherNearbyDeviceParcelable.mMedium)
+ && (mTxPower == otherNearbyDeviceParcelable.mTxPower)
+ && (mRssi == otherNearbyDeviceParcelable.mRssi)
+ && (mAction == otherNearbyDeviceParcelable.mAction)
+ && (Objects.equals(
+ mPublicCredential, otherNearbyDeviceParcelable.mPublicCredential))
+ && (Objects.equals(
+ mBluetoothAddress, otherNearbyDeviceParcelable.mBluetoothAddress))
+ && (Objects.equals(
+ mFastPairModelId, otherNearbyDeviceParcelable.mFastPairModelId))
+ && (Arrays.equals(mData, otherNearbyDeviceParcelable.mData));
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mName,
+ mMedium,
+ mRssi,
+ mAction,
+ mPublicCredential.hashCode(),
+ mBluetoothAddress,
+ mFastPairModelId,
+ Arrays.hashCode(mData));
+ }
+
+ /**
+ * Returns the type of the scan.
+ *
+ * @hide
+ */
+ @ScanRequest.ScanType
+ public int getScanType() {
+ return mScanType;
+ }
+
+ /** Gets the name of the NearbyDeviceParcelable. Returns {@code null} If there is no name. */
+ @Nullable
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Gets the {@link android.nearby.NearbyDevice.Medium} of the NearbyDeviceParcelable over which
+ * it is discovered.
+ */
+ @NearbyDevice.Medium
+ public int getMedium() {
+ return mMedium;
+ }
+
+ /**
+ * Gets the transmission power in dBm.
+ *
+ * @hide
+ */
+ @IntRange(from = -127, to = 126)
+ public int getTxPower() {
+ return mTxPower;
+ }
+
+ /** Gets the received signal strength in dBm. */
+ @IntRange(from = -127, to = 126)
+ public int getRssi() {
+ return mRssi;
+ }
+
+ /**
+ * Gets the Action.
+ *
+ * @hide
+ */
+ @IntRange(from = -127, to = 126)
+ public int getAction() {
+ return mAction;
+ }
+
+ /**
+ * Gets the public credential.
+ *
+ * @hide
+ */
+ @NonNull
+ public PublicCredential getPublicCredential() {
+ return mPublicCredential;
+ }
+
+ /**
+ * Gets the Fast Pair identifier. Returns {@code null} if there is no Model ID or this is not a
+ * Fast Pair device.
+ */
+ @Nullable
+ public String getFastPairModelId() {
+ return mFastPairModelId;
+ }
+
+ /**
+ * Gets the Bluetooth device hardware address. Returns {@code null} if the device is not
+ * discovered by Bluetooth.
+ */
+ @Nullable
+ public String getBluetoothAddress() {
+ return mBluetoothAddress;
+ }
+
+ /** Gets the raw data from the scanning. Returns {@code null} if there is no extra data. */
+ @Nullable
+ public byte[] getData() {
+ return mData;
+ }
+
+ /** Builder class for {@link NearbyDeviceParcelable}. */
+ public static final class Builder {
+ @Nullable private String mName;
+ @NearbyDevice.Medium private int mMedium;
+ private int mTxPower;
+ private int mRssi;
+ private int mAction;
+ private PublicCredential mPublicCredential;
+ @ScanRequest.ScanType int mScanType;
+ @Nullable private String mFastPairModelId;
+ @Nullable private String mBluetoothAddress;
+ @Nullable private byte[] mData;
+
+ /**
+ * Sets the scan type of the NearbyDeviceParcelable.
+ *
+ * @hide
+ */
+ public Builder setScanType(@ScanRequest.ScanType int scanType) {
+ mScanType = scanType;
+ return this;
+ }
+
+ /**
+ * Sets the name of the scanned device.
+ *
+ * @param name The local name of the scanned device.
+ */
+ @NonNull
+ public Builder setName(@Nullable String name) {
+ mName = name;
+ return this;
+ }
+
+ /**
+ * Sets the medium over which the device is discovered.
+ *
+ * @param medium The {@link NearbyDevice.Medium} over which the device is discovered.
+ */
+ @NonNull
+ public Builder setMedium(@NearbyDevice.Medium int medium) {
+ mMedium = medium;
+ return this;
+ }
+
+ /**
+ * Sets the transmission power of the discovered device.
+ *
+ * @param txPower The transmission power in dBm.
+ * @hide
+ */
+ @NonNull
+ public Builder setTxPower(int txPower) {
+ mTxPower = txPower;
+ return this;
+ }
+
+ /**
+ * Sets the RSSI between scanned device and the discovered device.
+ *
+ * @param rssi The received signal strength in dBm.
+ */
+ @NonNull
+ public Builder setRssi(@IntRange(from = -127, to = 126) int rssi) {
+ mRssi = rssi;
+ return this;
+ }
+
+ /**
+ * Sets the action from the discovered device.
+ *
+ * @param action The action of the discovered device.
+ * @hide
+ */
+ @NonNull
+ public Builder setAction(int action) {
+ mAction = action;
+ return this;
+ }
+
+ /**
+ * Sets the public credential of the discovered device.
+ *
+ * @param publicCredential The public credential.
+ * @hide
+ */
+ @NonNull
+ public Builder setPublicCredential(@NonNull PublicCredential publicCredential) {
+ mPublicCredential = publicCredential;
+ return this;
+ }
+
+ /**
+ * Sets the Fast Pair model Id.
+ *
+ * @param fastPairModelId Fast Pair device identifier.
+ */
+ @NonNull
+ public Builder setFastPairModelId(@Nullable String fastPairModelId) {
+ mFastPairModelId = fastPairModelId;
+ return this;
+ }
+
+ /**
+ * Sets the bluetooth address.
+ *
+ * @param bluetoothAddress The hardware address of the bluetooth device.
+ */
+ @NonNull
+ public Builder setBluetoothAddress(@Nullable String bluetoothAddress) {
+ mBluetoothAddress = bluetoothAddress;
+ return this;
+ }
+
+ /**
+ * Sets the scanned raw data.
+ *
+ * @param data Data the scan. For example, {@link ScanRecord#getServiceData()} if scanned by
+ * Bluetooth.
+ */
+ @NonNull
+ public Builder setData(@Nullable byte[] data) {
+ mData = data;
+ return this;
+ }
+
+ /** Builds a ScanResult. */
+ @NonNull
+ public NearbyDeviceParcelable build() {
+ return new NearbyDeviceParcelable(
+ mScanType,
+ mName,
+ mMedium,
+ mTxPower,
+ mRssi,
+ mAction,
+ mPublicCredential,
+ mFastPairModelId,
+ mBluetoothAddress,
+ mData);
+ }
+ }
+}
diff --git a/nearby/framework/java/android/nearby/NearbyFrameworkInitializer.java b/nearby/framework/java/android/nearby/NearbyFrameworkInitializer.java
new file mode 100644
index 0000000..3780fbb
--- /dev/null
+++ b/nearby/framework/java/android/nearby/NearbyFrameworkInitializer.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.SystemApi;
+import android.app.SystemServiceRegistry;
+import android.content.Context;
+
+/**
+ * Class for performing registration for all Nearby services.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public final class NearbyFrameworkInitializer {
+
+ private NearbyFrameworkInitializer() {}
+
+ /**
+ * Called by {@link SystemServiceRegistry}'s static initializer and registers all
+ * Nearby services to {@link Context}, so that {@link Context#getSystemService} can return them.
+ *
+ * @throws IllegalStateException if this is called from anywhere besides
+ * {@link SystemServiceRegistry}
+ */
+ public static void registerServiceWrappers() {
+ SystemServiceRegistry.registerContextAwareService(
+ Context.NEARBY_SERVICE,
+ NearbyManager.class,
+ (context, serviceBinder) -> {
+ INearbyManager service = INearbyManager.Stub.asInterface(serviceBinder);
+ return new NearbyManager(service);
+ }
+ );
+ }
+}
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
new file mode 100644
index 0000000..3dd08da
--- /dev/null
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.Manifest;
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.RemoteException;
+import android.provider.Settings;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import java.lang.ref.WeakReference;
+import java.util.Objects;
+import java.util.WeakHashMap;
+import java.util.concurrent.Executor;
+
+/**
+ * This class provides a way to perform Nearby related operations such as scanning, broadcasting
+ * and connecting to nearby devices.
+ *
+ * <p> To get a {@link NearbyManager} instance, call the
+ * <code>Context.getSystemService(NearbyManager.class)</code>.
+ *
+ * @hide
+ */
+@SystemApi
+@SystemService(Context.NEARBY_SERVICE)
+public class NearbyManager {
+
+ /**
+ * Represents the scanning state.
+ *
+ * @hide
+ */
+ @IntDef({
+ ScanStatus.UNKNOWN,
+ ScanStatus.SUCCESS,
+ ScanStatus.ERROR,
+ })
+ public @interface ScanStatus {
+ // Default, invalid state.
+ int UNKNOWN = 0;
+ // The successful state.
+ int SUCCESS = 1;
+ // Failed state.
+ int ERROR = 2;
+ }
+
+ /**
+ * Whether allows Fast Pair to scan.
+ *
+ * (0 = disabled, 1 = enabled)
+ *
+ * @hide
+ */
+ public static final String FAST_PAIR_SCAN_ENABLED = "fast_pair_scan_enabled";
+
+ @GuardedBy("sScanListeners")
+ private static final WeakHashMap<ScanCallback, WeakReference<ScanListenerTransport>>
+ sScanListeners = new WeakHashMap<>();
+ @GuardedBy("sBroadcastListeners")
+ private static final WeakHashMap<BroadcastCallback, WeakReference<BroadcastListenerTransport>>
+ sBroadcastListeners = new WeakHashMap<>();
+
+ private final INearbyManager mService;
+
+ /**
+ * Creates a new NearbyManager.
+ *
+ * @param service the service object
+ */
+ NearbyManager(@NonNull INearbyManager service) {
+ mService = service;
+ }
+
+ private static NearbyDevice toClientNearbyDevice(
+ NearbyDeviceParcelable nearbyDeviceParcelable,
+ @ScanRequest.ScanType int scanType) {
+ if (scanType == ScanRequest.SCAN_TYPE_FAST_PAIR) {
+ return new FastPairDevice.Builder()
+ .setName(nearbyDeviceParcelable.getName())
+ .addMedium(nearbyDeviceParcelable.getMedium())
+ .setRssi(nearbyDeviceParcelable.getRssi())
+ .setTxPower(nearbyDeviceParcelable.getTxPower())
+ .setModelId(nearbyDeviceParcelable.getFastPairModelId())
+ .setBluetoothAddress(nearbyDeviceParcelable.getBluetoothAddress())
+ .setData(nearbyDeviceParcelable.getData()).build();
+ }
+ return null;
+ }
+
+ /**
+ * Start scan for nearby devices with given parameters. Devices matching {@link ScanRequest}
+ * will be delivered through the given callback.
+ *
+ * @param scanRequest various parameters clients send when requesting scanning
+ * @param executor executor where the listener method is called
+ * @param scanCallback the callback to notify clients when there is a scan result
+ *
+ * @return whether scanning was successfully started
+ */
+ @RequiresPermission(allOf = {android.Manifest.permission.BLUETOOTH_SCAN,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED})
+ @ScanStatus
+ public int startScan(@NonNull ScanRequest scanRequest,
+ @CallbackExecutor @NonNull Executor executor,
+ @NonNull ScanCallback scanCallback) {
+ Objects.requireNonNull(scanRequest, "scanRequest must not be null");
+ Objects.requireNonNull(scanCallback, "scanCallback must not be null");
+ Objects.requireNonNull(executor, "executor must not be null");
+
+ try {
+ synchronized (sScanListeners) {
+ WeakReference<ScanListenerTransport> reference = sScanListeners.get(scanCallback);
+ ScanListenerTransport transport = reference != null ? reference.get() : null;
+ if (transport == null) {
+ transport = new ScanListenerTransport(scanRequest.getScanType(), scanCallback,
+ executor);
+ } else {
+ Preconditions.checkState(transport.isRegistered());
+ transport.setExecutor(executor);
+ }
+ @ScanStatus int status = mService.registerScanListener(scanRequest, transport);
+ if (status != ScanStatus.SUCCESS) {
+ return status;
+ }
+ sScanListeners.put(scanCallback, new WeakReference<>(transport));
+ return ScanStatus.SUCCESS;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Stops the nearby device scan for the specified callback. The given callback
+ * is guaranteed not to receive any invocations that happen after this method
+ * is invoked.
+ *
+ * Suppressed lint: Registration methods should have overload that accepts delivery Executor.
+ * Already have executor in startScan() method.
+ *
+ * @param scanCallback the callback that was used to start the scan
+ */
+ @SuppressLint("ExecutorRegistration")
+ @RequiresPermission(allOf = {android.Manifest.permission.BLUETOOTH_SCAN,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED})
+ public void stopScan(@NonNull ScanCallback scanCallback) {
+ Preconditions.checkArgument(scanCallback != null,
+ "invalid null scanCallback");
+ try {
+ synchronized (sScanListeners) {
+ WeakReference<ScanListenerTransport> reference = sScanListeners.remove(
+ scanCallback);
+ ScanListenerTransport transport = reference != null ? reference.get() : null;
+ if (transport != null) {
+ transport.unregister();
+ mService.unregisterScanListener(transport);
+ }
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Start broadcasting the request using nearby specification.
+ *
+ * @param broadcastRequest request for the nearby broadcast
+ * @param executor executor for running the callback
+ * @param callback callback for notifying the client
+ */
+ @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADVERTISE,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED})
+ public void startBroadcast(@NonNull BroadcastRequest broadcastRequest,
+ @CallbackExecutor @NonNull Executor executor, @NonNull BroadcastCallback callback) {
+ try {
+ synchronized (sBroadcastListeners) {
+ WeakReference<BroadcastListenerTransport> reference = sBroadcastListeners.get(
+ callback);
+ BroadcastListenerTransport transport = reference != null ? reference.get() : null;
+ if (transport == null) {
+ transport = new BroadcastListenerTransport(callback, executor);
+ } else {
+ Preconditions.checkState(transport.isRegistered());
+ transport.setExecutor(executor);
+ }
+ mService.startBroadcast(new BroadcastRequestParcelable(broadcastRequest),
+ transport);
+ sBroadcastListeners.put(callback, new WeakReference<>(transport));
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Stop the broadcast associated with the given callback.
+ *
+ * @param callback the callback that was used for starting the broadcast
+ */
+ @SuppressLint("ExecutorRegistration")
+ @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADVERTISE,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED})
+ public void stopBroadcast(@NonNull BroadcastCallback callback) {
+ try {
+ synchronized (sBroadcastListeners) {
+ WeakReference<BroadcastListenerTransport> reference = sBroadcastListeners.remove(
+ callback);
+ BroadcastListenerTransport transport = reference != null ? reference.get() : null;
+ if (transport != null) {
+ transport.unregister();
+ mService.stopBroadcast(transport);
+ }
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Read from {@link Settings} whether Fast Pair scan is enabled.
+ *
+ * @param context the {@link Context} to query the setting
+ * @return whether the Fast Pair is enabled
+ * @hide
+ */
+ public static boolean getFastPairScanEnabled(@NonNull Context context) {
+ final int enabled = Settings.Secure.getInt(
+ context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, 0);
+ return enabled != 0;
+ }
+
+ /**
+ * Write into {@link Settings} whether Fast Pair scan is enabled
+ *
+ * @param context the {@link Context} to set the setting
+ * @param enable whether the Fast Pair scan should be enabled
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ public static void setFastPairScanEnabled(@NonNull Context context, boolean enable) {
+ Settings.Secure.putInt(
+ context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, enable ? 1 : 0);
+ }
+
+ private static class ScanListenerTransport extends IScanListener.Stub {
+
+ private @ScanRequest.ScanType int mScanType;
+ private volatile @Nullable ScanCallback mScanCallback;
+ private Executor mExecutor;
+
+ ScanListenerTransport(@ScanRequest.ScanType int scanType, ScanCallback scanCallback,
+ @CallbackExecutor Executor executor) {
+ Preconditions.checkArgument(scanCallback != null,
+ "invalid null callback");
+ Preconditions.checkState(ScanRequest.isValidScanType(scanType),
+ "invalid scan type : " + scanType
+ + ", scan type must be one of ScanRequest#SCAN_TYPE_");
+ mScanType = scanType;
+ mScanCallback = scanCallback;
+ mExecutor = executor;
+ }
+
+ void setExecutor(Executor executor) {
+ Preconditions.checkArgument(
+ executor != null, "invalid null executor");
+ mExecutor = executor;
+ }
+
+ boolean isRegistered() {
+ return mScanCallback != null;
+ }
+
+ void unregister() {
+ mScanCallback = null;
+ }
+
+ @Override
+ public void onDiscovered(NearbyDeviceParcelable nearbyDeviceParcelable)
+ throws RemoteException {
+ mExecutor.execute(() -> {
+ if (mScanCallback != null) {
+ mScanCallback.onDiscovered(
+ toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
+ }
+ });
+ }
+
+ @Override
+ public void onUpdated(NearbyDeviceParcelable nearbyDeviceParcelable)
+ throws RemoteException {
+ mExecutor.execute(() -> {
+ if (mScanCallback != null) {
+ mScanCallback.onUpdated(
+ toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
+ }
+ });
+ }
+
+ @Override
+ public void onLost(NearbyDeviceParcelable nearbyDeviceParcelable) throws RemoteException {
+ mExecutor.execute(() -> {
+ if (mScanCallback != null) {
+ mScanCallback.onLost(
+ toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
+ }
+ });
+ }
+ }
+
+ private static class BroadcastListenerTransport extends IBroadcastListener.Stub {
+ private volatile @Nullable BroadcastCallback mBroadcastCallback;
+ private Executor mExecutor;
+
+ BroadcastListenerTransport(BroadcastCallback broadcastCallback,
+ @CallbackExecutor Executor executor) {
+ mBroadcastCallback = broadcastCallback;
+ mExecutor = executor;
+ }
+
+ void setExecutor(Executor executor) {
+ Preconditions.checkArgument(
+ executor != null, "invalid null executor");
+ mExecutor = executor;
+ }
+
+ boolean isRegistered() {
+ return mBroadcastCallback != null;
+ }
+
+ void unregister() {
+ mBroadcastCallback = null;
+ }
+
+ @Override
+ public void onStatusChanged(int status) {
+ mExecutor.execute(() -> {
+ if (mBroadcastCallback != null) {
+ mBroadcastCallback.onStatusChanged(status);
+ }
+ });
+ }
+ }
+}
diff --git a/nearby/framework/java/android/nearby/PairStatusMetadata.aidl b/nearby/framework/java/android/nearby/PairStatusMetadata.aidl
new file mode 100644
index 0000000..911a300
--- /dev/null
+++ b/nearby/framework/java/android/nearby/PairStatusMetadata.aidl
@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+/**
+ * Metadata about an ongoing paring. Wraps transient data like status and progress.
+ *
+ * @hide
+ */
+parcelable PairStatusMetadata;
diff --git a/nearby/framework/java/android/nearby/PairStatusMetadata.java b/nearby/framework/java/android/nearby/PairStatusMetadata.java
new file mode 100644
index 0000000..438cd6b
--- /dev/null
+++ b/nearby/framework/java/android/nearby/PairStatusMetadata.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Metadata about an ongoing paring. Wraps transient data like status and progress.
+ *
+ * @hide
+ */
+public final class PairStatusMetadata implements Parcelable {
+
+ @Status
+ private final int mStatus;
+
+ /** The status of the pairing. */
+ @IntDef({
+ Status.UNKNOWN,
+ Status.SUCCESS,
+ Status.FAIL,
+ Status.DISMISS
+ })
+ public @interface Status {
+ int UNKNOWN = 1000;
+ int SUCCESS = 1001;
+ int FAIL = 1002;
+ int DISMISS = 1003;
+ }
+
+ /** Converts the status to readable string. */
+ public static String statusToString(@Status int status) {
+ switch (status) {
+ case Status.SUCCESS:
+ return "SUCCESS";
+ case Status.FAIL:
+ return "FAIL";
+ case Status.DISMISS:
+ return "DISMISS";
+ case Status.UNKNOWN:
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ public int getStatus() {
+ return mStatus;
+ }
+
+ @Override
+ public String toString() {
+ return "PairStatusMetadata[ status=" + statusToString(mStatus) + "]";
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof PairStatusMetadata) {
+ return mStatus == ((PairStatusMetadata) other).mStatus;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mStatus);
+ }
+
+ public PairStatusMetadata(@Status int status) {
+ mStatus = status;
+ }
+
+ public static final Creator<PairStatusMetadata> CREATOR = new Creator<PairStatusMetadata>() {
+ @Override
+ public PairStatusMetadata createFromParcel(Parcel in) {
+ return new PairStatusMetadata(in.readInt());
+ }
+
+ @Override
+ public PairStatusMetadata[] newArray(int size) {
+ return new PairStatusMetadata[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public int getStability() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mStatus);
+ }
+}
diff --git a/nearby/framework/java/android/nearby/PresenceBroadcastRequest.java b/nearby/framework/java/android/nearby/PresenceBroadcastRequest.java
new file mode 100644
index 0000000..d01be06
--- /dev/null
+++ b/nearby/framework/java/android/nearby/PresenceBroadcastRequest.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Request for Nearby Presence Broadcast.
+ *
+ * @hide
+ */
+@SystemApi
+public final class PresenceBroadcastRequest extends BroadcastRequest implements Parcelable {
+ private final byte[] mSalt;
+ private final List<Integer> mActions;
+ private final PrivateCredential mCredential;
+ private final List<DataElement> mExtendedProperties;
+
+ private PresenceBroadcastRequest(@BroadcastVersion int version, int txPower,
+ List<Integer> mediums, byte[] salt, List<Integer> actions,
+ PrivateCredential credential, List<DataElement> extendedProperties) {
+ super(BROADCAST_TYPE_NEARBY_PRESENCE, version, txPower, mediums);
+ mSalt = salt;
+ mActions = actions;
+ mCredential = credential;
+ mExtendedProperties = extendedProperties;
+ }
+
+ private PresenceBroadcastRequest(Parcel in) {
+ super(BROADCAST_TYPE_NEARBY_PRESENCE, in);
+ mSalt = new byte[in.readInt()];
+ in.readByteArray(mSalt);
+
+ mActions = new ArrayList<>();
+ in.readList(mActions, Integer.class.getClassLoader(), Integer.class);
+ mCredential = in.readParcelable(PrivateCredential.class.getClassLoader(),
+ PrivateCredential.class);
+ mExtendedProperties = new ArrayList<>();
+ in.readList(mExtendedProperties, DataElement.class.getClassLoader(), DataElement.class);
+ }
+
+ @NonNull
+ public static final Creator<PresenceBroadcastRequest> CREATOR =
+ new Creator<PresenceBroadcastRequest>() {
+ @Override
+ public PresenceBroadcastRequest createFromParcel(Parcel in) {
+ // Skip Broadcast request type - it's used by parent class.
+ in.readInt();
+ return createFromParcelBody(in);
+ }
+
+ @Override
+ public PresenceBroadcastRequest[] newArray(int size) {
+ return new PresenceBroadcastRequest[size];
+ }
+ };
+
+ static PresenceBroadcastRequest createFromParcelBody(Parcel in) {
+ return new PresenceBroadcastRequest(in);
+ }
+
+ /**
+ * Returns the salt associated with this broadcast request.
+ */
+ @NonNull
+ public byte[] getSalt() {
+ return mSalt;
+ }
+
+ /**
+ * Returns actions associated with this broadcast request.
+ */
+ @NonNull
+ public List<Integer> getActions() {
+ return mActions;
+ }
+
+ /**
+ * Returns the private credential associated with this broadcast request.
+ */
+ @NonNull
+ public PrivateCredential getCredential() {
+ return mCredential;
+ }
+
+ /**
+ * Returns extended property information associated with this broadcast request.
+ */
+ @NonNull
+ public List<DataElement> getExtendedProperties() {
+ return mExtendedProperties;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(mSalt.length);
+ dest.writeByteArray(mSalt);
+ dest.writeList(mActions);
+ dest.writeParcelable(mCredential, /** parcelableFlags= */0);
+ dest.writeList(mExtendedProperties);
+ }
+
+ /**
+ * Builder for {@link PresenceBroadcastRequest}.
+ */
+ public static final class Builder {
+ private final List<Integer> mMediums;
+ private final List<Integer> mActions;
+ private final List<DataElement> mExtendedProperties;
+ private final byte[] mSalt;
+ private final PrivateCredential mCredential;
+
+ private int mVersion;
+ private int mTxPower;
+
+ public Builder(@NonNull List<Integer> mediums, @NonNull byte[] salt,
+ @NonNull PrivateCredential credential) {
+ Preconditions.checkState(!mediums.isEmpty(), "mediums cannot be empty");
+ Preconditions.checkState(salt != null && salt.length > 0, "salt cannot be empty");
+
+ mVersion = PRESENCE_VERSION_V0;
+ mTxPower = UNKNOWN_TX_POWER;
+ mCredential = credential;
+ mActions = new ArrayList<>();
+ mExtendedProperties = new ArrayList<>();
+
+ mSalt = salt;
+ mMediums = mediums;
+ }
+
+ /**
+ * Sets the version for this request.
+ */
+ @NonNull
+ public Builder setVersion(@BroadcastVersion int version) {
+ mVersion = version;
+ return this;
+ }
+
+ /**
+ * Sets the calibrated tx power level in dBm for this request. The tx power level should
+ * be between -127 dBm and 126 dBm.
+ */
+ @NonNull
+ public Builder setTxPower(@IntRange(from = -127, to = 126) int txPower) {
+ mTxPower = txPower;
+ return this;
+ }
+
+ /**
+ * Adds an action for the presence broadcast request.
+ */
+ @NonNull
+ public Builder addAction(@IntRange(from = 1, to = 255) int action) {
+ mActions.add(action);
+ return this;
+ }
+
+ /**
+ * Adds an extended property for the presence broadcast request.
+ */
+ @NonNull
+ public Builder addExtendedProperty(@NonNull DataElement dataElement) {
+ Objects.requireNonNull(dataElement);
+ mExtendedProperties.add(dataElement);
+ return this;
+ }
+
+ /**
+ * Builds a {@link PresenceBroadcastRequest}.
+ */
+ @NonNull
+ public PresenceBroadcastRequest build() {
+ return new PresenceBroadcastRequest(mVersion, mTxPower, mMediums, mSalt, mActions,
+ mCredential, mExtendedProperties);
+ }
+ }
+}
diff --git a/nearby/framework/java/android/nearby/PresenceCredential.java b/nearby/framework/java/android/nearby/PresenceCredential.java
new file mode 100644
index 0000000..0a3cc1d
--- /dev/null
+++ b/nearby/framework/java/android/nearby/PresenceCredential.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a credential for Nearby Presence.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class PresenceCredential {
+ /** Private credential type. */
+ public static final int CREDENTIAL_TYPE_PRIVATE = 0;
+
+ /** Public credential type. */
+ public static final int CREDENTIAL_TYPE_PUBLIC = 1;
+
+ /**
+ * @hide *
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({CREDENTIAL_TYPE_PUBLIC, CREDENTIAL_TYPE_PRIVATE})
+ public @interface CredentialType {}
+
+ /** Unknown identity type. */
+ public static final int IDENTITY_TYPE_UNKNOWN = 0;
+
+ /** Private identity type. */
+ public static final int IDENTITY_TYPE_PRIVATE = 1;
+ /** Provisioned identity type. */
+ public static final int IDENTITY_TYPE_PROVISIONED = 2;
+ /** Trusted identity type. */
+ public static final int IDENTITY_TYPE_TRUSTED = 3;
+
+ /**
+ * @hide *
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ IDENTITY_TYPE_UNKNOWN,
+ IDENTITY_TYPE_PRIVATE,
+ IDENTITY_TYPE_PROVISIONED,
+ IDENTITY_TYPE_TRUSTED
+ })
+ public @interface IdentityType {}
+
+ private final @CredentialType int mType;
+ private final @IdentityType int mIdentityType;
+ private final byte[] mSecretId;
+ private final byte[] mAuthenticityKey;
+ private final List<CredentialElement> mCredentialElements;
+
+ PresenceCredential(
+ @CredentialType int type,
+ @IdentityType int identityType,
+ byte[] secretId,
+ byte[] authenticityKey,
+ List<CredentialElement> credentialElements) {
+ mType = type;
+ mIdentityType = identityType;
+ mSecretId = secretId;
+ mAuthenticityKey = authenticityKey;
+ mCredentialElements = credentialElements;
+ }
+
+ PresenceCredential(@CredentialType int type, Parcel in) {
+ mType = type;
+ mIdentityType = in.readInt();
+ mSecretId = new byte[in.readInt()];
+ in.readByteArray(mSecretId);
+ mAuthenticityKey = new byte[in.readInt()];
+ in.readByteArray(mAuthenticityKey);
+ mCredentialElements = new ArrayList<>();
+ in.readList(
+ mCredentialElements,
+ CredentialElement.class.getClassLoader(),
+ CredentialElement.class);
+ }
+
+ /** Returns the type of the credential. */
+ public @CredentialType int getType() {
+ return mType;
+ }
+
+ /** Returns the identity type of the credential. */
+ public @IdentityType int getIdentityType() {
+ return mIdentityType;
+ }
+
+ /** Returns the secret id of the credential. */
+ @NonNull
+ public byte[] getSecretId() {
+ return mSecretId;
+ }
+
+ /** Returns the authenticity key of the credential. */
+ @NonNull
+ public byte[] getAuthenticityKey() {
+ return mAuthenticityKey;
+ }
+
+ /** Returns the elements of the credential. */
+ @NonNull
+ public List<CredentialElement> getCredentialElements() {
+ return mCredentialElements;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof PresenceCredential) {
+ PresenceCredential that = (PresenceCredential) obj;
+ return mType == that.mType
+ && mIdentityType == that.mIdentityType
+ && Arrays.equals(mSecretId, that.mSecretId)
+ && Arrays.equals(mAuthenticityKey, that.mAuthenticityKey)
+ && mCredentialElements.equals(that.mCredentialElements);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mType,
+ mIdentityType,
+ Arrays.hashCode(mSecretId),
+ Arrays.hashCode(mAuthenticityKey),
+ mCredentialElements.hashCode());
+ }
+
+ /**
+ * Writes the presence credential to the parcel.
+ *
+ * @hide
+ */
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mType);
+ dest.writeInt(mIdentityType);
+ dest.writeInt(mSecretId.length);
+ dest.writeByteArray(mSecretId);
+ dest.writeInt(mAuthenticityKey.length);
+ dest.writeByteArray(mAuthenticityKey);
+ dest.writeList(mCredentialElements);
+ }
+}
diff --git a/nearby/framework/java/android/nearby/PresenceDevice.java b/nearby/framework/java/android/nearby/PresenceDevice.java
new file mode 100644
index 0000000..12fc2a3
--- /dev/null
+++ b/nearby/framework/java/android/nearby/PresenceDevice.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a Presence device from nearby scans.
+ *
+ * @hide
+ */
+@SystemApi
+public final class PresenceDevice extends NearbyDevice implements Parcelable {
+
+ /** The type of presence device. */
+ /** @hide **/
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ DeviceType.UNKNOWN,
+ DeviceType.PHONE,
+ DeviceType.TABLET,
+ DeviceType.DISPLAY,
+ DeviceType.LAPTOP,
+ DeviceType.TV,
+ DeviceType.WATCH,
+ })
+ public @interface DeviceType {
+ /** The type of the device is unknown. */
+ int UNKNOWN = 0;
+ /** The device is a phone. */
+ int PHONE = 1;
+ /** The device is a tablet. */
+ int TABLET = 2;
+ /** The device is a display. */
+ int DISPLAY = 3;
+ /** The device is a laptop. */
+ int LAPTOP = 4;
+ /** The device is a TV. */
+ int TV = 5;
+ /** The device is a watch. */
+ int WATCH = 6;
+ }
+
+ private final String mDeviceId;
+ private final byte[] mSalt;
+ private final byte[] mSecretId;
+ private final byte[] mEncryptedIdentity;
+ private final int mDeviceType;
+ private final String mDeviceImageUrl;
+ private final long mDiscoveryTimestampMillis;
+ private final List<DataElement> mExtendedProperties;
+
+ /**
+ * The id of the device.
+ *
+ * <p>This id is not a hardware id. It may rotate based on the remote device's broadcasts.
+ */
+ @NonNull
+ public String getDeviceId() {
+ return mDeviceId;
+ }
+
+ /**
+ * Returns the salt used when presence device is discovered.
+ */
+ @NonNull
+ public byte[] getSalt() {
+ return mSalt;
+ }
+
+ /**
+ * Returns the secret used when presence device is discovered.
+ */
+ @NonNull
+ public byte[] getSecretId() {
+ return mSecretId;
+ }
+
+ /**
+ * Returns the encrypted identity used when presence device is discovered.
+ */
+ @NonNull
+ public byte[] getEncryptedIdentity() {
+ return mEncryptedIdentity;
+ }
+
+ /** The type of the device. */
+ @DeviceType
+ public int getDeviceType() {
+ return mDeviceType;
+ }
+
+ /** An image URL representing the device. */
+ @Nullable
+ public String getDeviceImageUrl() {
+ return mDeviceImageUrl;
+ }
+
+ /** The timestamp (since boot) when the device is discovered. */
+ public long getDiscoveryTimestampMillis() {
+ return mDiscoveryTimestampMillis;
+ }
+
+ /**
+ * The extended properties of the device.
+ */
+ @NonNull
+ public List<DataElement> getExtendedProperties() {
+ return mExtendedProperties;
+ }
+
+ private PresenceDevice(String deviceName, List<Integer> mMediums, int rssi, String deviceId,
+ byte[] salt, byte[] secretId, byte[] encryptedIdentity, int deviceType,
+ String deviceImageUrl, long discoveryTimestampMillis,
+ List<DataElement> extendedProperties) {
+ super(deviceName, mMediums, rssi);
+ mDeviceId = deviceId;
+ mSalt = salt;
+ mSecretId = secretId;
+ mEncryptedIdentity = encryptedIdentity;
+ mDeviceType = deviceType;
+ mDeviceImageUrl = deviceImageUrl;
+ mDiscoveryTimestampMillis = discoveryTimestampMillis;
+ mExtendedProperties = extendedProperties;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ String name = getName();
+ dest.writeInt(name == null ? 0 : 1);
+ if (name != null) {
+ dest.writeString(name);
+ }
+ List<Integer> mediums = getMediums();
+ dest.writeInt(mediums.size());
+ for (int medium : mediums) {
+ dest.writeInt(medium);
+ }
+ dest.writeInt(getRssi());
+ dest.writeInt(mSalt.length);
+ dest.writeByteArray(mSalt);
+ dest.writeInt(mSecretId.length);
+ dest.writeByteArray(mSecretId);
+ dest.writeInt(mEncryptedIdentity.length);
+ dest.writeByteArray(mEncryptedIdentity);
+ dest.writeString(mDeviceId);
+ dest.writeInt(mDeviceType);
+ dest.writeInt(mDeviceImageUrl == null ? 0 : 1);
+ if (mDeviceImageUrl != null) {
+ dest.writeString(mDeviceImageUrl);
+ }
+ dest.writeLong(mDiscoveryTimestampMillis);
+ dest.writeInt(mExtendedProperties.size());
+ for (DataElement dataElement : mExtendedProperties) {
+ dest.writeParcelable(dataElement, 0);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Creator<PresenceDevice> CREATOR = new Creator<PresenceDevice>() {
+ @Override
+ public PresenceDevice createFromParcel(Parcel in) {
+ String name = null;
+ if (in.readInt() == 1) {
+ name = in.readString();
+ }
+ int size = in.readInt();
+ List<Integer> mediums = new ArrayList<>();
+ for (int i = 0; i < size; i++) {
+ mediums.add(in.readInt());
+ }
+ int rssi = in.readInt();
+ byte[] salt = new byte[in.readInt()];
+ in.readByteArray(salt);
+ byte[] secretId = new byte[in.readInt()];
+ in.readByteArray(secretId);
+ byte[] encryptedIdentity = new byte[in.readInt()];
+ in.readByteArray(encryptedIdentity);
+ String deviceId = in.readString();
+ int deviceType = in.readInt();
+ String deviceImageUrl = null;
+ if (in.readInt() == 1) {
+ deviceImageUrl = in.readString();
+ }
+ long discoveryTimeMillis = in.readLong();
+ int dataElementSize = in.readInt();
+ List<DataElement> dataElements = new ArrayList<>();
+ for (int i = 0; i < dataElementSize; i++) {
+ dataElements.add(
+ in.readParcelable(DataElement.class.getClassLoader(), DataElement.class));
+ }
+ Builder builder = new Builder(deviceId, salt, secretId, encryptedIdentity)
+ .setName(name)
+ .setRssi(rssi)
+ .setDeviceType(deviceType)
+ .setDeviceImageUrl(deviceImageUrl)
+ .setDiscoveryTimestampMillis(discoveryTimeMillis);
+ for (int i = 0; i < mediums.size(); i++) {
+ builder.addMedium(mediums.get(i));
+ }
+ for (int i = 0; i < dataElements.size(); i++) {
+ builder.addExtendedProperty(dataElements.get(i));
+ }
+ return builder.build();
+ }
+
+ @Override
+ public PresenceDevice[] newArray(int size) {
+ return new PresenceDevice[size];
+ }
+ };
+
+ /**
+ * Builder class for {@link PresenceDevice}.
+ */
+ public static final class Builder {
+
+ private final List<DataElement> mExtendedProperties;
+ private final List<Integer> mMediums;
+ private final String mDeviceId;
+ private final byte[] mSalt;
+ private final byte[] mSecretId;
+ private final byte[] mEncryptedIdentity;
+
+ private String mName;
+ private int mRssi;
+ private int mDeviceType;
+ private String mDeviceImageUrl;
+ private long mDiscoveryTimestampMillis;
+
+ /**
+ * Constructs a {@link Builder}.
+ *
+ * @param deviceId the identifier on the discovered Presence device
+ * @param salt a random salt used in the beacon from the Presence device.
+ * @param secretId a secret identifier used in the beacon from the Presence device.
+ * @param encryptedIdentity the identity associated with the Presence device.
+ */
+ public Builder(@NonNull String deviceId, @NonNull byte[] salt, @NonNull byte[] secretId,
+ @NonNull byte[] encryptedIdentity) {
+ mDeviceId = deviceId;
+ mSalt = salt;
+ mSecretId = secretId;
+ mEncryptedIdentity = encryptedIdentity;
+ mMediums = new ArrayList<>();
+ mExtendedProperties = new ArrayList<>();
+ mRssi = -127;
+ }
+
+ /**
+ * Sets the name of the Presence device.
+ *
+ * @param name Name of the Presence. Can be {@code null} if there is no name.
+ */
+ @NonNull
+ public Builder setName(@Nullable String name) {
+ mName = name;
+ return this;
+ }
+
+ /**
+ * Adds the medium over which the Presence device is discovered.
+ *
+ * @param medium The {@link Medium} over which the device is discovered.
+ */
+ @NonNull
+ public Builder addMedium(@Medium int medium) {
+ mMediums.add(medium);
+ return this;
+ }
+
+ /**
+ * Sets the RSSI on the discovered Presence device.
+ *
+ * @param rssi The received signal strength in dBm.
+ */
+ @NonNull
+ public Builder setRssi(int rssi) {
+ mRssi = rssi;
+ return this;
+ }
+
+ /**
+ * Sets the type of discovered Presence device.
+ *
+ * @param deviceType Type of the Presence device.
+ */
+ @NonNull
+ public Builder setDeviceType(@DeviceType int deviceType) {
+ mDeviceType = deviceType;
+ return this;
+ }
+
+
+ /**
+ * Sets the image url of the discovered Presence device.
+ *
+ * @param deviceImageUrl Url of the image for the Presence device.
+ */
+ @NonNull
+ public Builder setDeviceImageUrl(@Nullable String deviceImageUrl) {
+ mDeviceImageUrl = deviceImageUrl;
+ return this;
+ }
+
+
+ /**
+ * Sets discovery timestamp, the clock is based on elapsed time.
+ *
+ * @param discoveryTimestampMillis Timestamp when the presence device is discovered.
+ */
+ @NonNull
+ public Builder setDiscoveryTimestampMillis(long discoveryTimestampMillis) {
+ mDiscoveryTimestampMillis = discoveryTimestampMillis;
+ return this;
+ }
+
+
+ /**
+ * Adds an extended property of the discovered presence device.
+ *
+ * @param dataElement Data element of the extended property.
+ */
+ @NonNull
+ public Builder addExtendedProperty(@NonNull DataElement dataElement) {
+ Objects.requireNonNull(dataElement);
+ mExtendedProperties.add(dataElement);
+ return this;
+ }
+
+ /**
+ * Builds a Presence device.
+ */
+ @NonNull
+ public PresenceDevice build() {
+ return new PresenceDevice(mName, mMediums, mRssi, mDeviceId,
+ mSalt, mSecretId, mEncryptedIdentity,
+ mDeviceType,
+ mDeviceImageUrl,
+ mDiscoveryTimestampMillis, mExtendedProperties);
+ }
+ }
+}
diff --git a/nearby/framework/java/android/nearby/PresenceScanFilter.java b/nearby/framework/java/android/nearby/PresenceScanFilter.java
new file mode 100644
index 0000000..f0c3c06
--- /dev/null
+++ b/nearby/framework/java/android/nearby/PresenceScanFilter.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArraySet;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Filter for scanning a nearby presence device.
+ *
+ * @hide
+ */
+@SystemApi
+public final class PresenceScanFilter extends ScanFilter implements Parcelable {
+
+ private final List<PublicCredential> mCredentials;
+ private final List<Integer> mPresenceActions;
+ private final List<DataElement> mExtendedProperties;
+
+ /**
+ * A list of credentials to filter on.
+ */
+ @NonNull
+ public List<PublicCredential> getCredentials() {
+ return mCredentials;
+ }
+
+ /**
+ * A list of presence actions for matching.
+ */
+ @NonNull
+ public List<Integer> getPresenceActions() {
+ return mPresenceActions;
+ }
+
+ /**
+ * A bundle of extended properties for matching.
+ */
+ @NonNull
+ public List<DataElement> getExtendedProperties() {
+ return mExtendedProperties;
+ }
+
+ private PresenceScanFilter(int rssiThreshold, List<PublicCredential> credentials,
+ List<Integer> presenceActions, List<DataElement> extendedProperties) {
+ super(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE, rssiThreshold);
+ mCredentials = new ArrayList<>(credentials);
+ mPresenceActions = new ArrayList<>(presenceActions);
+ mExtendedProperties = extendedProperties;
+ }
+
+ private PresenceScanFilter(Parcel in) {
+ super(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE, in);
+ mCredentials = new ArrayList<>();
+ if (in.readInt() != 0) {
+ in.readParcelableList(mCredentials, PublicCredential.class.getClassLoader(),
+ PublicCredential.class);
+ }
+ mPresenceActions = new ArrayList<>();
+ if (in.readInt() != 0) {
+ in.readList(mPresenceActions, Integer.class.getClassLoader(), Integer.class);
+ }
+ mExtendedProperties = new ArrayList<>();
+ if (in.readInt() != 0) {
+ in.readParcelableList(mExtendedProperties, DataElement.class.getClassLoader(),
+ DataElement.class);
+ }
+ }
+
+ @NonNull
+ public static final Creator<PresenceScanFilter> CREATOR = new Creator<PresenceScanFilter>() {
+ @Override
+ public PresenceScanFilter createFromParcel(Parcel in) {
+ // Skip Scan Filter type as it's used for parent class.
+ in.readInt();
+ return createFromParcelBody(in);
+ }
+
+ @Override
+ public PresenceScanFilter[] newArray(int size) {
+ return new PresenceScanFilter[size];
+ }
+ };
+
+ /**
+ * Create a {@link PresenceScanFilter} from the parcel body. Scan Filter type is skipped.
+ */
+ static PresenceScanFilter createFromParcelBody(Parcel in) {
+ return new PresenceScanFilter(in);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(mCredentials.size());
+ if (!mCredentials.isEmpty()) {
+ dest.writeParcelableList(mCredentials, 0);
+ }
+ dest.writeInt(mPresenceActions.size());
+ if (!mPresenceActions.isEmpty()) {
+ dest.writeList(mPresenceActions);
+ }
+ dest.writeInt(mExtendedProperties.size());
+ if (!mExtendedProperties.isEmpty()) {
+ dest.writeList(mExtendedProperties);
+ }
+ }
+
+ /**
+ * Builder for {@link PresenceScanFilter}.
+ */
+ public static final class Builder {
+ private int mMaxPathLoss;
+ private final Set<PublicCredential> mCredentials;
+ private final Set<Integer> mPresenceIdentities;
+ private final Set<Integer> mPresenceActions;
+ private final List<DataElement> mExtendedProperties;
+
+ public Builder() {
+ mMaxPathLoss = 127;
+ mCredentials = new ArraySet<>();
+ mPresenceIdentities = new ArraySet<>();
+ mPresenceActions = new ArraySet<>();
+ mExtendedProperties = new ArrayList<>();
+ }
+
+ /**
+ * Sets the max path loss (in dBm) for the scan request. The path loss is the attenuation
+ * of radio energy between sender and receiver. Path loss here is defined as (TxPower -
+ * Rssi).
+ */
+ @NonNull
+ public Builder setMaxPathLoss(@IntRange(from = 0, to = 127) int maxPathLoss) {
+ mMaxPathLoss = maxPathLoss;
+ return this;
+ }
+
+ /**
+ * Adds a credential the scan filter is expected to match.
+ */
+
+ @NonNull
+ public Builder addCredential(@NonNull PublicCredential credential) {
+ Objects.requireNonNull(credential);
+ mCredentials.add(credential);
+ return this;
+ }
+
+ /**
+ * Adds a presence action for filtering, which is an action the discoverer could take
+ * when it receives the broadcast of a presence device.
+ */
+ @NonNull
+ public Builder addPresenceAction(@IntRange(from = 1, to = 255) int action) {
+ mPresenceActions.add(action);
+ return this;
+ }
+
+ /**
+ * Add an extended property for scan filtering.
+ */
+ @NonNull
+ public Builder addExtendedProperty(@NonNull DataElement dataElement) {
+ Objects.requireNonNull(dataElement);
+ mExtendedProperties.add(dataElement);
+ return this;
+ }
+
+ /**
+ * Builds the scan filter.
+ */
+ @NonNull
+ public PresenceScanFilter build() {
+ Preconditions.checkState(!mCredentials.isEmpty(), "credentials cannot be empty");
+ return new PresenceScanFilter(mMaxPathLoss,
+ new ArrayList<>(mCredentials),
+ new ArrayList<>(mPresenceActions),
+ mExtendedProperties);
+ }
+ }
+}
diff --git a/nearby/framework/java/android/nearby/PrivateCredential.java b/nearby/framework/java/android/nearby/PrivateCredential.java
new file mode 100644
index 0000000..d915cc6
--- /dev/null
+++ b/nearby/framework/java/android/nearby/PrivateCredential.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a private credential.
+ *
+ * @hide
+ */
+@SystemApi
+public final class PrivateCredential extends PresenceCredential implements Parcelable {
+
+ @NonNull
+ public static final Creator<PrivateCredential> CREATOR = new Creator<PrivateCredential>() {
+ @Override
+ public PrivateCredential createFromParcel(Parcel in) {
+ in.readInt(); // Skip the type as it's used by parent class only.
+ return createFromParcelBody(in);
+ }
+
+ @Override
+ public PrivateCredential[] newArray(int size) {
+ return new PrivateCredential[size];
+ }
+ };
+
+ private byte[] mMetadataEncryptionKey;
+ private String mDeviceName;
+
+ private PrivateCredential(Parcel in) {
+ super(CREDENTIAL_TYPE_PRIVATE, in);
+ mMetadataEncryptionKey = new byte[in.readInt()];
+ in.readByteArray(mMetadataEncryptionKey);
+ mDeviceName = in.readString();
+ }
+
+ private PrivateCredential(int identityType, byte[] secretId,
+ String deviceName, byte[] authenticityKey, List<CredentialElement> credentialElements,
+ byte[] metadataEncryptionKey) {
+ super(CREDENTIAL_TYPE_PRIVATE, identityType, secretId, authenticityKey,
+ credentialElements);
+ mDeviceName = deviceName;
+ mMetadataEncryptionKey = metadataEncryptionKey;
+ }
+
+ static PrivateCredential createFromParcelBody(Parcel in) {
+ return new PrivateCredential(in);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(mMetadataEncryptionKey.length);
+ dest.writeByteArray(mMetadataEncryptionKey);
+ dest.writeString(mDeviceName);
+ }
+
+ /**
+ * Returns the metadata encryption key associated with this credential.
+ */
+ @NonNull
+ public byte[] getMetadataEncryptionKey() {
+ return mMetadataEncryptionKey;
+ }
+
+ /**
+ * Returns the device name associated with this credential.
+ */
+ @NonNull
+ public String getDeviceName() {
+ return mDeviceName;
+ }
+
+ /**
+ * Builder class for {@link PresenceCredential}.
+ */
+ public static final class Builder {
+ private final List<CredentialElement> mCredentialElements;
+
+ private @IdentityType int mIdentityType;
+ private final byte[] mSecretId;
+ private final byte[] mAuthenticityKey;
+ private final byte[] mMetadataEncryptionKey;
+ private final String mDeviceName;
+
+ public Builder(@NonNull byte[] secretId, @NonNull byte[] authenticityKey,
+ @NonNull byte[] metadataEncryptionKey, @NonNull String deviceName) {
+ Preconditions.checkState(secretId != null && secretId.length > 0,
+ "secret id cannot be empty");
+ Preconditions.checkState(authenticityKey != null && authenticityKey.length > 0,
+ "authenticity key cannot be empty");
+ Preconditions.checkState(
+ metadataEncryptionKey != null && metadataEncryptionKey.length > 0,
+ "metadataEncryptionKey cannot be empty");
+ Preconditions.checkState(deviceName != null && deviceName.length() > 0,
+ "deviceName cannot be empty");
+ mSecretId = secretId;
+ mAuthenticityKey = authenticityKey;
+ mMetadataEncryptionKey = metadataEncryptionKey;
+ mDeviceName = deviceName;
+ mCredentialElements = new ArrayList<>();
+ }
+
+ /**
+ * Sets the identity type for the presence credential.
+ */
+ @NonNull
+ public Builder setIdentityType(@IdentityType int identityType) {
+ mIdentityType = identityType;
+ return this;
+ }
+
+ /**
+ * Adds an element to the credential.
+ */
+ @NonNull
+ public Builder addCredentialElement(@NonNull CredentialElement credentialElement) {
+ mCredentialElements.add(credentialElement);
+ return this;
+ }
+
+ /**
+ * Builds the {@link PresenceCredential}.
+ */
+ @NonNull
+ public PrivateCredential build() {
+ return new PrivateCredential(mIdentityType, mSecretId, mDeviceName,
+ mAuthenticityKey, mCredentialElements, mMetadataEncryptionKey);
+ }
+
+ }
+}
diff --git a/nearby/framework/java/android/nearby/PublicCredential.java b/nearby/framework/java/android/nearby/PublicCredential.java
new file mode 100644
index 0000000..5998d19
--- /dev/null
+++ b/nearby/framework/java/android/nearby/PublicCredential.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a public credential.
+ *
+ * @hide
+ */
+@SystemApi
+public final class PublicCredential extends PresenceCredential implements Parcelable {
+ @NonNull
+ public static final Creator<PublicCredential> CREATOR =
+ new Creator<PublicCredential>() {
+ @Override
+ public PublicCredential createFromParcel(Parcel in) {
+ in.readInt(); // Skip the type as it's used by parent class only.
+ return createFromParcelBody(in);
+ }
+
+ @Override
+ public PublicCredential[] newArray(int size) {
+ return new PublicCredential[size];
+ }
+ };
+
+ private final byte[] mPublicKey;
+ private final byte[] mEncryptedMetadata;
+ private final byte[] mEncryptedMetadataKeyTag;
+
+ private PublicCredential(
+ int identityType,
+ byte[] secretId,
+ byte[] authenticityKey,
+ List<CredentialElement> credentialElements,
+ byte[] publicKey,
+ byte[] encryptedMetadata,
+ byte[] metadataEncryptionKeyTag) {
+ super(CREDENTIAL_TYPE_PUBLIC, identityType, secretId, authenticityKey, credentialElements);
+ mPublicKey = publicKey;
+ mEncryptedMetadata = encryptedMetadata;
+ mEncryptedMetadataKeyTag = metadataEncryptionKeyTag;
+ }
+
+ private PublicCredential(Parcel in) {
+ super(CREDENTIAL_TYPE_PUBLIC, in);
+ mPublicKey = new byte[in.readInt()];
+ in.readByteArray(mPublicKey);
+ mEncryptedMetadata = new byte[in.readInt()];
+ in.readByteArray(mEncryptedMetadata);
+ mEncryptedMetadataKeyTag = new byte[in.readInt()];
+ in.readByteArray(mEncryptedMetadataKeyTag);
+ }
+
+ static PublicCredential createFromParcelBody(Parcel in) {
+ return new PublicCredential(in);
+ }
+
+ /** Returns the public key associated with this credential. */
+ @NonNull
+ public byte[] getPublicKey() {
+ return mPublicKey;
+ }
+
+ /** Returns the encrypted metadata associated with this credential. */
+ @NonNull
+ public byte[] getEncryptedMetadata() {
+ return mEncryptedMetadata;
+ }
+
+ /** Returns the metadata encryption key tag associated with this credential. */
+ @NonNull
+ public byte[] getEncryptedMetadataKeyTag() {
+ return mEncryptedMetadataKeyTag;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj instanceof PublicCredential) {
+ PublicCredential that = (PublicCredential) obj;
+ return super.equals(obj)
+ && Arrays.equals(mPublicKey, that.mPublicKey)
+ && Arrays.equals(mEncryptedMetadata, that.mEncryptedMetadata)
+ && Arrays.equals(mEncryptedMetadataKeyTag, that.mEncryptedMetadataKeyTag);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ super.hashCode(),
+ Arrays.hashCode(mPublicKey),
+ Arrays.hashCode(mEncryptedMetadata),
+ Arrays.hashCode(mEncryptedMetadataKeyTag));
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(mPublicKey.length);
+ dest.writeByteArray(mPublicKey);
+ dest.writeInt(mEncryptedMetadata.length);
+ dest.writeByteArray(mEncryptedMetadata);
+ dest.writeInt(mEncryptedMetadataKeyTag.length);
+ dest.writeByteArray(mEncryptedMetadataKeyTag);
+ }
+
+ /** Builder class for {@link PresenceCredential}. */
+ public static final class Builder {
+ private final List<CredentialElement> mCredentialElements;
+
+ private @IdentityType int mIdentityType;
+ private final byte[] mSecretId;
+ private final byte[] mAuthenticityKey;
+ private final byte[] mPublicKey;
+ private final byte[] mEncryptedMetadata;
+ private final byte[] mEncryptedMetadataKeyTag;
+
+ public Builder(
+ @NonNull byte[] secretId,
+ @NonNull byte[] authenticityKey,
+ @NonNull byte[] publicKey,
+ @NonNull byte[] encryptedMetadata,
+ @NonNull byte[] encryptedMetadataKeyTag) {
+ Preconditions.checkState(
+ secretId != null && secretId.length > 0, "secret id cannot be empty");
+ Preconditions.checkState(
+ authenticityKey != null && authenticityKey.length > 0,
+ "authenticity key cannot be empty");
+ Preconditions.checkState(
+ publicKey != null && publicKey.length > 0, "publicKey cannot be empty");
+ Preconditions.checkState(
+ encryptedMetadata != null && encryptedMetadata.length > 0,
+ "encryptedMetadata cannot be empty");
+ Preconditions.checkState(
+ encryptedMetadataKeyTag != null && encryptedMetadataKeyTag.length > 0,
+ "encryptedMetadataKeyTag cannot be empty");
+
+ mSecretId = secretId;
+ mAuthenticityKey = authenticityKey;
+ mPublicKey = publicKey;
+ mEncryptedMetadata = encryptedMetadata;
+ mEncryptedMetadataKeyTag = encryptedMetadataKeyTag;
+ mCredentialElements = new ArrayList<>();
+ }
+
+ /** Sets the identity type for the presence credential. */
+ @NonNull
+ public Builder setIdentityType(@IdentityType int identityType) {
+ mIdentityType = identityType;
+ return this;
+ }
+
+ /** Adds an element to the credential. */
+ @NonNull
+ public Builder addCredentialElement(@NonNull CredentialElement credentialElement) {
+ Objects.requireNonNull(credentialElement);
+ mCredentialElements.add(credentialElement);
+ return this;
+ }
+
+ /** Builds the {@link PresenceCredential}. */
+ @NonNull
+ public PublicCredential build() {
+ return new PublicCredential(
+ mIdentityType,
+ mSecretId,
+ mAuthenticityKey,
+ mCredentialElements,
+ mPublicKey,
+ mEncryptedMetadata,
+ mEncryptedMetadataKeyTag);
+ }
+ }
+}
diff --git a/nearby/framework/java/android/nearby/ScanCallback.java b/nearby/framework/java/android/nearby/ScanCallback.java
new file mode 100644
index 0000000..1b1b4bc
--- /dev/null
+++ b/nearby/framework/java/android/nearby/ScanCallback.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+/**
+ * Reports newly discovered devices.
+ * Note: The frequency of the callback is dependent on whether the caller
+ * is in the foreground or background. Foreground callbacks will occur
+ * as fast as the underlying medium supports, whereas background
+ * use cases will be rate limited to improve performance (ie, only on
+ * found/lost/significant changes).
+ *
+ * @hide
+ */
+@SystemApi
+public interface ScanCallback {
+ /**
+ * Reports a {@link NearbyDevice} being discovered.
+ *
+ * @param device {@link NearbyDevice} that is found.
+ */
+ void onDiscovered(@NonNull NearbyDevice device);
+
+ /**
+ * Reports a {@link NearbyDevice} information(distance, packet, and etc) changed.
+ *
+ * @param device {@link NearbyDevice} that has updates.
+ */
+ void onUpdated(@NonNull NearbyDevice device);
+
+ /**
+ * Reports a {@link NearbyDevice} is no longer within range.
+ *
+ * @param device {@link NearbyDevice} that is lost.
+ */
+ void onLost(@NonNull NearbyDevice device);
+}
diff --git a/nearby/framework/java/android/nearby/ScanFilter.java b/nearby/framework/java/android/nearby/ScanFilter.java
new file mode 100644
index 0000000..1409426
--- /dev/null
+++ b/nearby/framework/java/android/nearby/ScanFilter.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+
+/**
+ * Filter for scanning a nearby device.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class ScanFilter {
+ /**
+ * Creates a {@link ScanFilter} from the parcel.
+ *
+ * @hide
+ */
+ public static ScanFilter createFromParcel(Parcel in) {
+ int type = in.readInt();
+ switch (type) {
+ // Currently, only Nearby Presence filtering is supported, in the future
+ // filtering other nearby specifications will be added.
+ case ScanRequest.SCAN_TYPE_NEARBY_PRESENCE:
+ return PresenceScanFilter.createFromParcelBody(in);
+ default:
+ throw new IllegalStateException(
+ "Unexpected scan type (value " + type + ") in parcel.");
+ }
+ }
+
+ private final @ScanRequest.ScanType int mType;
+ private final int mMaxPathLoss;
+
+ /**
+ * Constructs a Scan Filter.
+ *
+ * @hide
+ */
+ ScanFilter(@ScanRequest.ScanType int type, @IntRange(from = 0, to = 127) int maxPathLoss) {
+ mType = type;
+ mMaxPathLoss = maxPathLoss;
+ }
+
+ /**
+ * Constructs a Scan Filter.
+ *
+ * @hide
+ */
+ ScanFilter(@ScanRequest.ScanType int type, Parcel in) {
+ mType = type;
+ mMaxPathLoss = in.readInt();
+ }
+
+ /**
+ * Returns the type of this scan filter.
+ */
+ public @ScanRequest.ScanType int getType() {
+ return mType;
+ }
+
+ /**
+ * Returns the maximum path loss (in dBm) of the received scan result. The path loss is the
+ * attenuation of radio energy between sender and receiver. Path loss here is defined as
+ * (TxPower - Rssi).
+ */
+ @IntRange(from = 0, to = 127)
+ public int getMaxPathLoss() {
+ return mMaxPathLoss;
+ }
+
+ /**
+ *
+ * Writes the scan filter to the parcel.
+ *
+ * @hide
+ */
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mType);
+ dest.writeInt(mMaxPathLoss);
+ }
+}
diff --git a/nearby/framework/java/android/nearby/ScanRequest.aidl b/nearby/framework/java/android/nearby/ScanRequest.aidl
new file mode 100644
index 0000000..438dfed
--- /dev/null
+++ b/nearby/framework/java/android/nearby/ScanRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+parcelable ScanRequest;
diff --git a/nearby/framework/java/android/nearby/ScanRequest.java b/nearby/framework/java/android/nearby/ScanRequest.java
new file mode 100644
index 0000000..cf2dd43
--- /dev/null
+++ b/nearby/framework/java/android/nearby/ScanRequest.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.Manifest;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.WorkSource;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * An encapsulation of various parameters for requesting nearby scans.
+ *
+ * @hide
+ */
+@SystemApi
+public final class ScanRequest implements Parcelable {
+
+ /** Scan type for scanning devices using fast pair protocol. */
+ public static final int SCAN_TYPE_FAST_PAIR = 1;
+ /** Scan type for scanning devices using nearby presence protocol. */
+ public static final int SCAN_TYPE_NEARBY_PRESENCE = 2;
+
+ /** Scan mode uses highest duty cycle. */
+ public static final int SCAN_MODE_LOW_LATENCY = 2;
+ /** Scan in balanced power mode.
+ * Scan results are returned at a rate that provides a good trade-off between scan
+ * frequency and power consumption.
+ */
+ public static final int SCAN_MODE_BALANCED = 1;
+ /** Perform scan in low power mode. This is the default scan mode. */
+ public static final int SCAN_MODE_LOW_POWER = 0;
+ /**
+ * A special scan mode. Applications using this scan mode will passively listen for other scan
+ * results without starting BLE scans themselves.
+ */
+ public static final int SCAN_MODE_NO_POWER = -1;
+ /**
+ * Used to read a ScanRequest from a Parcel.
+ */
+ @NonNull
+ public static final Creator<ScanRequest> CREATOR = new Creator<ScanRequest>() {
+ @Override
+ public ScanRequest createFromParcel(Parcel in) {
+ ScanRequest.Builder builder = new ScanRequest.Builder()
+ .setScanType(in.readInt())
+ .setScanMode(in.readInt())
+ .setBleEnabled(in.readBoolean())
+ .setWorkSource(in.readTypedObject(WorkSource.CREATOR));
+ for (int i = 0; i < in.readInt(); i++) {
+ builder.addScanFilter(ScanFilter.createFromParcel(in));
+ }
+ return builder.build();
+ }
+
+ @Override
+ public ScanRequest[] newArray(int size) {
+ return new ScanRequest[size];
+ }
+ };
+
+ private final @ScanType int mScanType;
+ private final @ScanMode int mScanMode;
+ private final boolean mBleEnabled;
+ private final @NonNull WorkSource mWorkSource;
+ private final List<ScanFilter> mScanFilters;
+
+ private ScanRequest(@ScanType int scanType, @ScanMode int scanMode, boolean bleEnabled,
+ @NonNull WorkSource workSource, List<ScanFilter> scanFilters) {
+ mScanType = scanType;
+ mScanMode = scanMode;
+ mBleEnabled = bleEnabled;
+ mWorkSource = workSource;
+ mScanFilters = scanFilters;
+ }
+
+ /**
+ * Convert scan mode to readable string.
+ *
+ * @param scanMode Integer that may represent a{@link ScanMode}.
+ */
+ @NonNull
+ public static String scanModeToString(@ScanMode int scanMode) {
+ switch (scanMode) {
+ case SCAN_MODE_LOW_LATENCY:
+ return "SCAN_MODE_LOW_LATENCY";
+ case SCAN_MODE_BALANCED:
+ return "SCAN_MODE_BALANCED";
+ case SCAN_MODE_LOW_POWER:
+ return "SCAN_MODE_LOW_POWER";
+ case SCAN_MODE_NO_POWER:
+ return "SCAN_MODE_NO_POWER";
+ default:
+ return "SCAN_MODE_INVALID";
+ }
+ }
+
+ /**
+ * Returns true if an integer is a defined scan type.
+ */
+ public static boolean isValidScanType(@ScanType int scanType) {
+ return scanType == SCAN_TYPE_FAST_PAIR
+ || scanType == SCAN_TYPE_NEARBY_PRESENCE;
+ }
+
+ /**
+ * Returns true if an integer is a defined scan mode.
+ */
+ public static boolean isValidScanMode(@ScanMode int scanMode) {
+ return scanMode == SCAN_MODE_LOW_LATENCY
+ || scanMode == SCAN_MODE_BALANCED
+ || scanMode == SCAN_MODE_LOW_POWER
+ || scanMode == SCAN_MODE_NO_POWER;
+ }
+
+ /**
+ * Returns the scan type for this request.
+ */
+ public @ScanType int getScanType() {
+ return mScanType;
+ }
+
+ /**
+ * Returns the scan mode for this request.
+ */
+ public @ScanMode int getScanMode() {
+ return mScanMode;
+ }
+
+ /**
+ * Returns if Bluetooth Low Energy enabled for scanning.
+ */
+ public boolean isBleEnabled() {
+ return mBleEnabled;
+ }
+
+ /**
+ * Returns Scan Filters for this request.
+ */
+ @NonNull
+ public List<ScanFilter> getScanFilters() {
+ return mScanFilters;
+ }
+
+ /**
+ * Returns the work source used for power attribution of this request.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public WorkSource getWorkSource() {
+ return mWorkSource;
+ }
+
+ /**
+ * No special parcel contents.
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Returns a string representation of this ScanRequest.
+ */
+ @Override
+ public String toString() {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("Request[")
+ .append("scanType=").append(mScanType);
+ stringBuilder.append(", scanMode=").append(scanModeToString(mScanMode));
+ stringBuilder.append(", enableBle=").append(mBleEnabled);
+ stringBuilder.append(", workSource=").append(mWorkSource);
+ stringBuilder.append(", scanFilters=").append(mScanFilters);
+ stringBuilder.append("]");
+ return stringBuilder.toString();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mScanType);
+ dest.writeInt(mScanMode);
+ dest.writeBoolean(mBleEnabled);
+ dest.writeTypedObject(mWorkSource, /* parcelableFlags= */0);
+ dest.writeInt(mScanFilters.size());
+ for (int i = 0; i < mScanFilters.size(); ++i) {
+ mScanFilters.get(i).writeToParcel(dest, flags);
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof ScanRequest) {
+ ScanRequest otherRequest = (ScanRequest) other;
+ return mScanType == otherRequest.mScanType
+ && (mScanMode == otherRequest.mScanMode)
+ && (mBleEnabled == otherRequest.mBleEnabled)
+ && (Objects.equals(mWorkSource, otherRequest.mWorkSource));
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mScanType, mScanMode, mBleEnabled, mWorkSource);
+ }
+
+ /** @hide **/
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SCAN_TYPE_FAST_PAIR, SCAN_TYPE_NEARBY_PRESENCE})
+ public @interface ScanType {
+ }
+
+ /** @hide **/
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SCAN_MODE_LOW_LATENCY, SCAN_MODE_BALANCED,
+ SCAN_MODE_LOW_POWER,
+ SCAN_MODE_NO_POWER})
+ public @interface ScanMode {}
+
+ /** A builder class for {@link ScanRequest}. */
+ public static final class Builder {
+ private static final int INVALID_SCAN_TYPE = -1;
+ private @ScanType int mScanType;
+ private @ScanMode int mScanMode;
+
+ private boolean mBleEnabled;
+ private WorkSource mWorkSource;
+ private List<ScanFilter> mScanFilters;
+
+ /** Creates a new Builder with the given scan type. */
+ public Builder() {
+ mScanType = INVALID_SCAN_TYPE;
+ mBleEnabled = true;
+ mWorkSource = new WorkSource();
+ mScanFilters = new ArrayList<>();
+ }
+
+ /**
+ * Sets the scan type for the request. The scan type must be one of the SCAN_TYPE_ constants
+ * in {@link ScanRequest}.
+ *
+ * @param scanType The scan type for the request
+ */
+ @NonNull
+ public Builder setScanType(@ScanType int scanType) {
+ mScanType = scanType;
+ return this;
+ }
+
+ /**
+ * Sets the scan mode for the request. The scan type must be one of the SCAN_MODE_ constants
+ * in {@link ScanRequest}.
+ *
+ * @param scanMode The scan mode for the request
+ */
+ @NonNull
+ public Builder setScanMode(@ScanMode int scanMode) {
+ mScanMode = scanMode;
+ return this;
+ }
+
+ /**
+ * Sets if the ble is enabled for scanning.
+ *
+ * @param bleEnabled If the BluetoothLe is enabled in the device.
+ */
+ @NonNull
+ public Builder setBleEnabled(boolean bleEnabled) {
+ mBleEnabled = bleEnabled;
+ return this;
+ }
+
+ /**
+ * Sets the work source to use for power attribution for this scan request. Defaults to
+ * empty work source, which implies the caller that sends the scan request will be used
+ * for power attribution.
+ *
+ * <p>Permission enforcement occurs when the resulting scan request is used, not when
+ * this method is invoked.
+ *
+ * @param workSource identifying the application(s) for which to blame for the scan.
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.UPDATE_DEVICE_STATS)
+ @NonNull
+ @SystemApi
+ public Builder setWorkSource(@Nullable WorkSource workSource) {
+ if (workSource == null) {
+ mWorkSource = new WorkSource();
+ } else {
+ mWorkSource = workSource;
+ }
+ return this;
+ }
+
+ /**
+ * Adds a scan filter to the request. Client can call this method multiple times to add
+ * more than one scan filter. Scan results that match any of these scan filters will
+ * be returned.
+ *
+ * <p>On devices with hardware support, scan filters can significantly improve the battery
+ * usage of Nearby scans.
+ *
+ * @param scanFilter Filter for scanning the request.
+ */
+ @NonNull
+ public Builder addScanFilter(@NonNull ScanFilter scanFilter) {
+ Objects.requireNonNull(scanFilter);
+ mScanFilters.add(scanFilter);
+ return this;
+ }
+
+ /**
+ * Builds a scan request from this builder.
+ *
+ * @return a new nearby scan request.
+ * @throws IllegalStateException if the scanType is not one of the SCAN_TYPE_ constants in
+ * {@link ScanRequest}.
+ */
+ @NonNull
+ public ScanRequest build() {
+ Preconditions.checkState(isValidScanType(mScanType),
+ "invalid scan type : " + mScanType
+ + ", scan type must be one of ScanRequest#SCAN_TYPE_");
+ Preconditions.checkState(isValidScanMode(mScanMode),
+ "invalid scan mode : " + mScanMode
+ + ", scan mode must be one of ScanMode#SCAN_MODE_");
+ return new ScanRequest(mScanType, mScanMode, mBleEnabled, mWorkSource, mScanFilters);
+ }
+ }
+}
diff --git a/nearby/framework/java/android/nearby/aidl/ByteArrayParcel.aidl b/nearby/framework/java/android/nearby/aidl/ByteArrayParcel.aidl
new file mode 100644
index 0000000..53c73bd
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/ByteArrayParcel.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.aidl;
+
+/**
+ * This is to support 2D byte arrays.
+ * {@hide}
+ */
+parcelable ByteArrayParcel {
+ byte[] byteArray;
+}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairAccountDevicesMetadataRequestParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairAccountDevicesMetadataRequestParcel.aidl
new file mode 100644
index 0000000..fc3ba22
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/FastPairAccountDevicesMetadataRequestParcel.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.aidl;
+
+import android.accounts.Account;
+import android.nearby.aidl.ByteArrayParcel;
+
+/**
+ * Request details for Metadata of Fast Pair devices associated with an account.
+ * {@hide}
+ */
+parcelable FastPairAccountDevicesMetadataRequestParcel {
+ Account account;
+ ByteArrayParcel[] deviceAccountKeys;
+}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairAccountKeyDeviceMetadataParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairAccountKeyDeviceMetadataParcel.aidl
new file mode 100644
index 0000000..8014323
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/FastPairAccountKeyDeviceMetadataParcel.aidl
@@ -0,0 +1,35 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package android.nearby.aidl;
+
+import android.nearby.aidl.FastPairDeviceMetadataParcel;
+import android.nearby.aidl.FastPairDiscoveryItemParcel;
+
+/**
+ * Metadata of a Fast Pair device associated with an account.
+ * {@hide}
+ */
+ // TODO(b/204780849): remove unnecessary fields and polish comments.
+parcelable FastPairAccountKeyDeviceMetadataParcel {
+ // Key of the Fast Pair device associated with the account.
+ byte[] deviceAccountKey;
+ // Hash function of device account key and public bluetooth address.
+ byte[] sha256DeviceAccountKeyPublicAddress;
+ // Fast Pair device metadata for the Fast Pair device.
+ FastPairDeviceMetadataParcel metadata;
+ // Fast Pair discovery item tied to both the Fast Pair device and the
+ // account.
+ FastPairDiscoveryItemParcel discoveryItem;
+}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairAntispoofKeyDeviceMetadataParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairAntispoofKeyDeviceMetadataParcel.aidl
new file mode 100644
index 0000000..4fd4d4b
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/FastPairAntispoofKeyDeviceMetadataParcel.aidl
@@ -0,0 +1,31 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package android.nearby.aidl;
+
+import android.nearby.aidl.FastPairDeviceMetadataParcel;
+
+/**
+ * Metadata of a Fast Pair device keyed by AntispoofKey,
+ * Used by initial pairing without account association.
+ *
+ * {@hide}
+ */
+parcelable FastPairAntispoofKeyDeviceMetadataParcel {
+ // Anti-spoof public key.
+ byte[] antispoofPublicKey;
+
+ // Fast Pair device metadata for the Fast Pair device.
+ FastPairDeviceMetadataParcel deviceMetadata;
+}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairAntispoofKeyDeviceMetadataRequestParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairAntispoofKeyDeviceMetadataRequestParcel.aidl
new file mode 100644
index 0000000..afdcf15
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/FastPairAntispoofKeyDeviceMetadataRequestParcel.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.aidl;
+
+/**
+ * Request details for metadata of a Fast Pair device keyed by either
+ * antispoofKey or modelId.
+ * {@hide}
+ */
+parcelable FastPairAntispoofKeyDeviceMetadataRequestParcel {
+ byte[] modelId;
+}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairDeviceMetadataParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairDeviceMetadataParcel.aidl
new file mode 100644
index 0000000..d90f6a1
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/FastPairDeviceMetadataParcel.aidl
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.aidl;
+
+/**
+ * Fast Pair Device Metadata for a given device model ID.
+ * @hide
+ */
+// TODO(b/204780849): remove unnecessary fields and polish comments.
+parcelable FastPairDeviceMetadataParcel {
+ // The image to show on the notification.
+ String imageUrl;
+
+ // The intent that will be launched via the notification.
+ String intentUri;
+
+ // The transmit power of the device's BLE chip.
+ int bleTxPower;
+
+ // The distance that the device must be within to show a notification.
+ // If no distance is set, we default to 0.6 meters. Only Nearby admins can
+ // change this.
+ float triggerDistance;
+
+ // The image icon that shows in the notification.
+ byte[] image;
+
+ // The name of the device.
+ String name;
+
+ int deviceType;
+
+ // The image urls for device with device type "true wireless".
+ String trueWirelessImageUrlLeftBud;
+ String trueWirelessImageUrlRightBud;
+ String trueWirelessImageUrlCase;
+
+ // The notification description for when the device is initially discovered.
+ String initialNotificationDescription;
+
+ // The notification description for when the device is initially discovered
+ // and no account is logged in.
+ String initialNotificationDescriptionNoAccount;
+
+ // The notification description for once we have finished pairing and the
+ // companion app has been opened. For Bisto devices, this String will point
+ // users to setting up the assistant.
+ String openCompanionAppDescription;
+
+ // The notification description for once we have finished pairing and the
+ // companion app needs to be updated before use.
+ String updateCompanionAppDescription;
+
+ // The notification description for once we have finished pairing and the
+ // companion app needs to be installed.
+ String downloadCompanionAppDescription;
+
+ // The notification title when a pairing fails.
+ String unableToConnectTitle;
+
+ // The notification summary when a pairing fails.
+ String unableToConnectDescription;
+
+ // The description that helps user initially paired with device.
+ String initialPairingDescription;
+
+ // The description that let user open the companion app.
+ String connectSuccessCompanionAppInstalled;
+
+ // The description that let user download the companion app.
+ String connectSuccessCompanionAppNotInstalled;
+
+ // The description that reminds user there is a paired device nearby.
+ String subsequentPairingDescription;
+
+ // The description that reminds users opt in their device.
+ String retroactivePairingDescription;
+
+ // The description that indicates companion app is about to launch.
+ String waitLaunchCompanionAppDescription;
+
+ // The description that indicates go to bluetooth settings when connection
+ // fail.
+ String failConnectGoToSettingsDescription;
+}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairDiscoveryItemParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairDiscoveryItemParcel.aidl
new file mode 100644
index 0000000..2cc2daa
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/FastPairDiscoveryItemParcel.aidl
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.aidl;
+
+/**
+ * Fast Pair Discovery Item.
+ * @hide
+ */
+// TODO(b/204780849): remove unnecessary fields and polish comments.
+parcelable FastPairDiscoveryItemParcel {
+ // Offline item: unique ID generated on client.
+ // Online item: unique ID generated on server.
+ String id;
+
+ // The most recent all upper case mac associated with this item.
+ // (Mac-to-DiscoveryItem is a many-to-many relationship)
+ String macAddress;
+
+ String actionUrl;
+
+ // The bluetooth device name from advertisement
+ String deviceName;
+
+ // Item's title
+ String title;
+
+ // Item's description.
+ String description;
+
+ // The URL for display
+ String displayUrl;
+
+ // Client timestamp when the beacon was last observed in BLE scan.
+ long lastObservationTimestampMillis;
+
+ // Client timestamp when the beacon was first observed in BLE scan.
+ long firstObservationTimestampMillis;
+
+ // Item's current state. e.g. if the item is blocked.
+ int state;
+
+ // The resolved url type for the action_url.
+ int actionUrlType;
+
+ // The timestamp when the user is redirected to Play Store after clicking on
+ // the item.
+ long pendingAppInstallTimestampMillis;
+
+ // Beacon's RSSI value
+ int rssi;
+
+ // Beacon's tx power
+ int txPower;
+
+ // Human readable name of the app designated to open the uri
+ // Used in the second line of the notification, "Open in {} app"
+ String appName;
+
+ // Package name of the App that owns this item.
+ String packageName;
+
+ // TriggerId identifies the trigger/beacon that is attached with a message.
+ // It's generated from server for online messages to synchronize formatting
+ // across client versions.
+ // Example:
+ // * BLE_UID: 3||deadbeef
+ // * BLE_URL: http://trigger.id
+ // See go/discovery-store-message-and-trigger-id for more details.
+ String triggerId;
+
+ // Bytes of item icon in PNG format displayed in Discovery item list.
+ byte[] iconPng;
+
+ // A FIFE URL of the item icon displayed in Discovery item list.
+ String iconFifeUrl;
+
+ // Fast Pair antispoof key.
+ byte[] authenticationPublicKeySecp256r1;
+}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairEligibleAccountParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairEligibleAccountParcel.aidl
new file mode 100644
index 0000000..747758d
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/FastPairEligibleAccountParcel.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.aidl;
+
+import android.accounts.Account;
+
+/**
+ * Fast Pair Eligible Account.
+ * {@hide}
+ */
+parcelable FastPairEligibleAccountParcel {
+ Account account;
+ // Whether the account opts in Fast Pair.
+ boolean optIn;
+}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairEligibleAccountsRequestParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairEligibleAccountsRequestParcel.aidl
new file mode 100644
index 0000000..8db3356
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/FastPairEligibleAccountsRequestParcel.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.aidl;
+
+/**
+ * Request details for Fast Pair eligible accounts.
+ * Empty place holder for future expansion.
+ * {@hide}
+ */
+parcelable FastPairEligibleAccountsRequestParcel {
+}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairManageAccountDeviceRequestParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairManageAccountDeviceRequestParcel.aidl
new file mode 100644
index 0000000..59834b2
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/FastPairManageAccountDeviceRequestParcel.aidl
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.aidl;
+
+import android.accounts.Account;
+import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
+
+/**
+ * Request details for managing Fast Pair device-account mapping.
+ * {@hide}
+ */
+ // TODO(b/204780849): remove unnecessary fields and polish comments.
+parcelable FastPairManageAccountDeviceRequestParcel {
+ Account account;
+ // MANAGE_ACCOUNT_DEVICE_ADD: add Fast Pair device to the account.
+ // MANAGE_ACCOUNT_DEVICE_REMOVE: remove Fast Pair device from the account.
+ int requestType;
+ // Fast Pair account key-ed device metadata.
+ FastPairAccountKeyDeviceMetadataParcel accountKeyDeviceMetadata;
+}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/FastPairManageAccountRequestParcel.aidl b/nearby/framework/java/android/nearby/aidl/FastPairManageAccountRequestParcel.aidl
new file mode 100644
index 0000000..3d92064
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/FastPairManageAccountRequestParcel.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.aidl;
+
+import android.accounts.Account;
+
+/**
+ * Request details for managing a Fast Pair account.
+ *
+ * {@hide}
+ */
+parcelable FastPairManageAccountRequestParcel {
+ Account account;
+ // MANAGE_ACCOUNT_OPT_IN: opt account into Fast Pair.
+ // MANAGE_ACCOUNT_OPT_OUT: opt account out of Fast Pair.
+ int requestType;
+}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/IFastPairAccountDevicesMetadataCallback.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairAccountDevicesMetadataCallback.aidl
new file mode 100644
index 0000000..7db18d0
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/IFastPairAccountDevicesMetadataCallback.aidl
@@ -0,0 +1,28 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package android.nearby.aidl;
+
+import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
+
+/**
+ * Provides callback interface for OEMs to send back metadata of FastPair
+ * devices associated with an account.
+ *
+ * {@hide}
+ */
+interface IFastPairAccountDevicesMetadataCallback {
+ void onFastPairAccountDevicesMetadataReceived(in FastPairAccountKeyDeviceMetadataParcel[] accountDevicesMetadata);
+ void onError(int code, String message);
+}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/IFastPairAntispoofKeyDeviceMetadataCallback.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairAntispoofKeyDeviceMetadataCallback.aidl
new file mode 100644
index 0000000..38abba4
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/IFastPairAntispoofKeyDeviceMetadataCallback.aidl
@@ -0,0 +1,27 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package android.nearby.aidl;
+
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
+
+/**
+ * Provides callback interface for OEMs to send FastPair AntispoofKey Device metadata back.
+ *
+ * {@hide}
+ */
+interface IFastPairAntispoofKeyDeviceMetadataCallback {
+ void onFastPairAntispoofKeyDeviceMetadataReceived(in FastPairAntispoofKeyDeviceMetadataParcel metadata);
+ void onError(int code, String message);
+}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/IFastPairDataProvider.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairDataProvider.aidl
new file mode 100644
index 0000000..2956211
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/IFastPairDataProvider.aidl
@@ -0,0 +1,44 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package android.nearby.aidl;
+
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataRequestParcel;
+import android.nearby.aidl.IFastPairAntispoofKeyDeviceMetadataCallback;
+import android.nearby.aidl.FastPairAccountDevicesMetadataRequestParcel;
+import android.nearby.aidl.IFastPairAccountDevicesMetadataCallback;
+import android.nearby.aidl.FastPairEligibleAccountsRequestParcel;
+import android.nearby.aidl.IFastPairEligibleAccountsCallback;
+import android.nearby.aidl.FastPairManageAccountRequestParcel;
+import android.nearby.aidl.IFastPairManageAccountCallback;
+import android.nearby.aidl.FastPairManageAccountDeviceRequestParcel;
+import android.nearby.aidl.IFastPairManageAccountDeviceCallback;
+
+/**
+ * Interface for communicating with the fast pair providers.
+ *
+ * {@hide}
+ */
+oneway interface IFastPairDataProvider {
+ void loadFastPairAntispoofKeyDeviceMetadata(in FastPairAntispoofKeyDeviceMetadataRequestParcel request,
+ in IFastPairAntispoofKeyDeviceMetadataCallback callback);
+ void loadFastPairAccountDevicesMetadata(in FastPairAccountDevicesMetadataRequestParcel request,
+ in IFastPairAccountDevicesMetadataCallback callback);
+ void loadFastPairEligibleAccounts(in FastPairEligibleAccountsRequestParcel request,
+ in IFastPairEligibleAccountsCallback callback);
+ void manageFastPairAccount(in FastPairManageAccountRequestParcel request,
+ in IFastPairManageAccountCallback callback);
+ void manageFastPairAccountDevice(in FastPairManageAccountDeviceRequestParcel request,
+ in IFastPairManageAccountDeviceCallback callback);
+}
diff --git a/nearby/framework/java/android/nearby/aidl/IFastPairEligibleAccountsCallback.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairEligibleAccountsCallback.aidl
new file mode 100644
index 0000000..9990014
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/IFastPairEligibleAccountsCallback.aidl
@@ -0,0 +1,28 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package android.nearby.aidl;
+
+import android.accounts.Account;
+import android.nearby.aidl.FastPairEligibleAccountParcel;
+
+/**
+ * Provides callback interface for OEMs to return FastPair Eligible accounts.
+ *
+ * {@hide}
+ */
+interface IFastPairEligibleAccountsCallback {
+ void onFastPairEligibleAccountsReceived(in FastPairEligibleAccountParcel[] accounts);
+ void onError(int code, String message);
+ }
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/IFastPairManageAccountCallback.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairManageAccountCallback.aidl
new file mode 100644
index 0000000..6b4aaee
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/IFastPairManageAccountCallback.aidl
@@ -0,0 +1,25 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package android.nearby.aidl;
+
+/**
+ * Provides callback interface to send response for account management request.
+ *
+ * {@hide}
+ */
+interface IFastPairManageAccountCallback {
+ void onSuccess();
+ void onError(int code, String message);
+}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/IFastPairManageAccountDeviceCallback.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairManageAccountDeviceCallback.aidl
new file mode 100644
index 0000000..bffc533
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/IFastPairManageAccountDeviceCallback.aidl
@@ -0,0 +1,26 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package android.nearby.aidl;
+
+/**
+ * Provides callback interface to send response for account-device mapping
+ * management request.
+ *
+ * {@hide}
+ */
+interface IFastPairManageAccountDeviceCallback {
+ void onSuccess();
+ void onError(int code, String message);
+}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/aidl/IFastPairStatusCallback.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairStatusCallback.aidl
new file mode 100644
index 0000000..d844c06
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/IFastPairStatusCallback.aidl
@@ -0,0 +1,32 @@
+/*
+ * 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.aidl;
+
+import android.nearby.FastPairDevice;
+import android.nearby.PairStatusMetadata;
+
+/**
+ *
+ * Provides callbacks for Fast Pair foreground activity to learn about paring status from backend.
+ *
+ * {@hide}
+ */
+interface IFastPairStatusCallback {
+
+ /** Reports a pair status related metadata associated with a {@link FastPairDevice} */
+ void onPairUpdate(in FastPairDevice fastPairDevice, in PairStatusMetadata pairStatusMetadata);
+}
diff --git a/nearby/framework/java/android/nearby/aidl/IFastPairUiService.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairUiService.aidl
new file mode 100644
index 0000000..9200a9d
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/IFastPairUiService.aidl
@@ -0,0 +1,37 @@
+/*
+ * 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.aidl;
+
+import android.nearby.aidl.IFastPairStatusCallback;
+import android.nearby.FastPairDevice;
+
+/**
+ * 0p API for controlling Fast Pair. Used to talk between foreground activities
+ * and background services.
+ *
+ * {@hide}
+ */
+interface IFastPairUiService {
+
+ void registerCallback(in IFastPairStatusCallback fastPairStatusCallback);
+
+ void unregisterCallback(in IFastPairStatusCallback fastPairStatusCallback);
+
+ void connect(in FastPairDevice fastPairDevice);
+
+ void cancel(in FastPairDevice fastPairDevice);
+}
\ No newline at end of file
diff --git a/nearby/halfsheet/Android.bp b/nearby/halfsheet/Android.bp
new file mode 100644
index 0000000..486a3ff
--- /dev/null
+++ b/nearby/halfsheet/Android.bp
@@ -0,0 +1,57 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+ name: "HalfSheetUX",
+ defaults: ["platform_app_defaults"],
+ srcs: ["src/**/*.java"],
+ sdk_version: "module_current",
+ // This is included in tethering apex, which uses min SDK 30
+ min_sdk_version: "30",
+ target_sdk_version: "current",
+ updatable: true,
+ certificate: ":com.android.nearby.halfsheetcertificate",
+ libs: [
+ "framework-bluetooth",
+ "framework-connectivity-t",
+ "nearby-service-string",
+ ],
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.fragment_fragment",
+ "androidx-constraintlayout_constraintlayout",
+ "androidx.localbroadcastmanager_localbroadcastmanager",
+ "androidx.core_core",
+ "androidx.appcompat_appcompat",
+ "androidx.recyclerview_recyclerview",
+ "androidx.lifecycle_lifecycle-runtime",
+ "androidx.lifecycle_lifecycle-extensions",
+ "com.google.android.material_material",
+ "fast-pair-lite-protos",
+ ],
+ plugins: ["java_api_finder"],
+ manifest: "AndroidManifest.xml",
+ jarjar_rules: ":nearby-jarjar-rules",
+ apex_available: ["com.android.tethering",],
+ lint: { strict_updatability_linting: true }
+}
+
+android_app_certificate {
+ name: "com.android.nearby.halfsheetcertificate",
+ certificate: "apk-certs/com.android.nearby.halfsheet"
+}
diff --git a/nearby/halfsheet/AndroidManifest.xml b/nearby/halfsheet/AndroidManifest.xml
new file mode 100644
index 0000000..22987fb
--- /dev/null
+++ b/nearby/halfsheet/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.nearby.halfsheet">
+ <application>
+ <activity
+ android:name="com.android.nearby.halfsheet.HalfSheetActivity"
+ android:exported="true"
+ android:launchMode="singleInstance"
+ android:theme="@style/HalfSheetStyle" >
+ <intent-filter>
+ <action android:name="android.nearby.SHOW_HALFSHEET"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/nearby/halfsheet/apk-certs/com.android.nearby.halfsheet.pk8 b/nearby/halfsheet/apk-certs/com.android.nearby.halfsheet.pk8
new file mode 100644
index 0000000..187d51e
--- /dev/null
+++ b/nearby/halfsheet/apk-certs/com.android.nearby.halfsheet.pk8
Binary files differ
diff --git a/nearby/halfsheet/apk-certs/com.android.nearby.halfsheet.x509.pem b/nearby/halfsheet/apk-certs/com.android.nearby.halfsheet.x509.pem
new file mode 100644
index 0000000..440c524
--- /dev/null
+++ b/nearby/halfsheet/apk-certs/com.android.nearby.halfsheet.x509.pem
@@ -0,0 +1,34 @@
+-----BEGIN CERTIFICATE-----
+MIIF6zCCA9OgAwIBAgIUU5ATKevcNA5ZSurwgwGenwrr4c4wDQYJKoZIhvcNAQEL
+BQAwgYMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMQwwCgYDVQQH
+DANNVFYxDzANBgNVBAoMBkdvb2dsZTEPMA0GA1UECwwGbmVhcmJ5MQswCQYDVQQD
+DAJ3czEiMCAGCSqGSIb3DQEJARYTd2VpY2VzdW5AZ29vZ2xlLmNvbTAgFw0yMTEy
+MDgwMTMxMzFaGA80NzU5MTEwNDAxMzEzMVowgYMxCzAJBgNVBAYTAlVTMRMwEQYD
+VQQIDApDYWxpZm9ybmlhMQwwCgYDVQQHDANNVFYxDzANBgNVBAoMBkdvb2dsZTEP
+MA0GA1UECwwGbmVhcmJ5MQswCQYDVQQDDAJ3czEiMCAGCSqGSIb3DQEJARYTd2Vp
+Y2VzdW5AZ29vZ2xlLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
+AO0JW1YZ5bKHZG5B9eputz3kGREmXcWZ97dg/ODDs3+op4ulBmgaYeo5yeCy29GI
+Sjgxo4G+9fNZ7Fejrk5/LLWovAoRvVxnkRxCkTfp15jZpKNnZjT2iTRLXzNz2O04
+cC0jB81mu5vJ9a8pt+EQkuSwjDMiUi6q4Sf6IRxtTCd5a1yn9eHf1y2BbCmU+Eys
+bs97HJl9PgMCp7hP+dYDxEtNTAESg5IpJ1i7uINgPNl8d0tvJ9rOEdy0IcdeGwt/
+t0L9fIoRCePttH+idKIyDjcNyp9WtX2/wZKlsGap83rGzLdL2PI4DYJ2Ytmy8W3a
+9qFJNrhl3Q3BYgPlcCg9qQOIKq6ZJgFFH3snVDKvtSFd8b9ofK7UzD5g2SllTqDA
+4YvrdK4GETQunSjG7AC/2PpvN/FdhHm7pBi0fkgwykMh35gv0h8mmb6pBISYgr85
++GMBilNiNJ4G6j3cdOa72pvfDW5qn5dn5ks8cIgW2X1uF/GT8rR6Mb2rwhjY9eXk
+TaP0RykyzheMY/7dWeA/PdN3uMCEJEt72ZakDIswgQVPCIw8KQPIf6pl0d5hcLSV
+QzhqBaXudseVg0QlZ86iaobpZvCrW0KqQmMU5GVhEtDc2sPe5e+TCmUC/H+vo8F8
+1UYu3MJaBcpePFlgIsLhW0niUTfCq2FiNrPykOJT7U9NAgMBAAGjUzBRMB0GA1Ud
+DgQWBBQKSepRcKTv9hr8mmKjYCL7NeG2izAfBgNVHSMEGDAWgBQKSepRcKTv9hr8
+mmKjYCL7NeG2izAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQC/
+BoItafzvjYPzENY16BIkgRqJVU7IosWxGLczzg19NFu6HPa54alqkawp7RI1ZNVH
+bJjQma5ap0L+Y06/peIU9rvEtfbCkkYJwvIaSRlTlzrNwNEcj3yJMmGTr/wfIzq8
+PN1t0hihnqI8ZguOPC+sV6ARoC+ygkwaLU1oPbVvOGz9WplvSokE1mvtqKAyuDoL
+LZfWwbhxRAgwgCIEz6cPfEcgg3Xzc+L4OzmNhTTc7GNOAtvvW7Zqc2Lohb8nQMNw
+uY65yiHPNmjmc+xLHZk3jQg82tKv792JJRkVXPsIfQV087IzxFFjjvKy82rVfeaN
+F9g2EpUvdjtm8zx7K5tiDv9Es/Up7oOnoB5baLgnMAEVMTZY+4k/6BfVM5CVUu+H
+AO1yh2yeNWbzY8B+zxRef3C2Ax68lJHFyz8J1pfrGpWxML3rDmWiVDMtEk73t3g+
+lcyLYo7OW+iBn6BODRcINO4R640oyMjFz2wPSPAsU0Zj/MbgC6iaS+goS3QnyPQS
+O3hKWfwqQuA7BZ0la1n+plKH5PKxQESAbd37arzCsgQuktl33ONiwYOt6eUyHl/S
+E3ZdldkmGm9z0mcBYG9NczDBSYmtuZOGjEzIRqI5GFD2WixE+dqTzVP/kyBd4BLc
+OTmBynN/8D/qdUZNrT+tgs+mH/I2SsKYW9Zymwf7Qw==
+-----END CERTIFICATE-----
diff --git a/nearby/halfsheet/apk-certs/key.pem b/nearby/halfsheet/apk-certs/key.pem
new file mode 100644
index 0000000..e9f4288
--- /dev/null
+++ b/nearby/halfsheet/apk-certs/key.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDtCVtWGeWyh2Ru
+QfXqbrc95BkRJl3Fmfe3YPzgw7N/qKeLpQZoGmHqOcngstvRiEo4MaOBvvXzWexX
+o65Ofyy1qLwKEb1cZ5EcQpE36deY2aSjZ2Y09ok0S18zc9jtOHAtIwfNZrubyfWv
+KbfhEJLksIwzIlIuquEn+iEcbUwneWtcp/Xh39ctgWwplPhMrG7PexyZfT4DAqe4
+T/nWA8RLTUwBEoOSKSdYu7iDYDzZfHdLbyfazhHctCHHXhsLf7dC/XyKEQnj7bR/
+onSiMg43DcqfVrV9v8GSpbBmqfN6xsy3S9jyOA2CdmLZsvFt2vahSTa4Zd0NwWID
+5XAoPakDiCqumSYBRR97J1Qyr7UhXfG/aHyu1Mw+YNkpZU6gwOGL63SuBhE0Lp0o
+xuwAv9j6bzfxXYR5u6QYtH5IMMpDId+YL9IfJpm+qQSEmIK/OfhjAYpTYjSeBuo9
+3HTmu9qb3w1uap+XZ+ZLPHCIFtl9bhfxk/K0ejG9q8IY2PXl5E2j9EcpMs4XjGP+
+3VngPz3Td7jAhCRLe9mWpAyLMIEFTwiMPCkDyH+qZdHeYXC0lUM4agWl7nbHlYNE
+JWfOomqG6Wbwq1tCqkJjFORlYRLQ3NrD3uXvkwplAvx/r6PBfNVGLtzCWgXKXjxZ
+YCLC4VtJ4lE3wqthYjaz8pDiU+1PTQIDAQABAoICAQCt4R5CM+8enlka1IIbvann
+2cpVnUpOaNqhh6EZFBY5gDOfqafgd/H5yvh/P1UnCI5BWJBz3ew33nAT/fsglAPt
+ImEGFetNvJ9jFqXGWWCRPJ6cS35bPbp6RQwKB2JK6grH4ZmYoFLhPi5elwDPNcQ7
+xBKkc/nLSAiwtbjSTI7/qf8K0h752aTUOctpWWEnhZon00ywf4Ic3TbBatF/n/W/
+s20coEMp1cyKN/JrVQ5uD/LGwDyBModB2lWpFSxLrB14I9DWyxbxP28X7ckXLhbl
+ZdWMOyQZoa/S7n5PYT49g1Wq5BW54UpvuH5c6fpWtrgSqk1cyUR2EbTf3NAAhPLU
+PgPK8wbFMcMB3TpQDXl7USA7QX5wSv22OfhivPsHQ9szGM0f84mK0PhXYPWBiNUY
+Y8rrIjOijB4eFGDFnTIMTofAb07NxRThci710BYUqgBVTBG5N+avIesjwkikMjOI
+PwYukKSQSw/Tqxy5Z9l22xksGynBZFjEFs/WT5pDczPAktA4xW3CGxjkMsIYaOBs
+OCEujqc5+mHSywYvy8aN+nA+yPucJP5e5pLZ1qaU0tqyakCx8XeeOyP6Wfm3UAAV
+AYelBRcWcJxM51w4o5UnUnpBD+Uxiz1sRVlqa9bLJjP4M+wJNL+WaIn9D6WhPOvl
++naDC+p29ou2JzyKFDsOQQKCAQEA+Jalm+xAAPc+t/gCdAqEDo0NMA2/NG8m9BRc
+CVZRRaWVyGPeg5ziT/7caGwy2jpOZEjK0OOTCAqF+sJRDj6DDIw7nDrlxNyaXnCF
+gguQHFIYaHcjKGTs5l0vgL3H7pMFHN2qVynf4xrTuBXyT1GJ4vdWKAJbooa02c8W
+XI2fjwZ7Y8wSWrm1tn3oTTBR3N6o1GyPY6/TrL0mhpWwgx5eJeLl3GuUxOhXY5R9
+y48ziS97Dqdq75MxUOHickofCNcm7p+jA8Hg+SxLMR/kUFsXOxawmvsBqdL1XzU5
+LTS7xAEY9iMuBcO6yIxcxqBx96idjsPXx1lgARo1CpaZYCzgPQKCAQEA9BqKMN/Y
+o+T+ac99St8x3TYkk5lkvLVqlPw+EQhEqrm9EEBPntxWM5FEIpPVmFm7taGTgPfN
+KKaaNxX5XyK9B2v1QqN7XrX0nF4+6x7ao64fdpRUParIuBVctqzQWWthme66eHrf
+L86T/tkt3o/7p+Hd4Z9UT3FaAew1ggWr00xz5PJ/4b3f3mRmtNmgeTYskWMxOpSj
+bEenom4Row7sfLNeXNSWDGlzJ/lf6svvbVM2X5h2uFsxlt/Frq9ooTA3wwhnbd1i
+cFifDQ6cxF5mBpz/V/hnlHVfuXlknEZa9EQXHNo/aC9y+bR+ai05FJyK/WgqleW8
+5PBmoTReWA2MUQKCAQAnnnLkh+GnhcBEN83ESszDOO3KI9a+d5yguAH3Jv+q9voJ
+Rwl2tnFHSJo+NkhgiXxm9UcFxc9wL6Us0v1yJLpkLJFvk9984Z/kv1A36rncGaV0
+ONCspnEvQdjJTvXnax0cfaOhYrYhDuyBYVYOGDO+rabYl4+dNpTqRdwNgjDU7baK
+sEKYnRJ99FEqxDG33vDPckHkJGi7FiZmusK4EwX0SdZSq/6450LORyNJZxhSm/Oj
+4UDkz/PDLU0W5ANQOGInE+A6QBMoA0w0lx2fRPVN4I7jFHAubcXXl7b2InpugbJF
+wFOcbZZ+UgiTS4z+aKw7zbC9P9xSMKgVeO0W6/ANAoIBABe0LA8q7YKczgfAWk5W
+9iShCVQ75QheJYdqJyzIPMLHXpChbhnjE4vWY2NoL6mnrQ6qLgSsC4QTCY6n15th
+aDG8Tgi2j1hXGvXEQR/b0ydp1SxSowuJ9gvKJ0Kl7WWBg+zKvdjNNbcSvFRXCpk+
+KhXXXRB3xFwiibb+FQQXQOQ33FkzIy/snDygS0jsiSS8Gf/UPgeOP4BYRPME9Tl8
+TYKeeF9TVW7HHqOXF7VZMFrRZcpKp9ynHl2kRTH9Xo+oewG5YzHL+a8nK+q8rIR1
+Fjs2K6WDPauw6ia8nwR94H8vzX7Dwrx/Pw74c/4jfhN+UBDjeJ8tu/YPUif9SdwL
+FMECggEALdCGKfQ4vPmqI6UdfVB5hdCPoM6tUsI2yrXFvlHjSGVanC/IG9x2mpRb
+4odamLYx4G4NjP1IJSY08LFT9VhLZtRM1W3fGeboW12LTEVNrI3lRBU84rAQ1ced
+l6/DvTKJjhfwTxb/W7sqmZY5hF3QuNxs67Z8x0pe4b58musa0qFCs4Sa8qTNZKRW
+fIbxIKuvu1HSNOKkZLu6Gq8km+XIlVAaSVA03Tt+EK74MFL6+pcd7/VkS00MAYUC
+gS4ic+QFzCl5P8zl/GoX8iUFsRZQCSJkZ75VwO13pEupVwCAW8WWJO83U4jBsnJs
+ayrX7pbsnW6jsNYBUlck+RYVYkVkxA==
+-----END PRIVATE KEY-----
diff --git a/nearby/halfsheet/res/anim/fast_pair_bottom_sheet_enter.xml b/nearby/halfsheet/res/anim/fast_pair_bottom_sheet_enter.xml
new file mode 100644
index 0000000..098dccb
--- /dev/null
+++ b/nearby/halfsheet/res/anim/fast_pair_bottom_sheet_enter.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:interpolator/decelerate_quint">
+ <translate android:fromYDelta="100%"
+ android:toYDelta="0"
+ android:duration="900"/>
+</set>
diff --git a/nearby/halfsheet/res/anim/fast_pair_bottom_sheet_exit.xml b/nearby/halfsheet/res/anim/fast_pair_bottom_sheet_exit.xml
new file mode 100644
index 0000000..1cf7401
--- /dev/null
+++ b/nearby/halfsheet/res/anim/fast_pair_bottom_sheet_exit.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:interpolator/decelerate_quint">
+ <translate android:fromYDelta="0"
+ android:toYDelta="100%"
+ android:duration="500"/>
+</set>
diff --git a/nearby/halfsheet/res/anim/fast_pair_half_sheet_slide_in.xml b/nearby/halfsheet/res/anim/fast_pair_half_sheet_slide_in.xml
new file mode 100644
index 0000000..9a51ddb
--- /dev/null
+++ b/nearby/halfsheet/res/anim/fast_pair_half_sheet_slide_in.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:targetApi="23"
+ android:duration="@integer/half_sheet_slide_in_duration"
+ android:interpolator="@android:interpolator/fast_out_slow_in">
+ <translate
+ android:fromYDelta="100%p"
+ android:toYDelta="0%p"/>
+
+ <alpha
+ android:fromAlpha="0.0"
+ android:toAlpha="1.0"/>
+</set>
diff --git a/nearby/halfsheet/res/anim/fast_pair_half_sheet_slide_out.xml b/nearby/halfsheet/res/anim/fast_pair_half_sheet_slide_out.xml
new file mode 100644
index 0000000..c589482
--- /dev/null
+++ b/nearby/halfsheet/res/anim/fast_pair_half_sheet_slide_out.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:duration="@integer/half_sheet_fade_out_duration"
+ android:interpolator="@android:interpolator/fast_out_slow_in">
+
+ <translate
+ android:fromYDelta="0%p"
+ android:toYDelta="100%p"/>
+
+ <alpha
+ android:fromAlpha="1.0"
+ android:toAlpha="0.0"/>
+
+</set>
diff --git a/nearby/halfsheet/res/drawable/fast_pair_ic_info.xml b/nearby/halfsheet/res/drawable/fast_pair_ic_info.xml
new file mode 100644
index 0000000..7d61d1c
--- /dev/null
+++ b/nearby/halfsheet/res/drawable/fast_pair_ic_info.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="@color/fast_pair_half_sheet_subtitle_color">
+ <path
+ android:fillColor="@color/fast_pair_half_sheet_subtitle_color"
+ android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/>
+</vector>
\ No newline at end of file
diff --git a/nearby/halfsheet/res/drawable/fastpair_outline.xml b/nearby/halfsheet/res/drawable/fastpair_outline.xml
new file mode 100644
index 0000000..6765e11
--- /dev/null
+++ b/nearby/halfsheet/res/drawable/fastpair_outline.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <stroke
+ android:width="1dp"
+ android:color="@color/fast_pair_notification_image_outline"/>
+</shape>
diff --git a/nearby/halfsheet/res/drawable/half_sheet_bg.xml b/nearby/halfsheet/res/drawable/half_sheet_bg.xml
new file mode 100644
index 0000000..7e7d8dd
--- /dev/null
+++ b/nearby/halfsheet/res/drawable/half_sheet_bg.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:targetApi="23">
+ <solid android:color="@color/fast_pair_half_sheet_background" />
+ <corners
+ android:topLeftRadius="16dp"
+ android:topRightRadius="16dp"
+ android:padding="8dp"/>
+</shape>
diff --git a/nearby/halfsheet/res/layout/fast_pair_device_pairing_fragment.xml b/nearby/halfsheet/res/layout/fast_pair_device_pairing_fragment.xml
new file mode 100644
index 0000000..7fbe229
--- /dev/null
+++ b/nearby/halfsheet/res/layout/fast_pair_device_pairing_fragment.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:orientation="vertical"
+ tools:ignore="RtlCompat"
+ android:layout_width="match_parent" android:layout_height="match_parent">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/image_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="340dp"
+ android:paddingStart="12dp"
+ android:paddingEnd="12dp"
+ android:paddingTop="12dp"
+ android:paddingBottom="12dp">
+ <TextView
+ android:id="@+id/header_subtitle"
+ android:textColor="@color/fast_pair_half_sheet_subtitle_color"
+ android:fontFamily="google-sans"
+ android:textSize="14sp"
+ android:maxLines="3"
+ android:gravity="center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <ImageView
+ android:id="@+id/pairing_pic"
+ android:layout_width="@dimen/fast_pair_half_sheet_image_size"
+ android:layout_height="@dimen/fast_pair_half_sheet_image_size"
+ android:paddingTop="18dp"
+ android:paddingBottom="18dp"
+ android:importantForAccessibility="no"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/header_subtitle" />
+
+ <TextView
+ android:id="@+id/pin_code"
+ android:textColor="@color/fast_pair_half_sheet_subtitle_color"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/fast_pair_half_sheet_image_size"
+ android:paddingTop="18dp"
+ android:paddingBottom="18dp"
+ android:visibility="invisible"
+ android:textSize="50sp"
+ android:letterSpacing="0.2"
+ android:fontFamily="google-sans-medium"
+ android:gravity="center"
+ android:importantForAccessibility="yes"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/header_subtitle" />
+
+ <ProgressBar
+ android:id="@+id/connect_progressbar"
+ android:layout_width="@dimen/fast_pair_half_sheet_image_size"
+ android:layout_height="2dp"
+ android:indeterminate="true"
+ android:indeterminateTint="@color/fast_pair_progress_color"
+ android:indeterminateTintMode="src_in"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_marginBottom="6dp"
+ app:layout_constraintTop_toBottomOf="@+id/pairing_pic"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toBottomOf="@+id/connect_progressbar"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent">
+
+ <ImageView
+ android:id="@+id/info_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ app:srcCompat="@drawable/fast_pair_ic_info"
+ android:layout_centerInParent="true"
+ android:contentDescription="@null"
+ android:layout_marginEnd="10dp"
+ android:layout_toStartOf="@id/connect_btn"
+ android:visibility="invisible" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/connect_btn"
+ android:layout_width="@dimen/fast_pair_half_sheet_image_size"
+ android:layout_height="wrap_content"
+ android:text="@string/paring_action_connect"
+ android:layout_centerInParent="true"
+ style="@style/HalfSheetButton" />
+
+ </RelativeLayout>
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/settings_btn"
+ android:text="@string/paring_action_settings"
+ android:layout_height="wrap_content"
+ android:layout_width="@dimen/fast_pair_half_sheet_image_size"
+ app:layout_constraintTop_toBottomOf="@+id/connect_progressbar"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ android:visibility="invisible"
+ style="@style/HalfSheetButton" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/cancel_btn"
+ android:text="@string/paring_action_done"
+ android:visibility="invisible"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:gravity="start|center_vertical"
+ android:layout_marginTop="6dp"
+ style="@style/HalfSheetButtonBorderless"/>
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/setup_btn"
+ android:text="@string/paring_action_launch"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:layout_marginTop="6dp"
+ android:layout_marginBottom="16dp"
+ android:background="@color/fast_pair_half_sheet_button_color"
+ android:visibility="invisible"
+ android:layout_height="@dimen/fast_pair_half_sheet_bottom_button_height"
+ android:layout_width="wrap_content"
+ style="@style/HalfSheetButton" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</LinearLayout>
diff --git a/nearby/halfsheet/res/layout/fast_pair_half_sheet.xml b/nearby/halfsheet/res/layout/fast_pair_half_sheet.xml
new file mode 100644
index 0000000..705aa1b
--- /dev/null
+++ b/nearby/halfsheet/res/layout/fast_pair_half_sheet.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="RtlCompat, UselessParent, MergeRootFrame"
+ android:id="@+id/background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:id="@+id/card"
+ android:orientation="vertical"
+ android:transitionName="card"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_gravity= "center|bottom"
+ android:paddingLeft="12dp"
+ android:paddingRight="12dp"
+ android:background="@drawable/half_sheet_bg"
+ android:accessibilityLiveRegion="polite"
+ android:gravity="bottom">
+
+ <RelativeLayout
+ android:id="@+id/toolbar_wrapper"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="20dp"
+ android:paddingRight="20dp">
+
+ <ImageView
+ android:layout_marginTop="9dp"
+ android:layout_marginBottom="9dp"
+ android:id="@+id/toolbar_image"
+ android:layout_width="42dp"
+ android:layout_height="42dp"
+ android:contentDescription="@null"
+ android:layout_toStartOf="@id/toolbar_title"
+ android:layout_centerHorizontal="true"
+ android:visibility="invisible"/>
+
+ <TextView
+ android:layout_marginTop="18dp"
+ android:layout_marginBottom="18dp"
+ android:layout_centerHorizontal="true"
+ android:id="@+id/toolbar_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fontFamily="google-sans-medium"
+ android:textSize="24sp"
+ android:textColor="@color/fast_pair_half_sheet_text_color"
+ style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />
+ </RelativeLayout>
+
+ <FrameLayout
+ android:id="@+id/fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ </LinearLayout>
+
+</FrameLayout>
+
diff --git a/nearby/halfsheet/res/layout/fast_pair_heads_up_notification.xml b/nearby/halfsheet/res/layout/fast_pair_heads_up_notification.xml
new file mode 100644
index 0000000..11b8343
--- /dev/null
+++ b/nearby/halfsheet/res/layout/fast_pair_heads_up_notification.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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="wrap_content"
+ android:orientation="horizontal"
+ android:baselineAligned="false"
+ android:background="@color/fast_pair_notification_background"
+ tools:ignore="ContentDescription,UnusedAttribute,RtlCompat,Overdraw">
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginTop="@dimen/fast_pair_notification_padding"
+ android:layout_marginStart="@dimen/fast_pair_notification_padding"
+ android:layout_marginEnd="@dimen/fast_pair_notification_padding">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fontFamily="sans-serif-medium"
+ android:textSize="@dimen/fast_pair_notification_text_size"
+ android:textColor="@color/fast_pair_primary_text"
+ android:layout_marginBottom="2dp"
+ android:lines="1"/>
+
+ <TextView
+ android:id="@android:id/text2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/fast_pair_notification_text_size_small"
+ android:textColor="@color/fast_pair_primary_text"
+ android:layout_marginBottom="2dp"
+ android:layout_marginStart="4dp"
+ android:lines="1"/>
+ </LinearLayout>
+
+ <TextView
+ android:id="@android:id/text1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/fast_pair_notification_text_size"
+ android:textColor="@color/fast_pair_primary_text"
+ android:maxLines="2"
+ android:ellipsize="end"
+ android:breakStrategy="simple" />
+
+ <FrameLayout
+ android:id="@android:id/secondaryProgress"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="32dp"
+ android:orientation="horizontal"
+ android:visibility="gone">
+
+ <ProgressBar
+ android:id="@android:id/progress"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:indeterminateTint="@color/discovery_activity_accent"/>
+
+ </FrameLayout>
+
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@android:id/icon1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+</LinearLayout>
diff --git a/nearby/halfsheet/res/layout/fast_pair_heads_up_notification_large_image.xml b/nearby/halfsheet/res/layout/fast_pair_heads_up_notification_large_image.xml
new file mode 100644
index 0000000..dd28947
--- /dev/null
+++ b/nearby/halfsheet/res/layout/fast_pair_heads_up_notification_large_image.xml
@@ -0,0 +1,7 @@
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/fast_pair_notification_large_image_size"
+ android:layout_height="@dimen/fast_pair_notification_large_image_size"
+ android:scaleType="fitStart"
+ tools:ignore="ContentDescription"/>
diff --git a/nearby/halfsheet/res/layout/fast_pair_heads_up_notification_small_image.xml b/nearby/halfsheet/res/layout/fast_pair_heads_up_notification_small_image.xml
new file mode 100644
index 0000000..ee1d89f
--- /dev/null
+++ b/nearby/halfsheet/res/layout/fast_pair_heads_up_notification_small_image.xml
@@ -0,0 +1,11 @@
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/fast_pair_notification_small_image_size"
+ android:layout_height="@dimen/fast_pair_notification_small_image_size"
+ android:layout_marginTop="@dimen/fast_pair_notification_padding"
+ android:layout_marginBottom="@dimen/fast_pair_notification_padding"
+ android:layout_marginStart="@dimen/fast_pair_notification_padding"
+ android:layout_marginEnd="@dimen/fast_pair_notification_padding"
+ android:scaleType="fitStart"
+ tools:ignore="ContentDescription,RtlCompat"/>
diff --git a/nearby/halfsheet/res/values-af/strings.xml b/nearby/halfsheet/res/values-af/strings.xml
new file mode 100644
index 0000000..7333e63
--- /dev/null
+++ b/nearby/halfsheet/res/values-af/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Begin tans opstelling …"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Stel toestel op"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Toestel is gekoppel"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Kon nie koppel nie"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Klaar"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Stoor"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Koppel"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Stel op"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Instellings"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-am/strings.xml b/nearby/halfsheet/res/values-am/strings.xml
new file mode 100644
index 0000000..da3b144
--- /dev/null
+++ b/nearby/halfsheet/res/values-am/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ማዋቀርን በመጀመር ላይ…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"መሣሪያ አዋቅር"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"መሣሪያ ተገናኝቷል"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"መገናኘት አልተቻለም"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"ተጠናቅቋል"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"አስቀምጥ"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"አገናኝ"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"አዋቅር"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"ቅንብሮች"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-ar/strings.xml b/nearby/halfsheet/res/values-ar/strings.xml
new file mode 100644
index 0000000..d0bfce4
--- /dev/null
+++ b/nearby/halfsheet/res/values-ar/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"جارٍ الإعداد…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"إعداد جهاز"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"تمّ إقران الجهاز"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"تعذّر الربط"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"تم"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"حفظ"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"ربط"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"إعداد"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"الإعدادات"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-as/strings.xml b/nearby/halfsheet/res/values-as/strings.xml
new file mode 100644
index 0000000..8ff4946
--- /dev/null
+++ b/nearby/halfsheet/res/values-as/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ছেটআপ আৰম্ভ কৰি থকা হৈছে…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"ডিভাইচ ছেট আপ কৰক"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"ডিভাইচ সংযোগ কৰা হ’ল"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"সংযোগ কৰিব পৰা নগ’ল"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"হ’ল"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"ছেভ কৰক"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"সংযোগ কৰক"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"ছেট আপ কৰক"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"ছেটিং"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-az/strings.xml b/nearby/halfsheet/res/values-az/strings.xml
new file mode 100644
index 0000000..af499ef
--- /dev/null
+++ b/nearby/halfsheet/res/values-az/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Ayarlama başladılır…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Cihazı quraşdırın"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Cihaz qoşulub"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Qoşulmaq mümkün olmadı"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Oldu"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Saxlayın"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Qoşun"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Ayarlayın"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Ayarlar"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-b+sr+Latn/strings.xml b/nearby/halfsheet/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..eea6b64
--- /dev/null
+++ b/nearby/halfsheet/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Podešavanje se pokreće…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Podesite uređaj"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Uređaj je povezan"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Povezivanje nije uspelo"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Gotovo"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Sačuvaj"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Poveži"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Podesi"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Podešavanja"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-be/strings.xml b/nearby/halfsheet/res/values-be/strings.xml
new file mode 100644
index 0000000..a5c1ef6
--- /dev/null
+++ b/nearby/halfsheet/res/values-be/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Пачынаецца наладжванне…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Наладзьце прыладу"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Прылада падключана"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Не ўдалося падключыцца"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Гатова"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Захаваць"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Падключыць"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Наладзіць"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Налады"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-bg/strings.xml b/nearby/halfsheet/res/values-bg/strings.xml
new file mode 100644
index 0000000..0ee7aef
--- /dev/null
+++ b/nearby/halfsheet/res/values-bg/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Настройването се стартира…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Настройване на устройството"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Устройството е свързано"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Свързването не бе успешно"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Готово"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Запазване"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Свързване"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Настройване"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Настройки"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-bn/strings.xml b/nearby/halfsheet/res/values-bn/strings.xml
new file mode 100644
index 0000000..484e35b
--- /dev/null
+++ b/nearby/halfsheet/res/values-bn/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"সেট-আপ করা শুরু হচ্ছে…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"ডিভাইস সেট-আপ করুন"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"ডিভাইস কানেক্ট হয়েছে"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"কানেক্ট করা যায়নি"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"হয়ে গেছে"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"সেভ করুন"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"কানেক্ট করুন"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"সেট-আপ করুন"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"সেটিংস"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-bs/strings.xml b/nearby/halfsheet/res/values-bs/strings.xml
new file mode 100644
index 0000000..2fc8644
--- /dev/null
+++ b/nearby/halfsheet/res/values-bs/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Pokretanje postavljanja…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Postavi uređaj"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Uređaj je povezan"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Povezivanje nije uspjelo"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Gotovo"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Sačuvaj"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Poveži"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Postavi"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Postavke"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-ca/strings.xml b/nearby/halfsheet/res/values-ca/strings.xml
new file mode 100644
index 0000000..8912792
--- /dev/null
+++ b/nearby/halfsheet/res/values-ca/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciant la configuració…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configura el dispositiu"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"El dispositiu s\'ha connectat"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"No s\'ha pogut connectar"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Fet"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Desa"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Connecta"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Configura"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Configuració"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-cs/strings.xml b/nearby/halfsheet/res/values-cs/strings.xml
new file mode 100644
index 0000000..7e7ea3c
--- /dev/null
+++ b/nearby/halfsheet/res/values-cs/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Zahajování nastavení…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Nastavení zařízení"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Zařízení je připojeno"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Nelze se připojit"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Hotovo"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Uložit"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Připojit"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Nastavit"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Nastavení"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-da/strings.xml b/nearby/halfsheet/res/values-da/strings.xml
new file mode 100644
index 0000000..1d937e2
--- /dev/null
+++ b/nearby/halfsheet/res/values-da/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Begynder konfiguration…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Konfigurer enhed"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Enheden er forbundet"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Forbindelsen kan ikke oprettes"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Luk"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Gem"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Opret forbindelse"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Konfigurer"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Indstillinger"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-de/strings.xml b/nearby/halfsheet/res/values-de/strings.xml
new file mode 100644
index 0000000..9186a44
--- /dev/null
+++ b/nearby/halfsheet/res/values-de/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Einrichtung wird gestartet..."</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Gerät einrichten"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Gerät verbunden"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Verbindung nicht möglich"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Fertig"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Speichern"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Verbinden"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Einrichten"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Einstellungen"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-el/strings.xml b/nearby/halfsheet/res/values-el/strings.xml
new file mode 100644
index 0000000..3e18a93
--- /dev/null
+++ b/nearby/halfsheet/res/values-el/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Έναρξη ρύθμισης…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Ρύθμιση συσκευής"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Η συσκευή συνδέθηκε"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Αδυναμία σύνδεσης"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Τέλος"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Αποθήκευση"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Σύνδεση"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Ρύθμιση"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Ρυθμίσεις"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-en-rAU/strings.xml b/nearby/halfsheet/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..d4ed675
--- /dev/null
+++ b/nearby/halfsheet/res/values-en-rAU/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starting setup…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Set up device"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Device connected"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Couldn\'t connect"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Done"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Save"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Connect"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Set up"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Settings"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-en-rCA/strings.xml b/nearby/halfsheet/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..d4ed675
--- /dev/null
+++ b/nearby/halfsheet/res/values-en-rCA/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starting setup…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Set up device"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Device connected"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Couldn\'t connect"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Done"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Save"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Connect"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Set up"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Settings"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-en-rGB/strings.xml b/nearby/halfsheet/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..d4ed675
--- /dev/null
+++ b/nearby/halfsheet/res/values-en-rGB/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starting setup…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Set up device"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Device connected"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Couldn\'t connect"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Done"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Save"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Connect"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Set up"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Settings"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-en-rIN/strings.xml b/nearby/halfsheet/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..d4ed675
--- /dev/null
+++ b/nearby/halfsheet/res/values-en-rIN/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starting setup…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Set up device"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Device connected"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Couldn\'t connect"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Done"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Save"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Connect"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Set up"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Settings"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-en-rXC/strings.xml b/nearby/halfsheet/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..460cc1b
--- /dev/null
+++ b/nearby/halfsheet/res/values-en-rXC/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starting Setup…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Set up device"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Device connected"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Couldn\'t connect"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Done"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Save"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Connect"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Set up"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Settings"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-es-rUS/strings.xml b/nearby/halfsheet/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..d8fb283
--- /dev/null
+++ b/nearby/halfsheet/res/values-es-rUS/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciando la configuración…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configuración del dispositivo"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Se conectó el dispositivo"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"No se pudo establecer conexión"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Listo"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Guardar"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Conectar"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Configurar"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Configuración"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-es/strings.xml b/nearby/halfsheet/res/values-es/strings.xml
new file mode 100644
index 0000000..4b8340a
--- /dev/null
+++ b/nearby/halfsheet/res/values-es/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciando configuración…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurar el dispositivo"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispositivo conectado"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"No se ha podido conectar"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Hecho"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Guardar"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Conectar"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Configurar"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Ajustes"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-et/strings.xml b/nearby/halfsheet/res/values-et/strings.xml
new file mode 100644
index 0000000..e6abc64
--- /dev/null
+++ b/nearby/halfsheet/res/values-et/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Seadistuse käivitamine …"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Seadistage seade"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Seade on ühendatud"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Ühendamine ebaõnnestus"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Valmis"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Salvesta"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Ühenda"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Seadistamine"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Seaded"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-eu/strings.xml b/nearby/halfsheet/res/values-eu/strings.xml
new file mode 100644
index 0000000..4243fd5
--- /dev/null
+++ b/nearby/halfsheet/res/values-eu/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Konfigurazio-prozesua abiarazten…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Konfiguratu gailua"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Konektatu da gailua"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Ezin izan da konektatu"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Eginda"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Gorde"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Konektatu"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Konfiguratu"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Ezarpenak"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-fa/strings.xml b/nearby/halfsheet/res/values-fa/strings.xml
new file mode 100644
index 0000000..3585f95
--- /dev/null
+++ b/nearby/halfsheet/res/values-fa/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"درحال شروع راهاندازی…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"راهاندازی دستگاه"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"دستگاه متصل شد"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"متصل نشد"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"تمام"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"ذخیره"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"متصل کردن"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"راهاندازی"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"تنظیمات"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-fi/strings.xml b/nearby/halfsheet/res/values-fi/strings.xml
new file mode 100644
index 0000000..e8d47de
--- /dev/null
+++ b/nearby/halfsheet/res/values-fi/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Aloitetaan käyttöönottoa…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Määritä laite"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Laite on yhdistetty"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Ei yhteyttä"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Valmis"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Tallenna"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Yhdistä"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Ota käyttöön"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Asetukset"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-fr-rCA/strings.xml b/nearby/halfsheet/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..64dd107
--- /dev/null
+++ b/nearby/halfsheet/res/values-fr-rCA/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Démarrage de la configuration…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurer l\'appareil"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Appareil associé"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Impossible d\'associer"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"OK"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Enregistrer"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Associer"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Configurer"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Paramètres"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-fr/strings.xml b/nearby/halfsheet/res/values-fr/strings.xml
new file mode 100644
index 0000000..484c57b
--- /dev/null
+++ b/nearby/halfsheet/res/values-fr/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Début de la configuration…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurer un appareil"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Appareil associé"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Impossible de se connecter"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"OK"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Enregistrer"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Connecter"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Configurer"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Paramètres"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-gl/strings.xml b/nearby/halfsheet/res/values-gl/strings.xml
new file mode 100644
index 0000000..30393ff
--- /dev/null
+++ b/nearby/halfsheet/res/values-gl/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciando configuración…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configura o dispositivo"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Conectouse o dispositivo"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Non se puido conectar"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Feito"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Gardar"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Conectar"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Configurar"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Configuración"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-gu/strings.xml b/nearby/halfsheet/res/values-gu/strings.xml
new file mode 100644
index 0000000..03b057d
--- /dev/null
+++ b/nearby/halfsheet/res/values-gu/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"સેટઅપ શરૂ કરી રહ્યાં છીએ…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"ડિવાઇસનું સેટઅપ કરો"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"ડિવાઇસ કનેક્ટ કર્યું"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"કનેક્ટ કરી શક્યા નથી"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"થઈ ગયું"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"સાચવો"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"કનેક્ટ કરો"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"સેટઅપ કરો"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"સેટિંગ"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-hi/strings.xml b/nearby/halfsheet/res/values-hi/strings.xml
new file mode 100644
index 0000000..ecd420e
--- /dev/null
+++ b/nearby/halfsheet/res/values-hi/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"सेट अप शुरू किया जा रहा है…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"डिवाइस सेट अप करें"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"डिवाइस कनेक्ट हो गया"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"कनेक्ट नहीं किया जा सका"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"हो गया"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"सेव करें"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"कनेक्ट करें"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"सेट अप करें"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"सेटिंग"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-hr/strings.xml b/nearby/halfsheet/res/values-hr/strings.xml
new file mode 100644
index 0000000..5a3de8f
--- /dev/null
+++ b/nearby/halfsheet/res/values-hr/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Pokretanje postavljanja…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Postavi uređaj"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Uređaj je povezan"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Povezivanje nije uspjelo"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Gotovo"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Spremi"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Poveži"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Postavi"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Postavke"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-hu/strings.xml b/nearby/halfsheet/res/values-hu/strings.xml
new file mode 100644
index 0000000..ba3d2e0
--- /dev/null
+++ b/nearby/halfsheet/res/values-hu/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Beállítás megkezdése…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Eszköz beállítása"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Eszköz csatlakoztatva"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Nem sikerült csatlakozni"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Kész"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Mentés"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Csatlakozás"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Beállítás"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Beállítások"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-hy/strings.xml b/nearby/halfsheet/res/values-hy/strings.xml
new file mode 100644
index 0000000..ecabd16
--- /dev/null
+++ b/nearby/halfsheet/res/values-hy/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Կարգավորում…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Կարգավորեք սարքը"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Սարքը զուգակցվեց"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Չհաջողվեց միանալ"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Պատրաստ է"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Պահել"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Միանալ"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Կարգավորել"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Կարգավորումներ"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-in/strings.xml b/nearby/halfsheet/res/values-in/strings.xml
new file mode 100644
index 0000000..dc777b2
--- /dev/null
+++ b/nearby/halfsheet/res/values-in/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Memulai Penyiapan …"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Siapkan perangkat"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Perangkat terhubung"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Tidak dapat terhubung"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Selesai"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Simpan"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Hubungkan"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Siapkan"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Setelan"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-is/strings.xml b/nearby/halfsheet/res/values-is/strings.xml
new file mode 100644
index 0000000..ee094d9
--- /dev/null
+++ b/nearby/halfsheet/res/values-is/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Ræsir uppsetningu…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Uppsetning tækis"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Tækið er tengt"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Tenging mistókst"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Lokið"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Vista"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Tengja"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Setja upp"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Stillingar"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-it/strings.xml b/nearby/halfsheet/res/values-it/strings.xml
new file mode 100644
index 0000000..700dd77
--- /dev/null
+++ b/nearby/halfsheet/res/values-it/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Avvio della configurazione…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configura dispositivo"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispositivo connesso"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Impossibile connettere"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Fine"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Salva"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Connetti"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Configura"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Impostazioni"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-iw/strings.xml b/nearby/halfsheet/res/values-iw/strings.xml
new file mode 100644
index 0000000..e6ff9b9
--- /dev/null
+++ b/nearby/halfsheet/res/values-iw/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ההגדרה מתבצעת…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"הגדרת המכשיר"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"המכשיר מחובר"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"לא ניתן להתחבר"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"סיום"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"שמירה"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"התחברות"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"הגדרה"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"הגדרות"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-ja/strings.xml b/nearby/halfsheet/res/values-ja/strings.xml
new file mode 100644
index 0000000..a429b7e
--- /dev/null
+++ b/nearby/halfsheet/res/values-ja/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"セットアップを開始中…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"デバイスのセットアップ"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"デバイス接続完了"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"接続エラー"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"完了"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"保存"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"接続"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"セットアップ"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"設定"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-ka/strings.xml b/nearby/halfsheet/res/values-ka/strings.xml
new file mode 100644
index 0000000..4353ae9
--- /dev/null
+++ b/nearby/halfsheet/res/values-ka/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"დაყენება იწყება…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"მოწყობილობის დაყენება"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"მოწყობილობა დაკავშირებულია"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"დაკავშირება ვერ მოხერხდა"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"მზადაა"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"შენახვა"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"დაკავშირება"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"დაყენება"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"პარამეტრები"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-kk/strings.xml b/nearby/halfsheet/res/values-kk/strings.xml
new file mode 100644
index 0000000..98d8073
--- /dev/null
+++ b/nearby/halfsheet/res/values-kk/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Реттеу басталуда…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Құрылғыны реттеу"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Құрылғы байланыстырылды"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Қосылмады"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Дайын"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Сақтау"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Қосу"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Реттеу"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Параметрлер"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-km/strings.xml b/nearby/halfsheet/res/values-km/strings.xml
new file mode 100644
index 0000000..85e39db
--- /dev/null
+++ b/nearby/halfsheet/res/values-km/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"កំពុងចាប់ផ្ដើមរៀបចំ…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"រៀបចំឧបករណ៍"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"បានភ្ជាប់ឧបករណ៍"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"មិនអាចភ្ជាប់បានទេ"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"រួចរាល់"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"រក្សាទុក"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"ភ្ជាប់"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"រៀបចំ"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"ការកំណត់"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-kn/strings.xml b/nearby/halfsheet/res/values-kn/strings.xml
new file mode 100644
index 0000000..fb62bb1
--- /dev/null
+++ b/nearby/halfsheet/res/values-kn/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ಸೆಟಪ್ ಪ್ರಾರಂಭಿಸಲಾಗುತ್ತಿದೆ…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"ಸಾಧನವನ್ನು ಸೆಟಪ್ ಮಾಡಿ"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"ಸಾಧನವನ್ನು ಕನೆಕ್ಟ್ ಮಾಡಲಾಗಿದೆ"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"ಕನೆಕ್ಟ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"ಮುಗಿದಿದೆ"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"ಉಳಿಸಿ"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"ಕನೆಕ್ಟ್ ಮಾಡಿ"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"ಸೆಟಪ್ ಮಾಡಿ"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"ಸೆಟ್ಟಿಂಗ್ಗಳು"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-ko/strings.xml b/nearby/halfsheet/res/values-ko/strings.xml
new file mode 100644
index 0000000..c94ff76
--- /dev/null
+++ b/nearby/halfsheet/res/values-ko/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"설정을 시작하는 중…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"기기 설정"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"기기 연결됨"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"연결할 수 없음"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"완료"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"저장"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"연결"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"설정"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"설정"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-ky/strings.xml b/nearby/halfsheet/res/values-ky/strings.xml
new file mode 100644
index 0000000..812e0e8
--- /dev/null
+++ b/nearby/halfsheet/res/values-ky/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Жөндөлүп баштады…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Түзмөктү жөндөө"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Түзмөк туташты"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Туташпай койду"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Бүттү"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Сактоо"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Туташуу"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Жөндөө"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Жөндөөлөр"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-lo/strings.xml b/nearby/halfsheet/res/values-lo/strings.xml
new file mode 100644
index 0000000..9c945b2
--- /dev/null
+++ b/nearby/halfsheet/res/values-lo/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ກຳລັງເລີ່ມການຕັ້ງຄ່າ…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"ຕັ້ງຄ່າອຸປະກອນ"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"ເຊື່ອມຕໍ່ອຸປະກອນແລ້ວ"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"ບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"ແລ້ວໆ"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"ບັນທຶກ"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"ເຊື່ອມຕໍ່"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"ຕັ້ງຄ່າ"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"ການຕັ້ງຄ່າ"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-lt/strings.xml b/nearby/halfsheet/res/values-lt/strings.xml
new file mode 100644
index 0000000..5dbad0a
--- /dev/null
+++ b/nearby/halfsheet/res/values-lt/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Pradedama sąranka…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Įrenginio nustatymas"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Įrenginys prijungtas"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Prisijungti nepavyko"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Atlikta"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Išsaugoti"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Prisijungti"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Nustatyti"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Nustatymai"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-lv/strings.xml b/nearby/halfsheet/res/values-lv/strings.xml
new file mode 100644
index 0000000..a9e1bf9
--- /dev/null
+++ b/nearby/halfsheet/res/values-lv/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Tiek sākta iestatīšana…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Iestatiet ierīci"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Ierīce ir pievienota"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Nevarēja izveidot savienojumu"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Gatavs"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Saglabāt"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Izveidot savienojumu"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Iestatīt"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Iestatījumi"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-mk/strings.xml b/nearby/halfsheet/res/values-mk/strings.xml
new file mode 100644
index 0000000..e29dfa1
--- /dev/null
+++ b/nearby/halfsheet/res/values-mk/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Се започнува со поставување…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Поставете го уредот"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Уредот е поврзан"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Не може да се поврзе"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Готово"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Зачувај"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Поврзи"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Поставете"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Поставки"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-ml/strings.xml b/nearby/halfsheet/res/values-ml/strings.xml
new file mode 100644
index 0000000..cbc171b
--- /dev/null
+++ b/nearby/halfsheet/res/values-ml/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"സജ്ജീകരിക്കൽ ആരംഭിക്കുന്നു…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"ഉപകരണം സജ്ജീകരിക്കുക"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"ഉപകരണം കണക്റ്റ് ചെയ്തു"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"കണക്റ്റ് ചെയ്യാനായില്ല"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"പൂർത്തിയായി"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"സംരക്ഷിക്കുക"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"കണക്റ്റ് ചെയ്യുക"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"സജ്ജീകരിക്കുക"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"ക്രമീകരണം"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-mn/strings.xml b/nearby/halfsheet/res/values-mn/strings.xml
new file mode 100644
index 0000000..6d21eff
--- /dev/null
+++ b/nearby/halfsheet/res/values-mn/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Тохируулгыг эхлүүлж байна…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Төхөөрөмж тохируулах"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Төхөөрөмж холбогдсон"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Холбогдож чадсангүй"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Болсон"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Хадгалах"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Холбох"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Тохируулах"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Тохиргоо"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-mr/strings.xml b/nearby/halfsheet/res/values-mr/strings.xml
new file mode 100644
index 0000000..a3e1d7a
--- /dev/null
+++ b/nearby/halfsheet/res/values-mr/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"सेटअप सुरू करत आहे…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"डिव्हाइस सेट करा"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"डिव्हाइस कनेक्ट केले आहे"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"कनेक्ट करता आले नाही"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"पूर्ण झाले"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"सेव्ह करा"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"कनेक्ट करा"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"सेट करा"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"सेटिंग्ज"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-ms/strings.xml b/nearby/halfsheet/res/values-ms/strings.xml
new file mode 100644
index 0000000..4835c1b
--- /dev/null
+++ b/nearby/halfsheet/res/values-ms/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Memulakan Persediaan…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Sediakan peranti"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Peranti disambungkan"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Tidak dapat menyambung"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Selesai"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Simpan"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Sambung"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Sediakan"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Tetapan"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-my/strings.xml b/nearby/halfsheet/res/values-my/strings.xml
new file mode 100644
index 0000000..32c3105
--- /dev/null
+++ b/nearby/halfsheet/res/values-my/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"စနစ်ထည့်သွင်းခြင်း စတင်နေသည်…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"စက်ကို စနစ်ထည့်သွင်းရန်"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"စက်ကို ချိတ်ဆက်လိုက်ပြီ"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"ချိတ်ဆက်၍မရပါ"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"ပြီးပြီ"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"သိမ်းရန်"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"ချိတ်ဆက်ရန်"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"စနစ်ထည့်သွင်းရန်"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"ဆက်တင်များ"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-nb/strings.xml b/nearby/halfsheet/res/values-nb/strings.xml
new file mode 100644
index 0000000..9d72565
--- /dev/null
+++ b/nearby/halfsheet/res/values-nb/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starter konfigureringen …"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Konfigurer enheten"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Enheten er tilkoblet"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Kunne ikke koble til"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Ferdig"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Lagre"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Koble til"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Konfigurer"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Innstillinger"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-ne/strings.xml b/nearby/halfsheet/res/values-ne/strings.xml
new file mode 100644
index 0000000..1370412
--- /dev/null
+++ b/nearby/halfsheet/res/values-ne/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"सेटअप प्रक्रिया सुरु गरिँदै छ…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"डिभाइस सेटअप गर्नुहोस्"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"डिभाइस कनेक्ट गरियो"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"कनेक्ट गर्न सकिएन"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"सम्पन्न भयो"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"सेभ गर्नुहोस्"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"कनेक्ट गर्नुहोस्"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"सेटअप गर्नुहोस्"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"सेटिङ"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-nl/strings.xml b/nearby/halfsheet/res/values-nl/strings.xml
new file mode 100644
index 0000000..4eb7624
--- /dev/null
+++ b/nearby/halfsheet/res/values-nl/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Instellen starten…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Apparaat instellen"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Apparaat verbonden"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Kan geen verbinding maken"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Klaar"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Opslaan"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Verbinden"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Instellen"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Instellingen"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-or/strings.xml b/nearby/halfsheet/res/values-or/strings.xml
new file mode 100644
index 0000000..c5e8cfc
--- /dev/null
+++ b/nearby/halfsheet/res/values-or/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ସେଟଅପ ଆରମ୍ଭ କରାଯାଉଛି…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"ଡିଭାଇସ ସେଟ ଅପ କରନ୍ତୁ"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"ଡିଭାଇସ ସଂଯୁକ୍ତ ହୋଇଛି"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"ସଂଯୋଗ କରାଯାଇପାରିଲା ନାହିଁ"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"ହୋଇଗଲା"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"ସେଭ କରନ୍ତୁ"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"ସଂଯୋଗ କରନ୍ତୁ"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"ସେଟ ଅପ କରନ୍ତୁ"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"ସେଟିଂସ"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-pa/strings.xml b/nearby/halfsheet/res/values-pa/strings.xml
new file mode 100644
index 0000000..f0523a3
--- /dev/null
+++ b/nearby/halfsheet/res/values-pa/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ਸੈੱਟਅੱਪ ਸ਼ੁਰੂ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"ਡੀਵਾਈਸ ਸੈੱਟਅੱਪ ਕਰੋ"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"ਡੀਵਾਈਸ ਕਨੈਕਟ ਕੀਤਾ ਗਿਆ"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"ਕਨੈਕਟ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"ਹੋ ਗਿਆ"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"ਰੱਖਿਅਤ ਕਰੋ"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"ਕਨੈਕਟ ਕਰੋ"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"ਸੈੱਟਅੱਪ ਕਰੋ"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"ਸੈਟਿੰਗਾਂ"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-pl/strings.xml b/nearby/halfsheet/res/values-pl/strings.xml
new file mode 100644
index 0000000..5abf5fd
--- /dev/null
+++ b/nearby/halfsheet/res/values-pl/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Rozpoczynam konfigurowanie…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Skonfiguruj urządzenie"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Urządzenie połączone"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Nie udało się połączyć"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Gotowe"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Zapisz"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Połącz"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Skonfiguruj"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Ustawienia"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-pt-rBR/strings.xml b/nearby/halfsheet/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..b021b39
--- /dev/null
+++ b/nearby/halfsheet/res/values-pt-rBR/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciando a configuração…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurar dispositivo"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispositivo conectado"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Erro ao conectar"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Concluído"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Salvar"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Conectar"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Configurar"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Configurações"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-pt-rPT/strings.xml b/nearby/halfsheet/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..3285c73
--- /dev/null
+++ b/nearby/halfsheet/res/values-pt-rPT/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"A iniciar a configuração…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configure o dispositivo"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispositivo ligado"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Não foi possível ligar"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Concluir"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Guardar"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Ligar"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Configurar"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Definições"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-pt/strings.xml b/nearby/halfsheet/res/values-pt/strings.xml
new file mode 100644
index 0000000..b021b39
--- /dev/null
+++ b/nearby/halfsheet/res/values-pt/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciando a configuração…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurar dispositivo"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispositivo conectado"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Erro ao conectar"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Concluído"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Salvar"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Conectar"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Configurar"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Configurações"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-ro/strings.xml b/nearby/halfsheet/res/values-ro/strings.xml
new file mode 100644
index 0000000..5b50f15
--- /dev/null
+++ b/nearby/halfsheet/res/values-ro/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Începe configurarea…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurați dispozitivul"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispozitivul s-a conectat"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Nu s-a putut conecta"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Gata"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Salvați"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Conectați"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Configurați"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Setări"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-ru/strings.xml b/nearby/halfsheet/res/values-ru/strings.xml
new file mode 100644
index 0000000..ee869df
--- /dev/null
+++ b/nearby/halfsheet/res/values-ru/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Начинаем настройку…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Настройка устройства"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Устройство подключено"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Ошибка подключения"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Готово"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Сохранить"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Подключить"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Настроить"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Открыть настройки"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-si/strings.xml b/nearby/halfsheet/res/values-si/strings.xml
new file mode 100644
index 0000000..f4274c2
--- /dev/null
+++ b/nearby/halfsheet/res/values-si/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"පිහිටුවීම ආරම්භ කරමින්…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"උපාංගය පිහිටුවන්න"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"උපාංගය සම්බන්ධිතයි"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"සම්බන්ධ කළ නොහැකි විය"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"නිමයි"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"සුරකින්න"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"සම්බන්ධ කරන්න"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"පිහිටුවන්න"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"සැකසීම්"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-sk/strings.xml b/nearby/halfsheet/res/values-sk/strings.xml
new file mode 100644
index 0000000..46c45af
--- /dev/null
+++ b/nearby/halfsheet/res/values-sk/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Spúšťa sa nastavenie…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Nastavte zariadenie"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Zariadenie bolo pripojené"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Nepodarilo sa pripojiť"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Hotovo"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Uložiť"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Pripojiť"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Nastaviť"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Nastavenia"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-sl/strings.xml b/nearby/halfsheet/res/values-sl/strings.xml
new file mode 100644
index 0000000..e4f3c91
--- /dev/null
+++ b/nearby/halfsheet/res/values-sl/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Začetek nastavitve …"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Nastavitev naprave"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Naprava je povezana"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Povezava ni mogoča"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Končano"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Shrani"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Poveži"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Nastavi"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Nastavitve"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-sq/strings.xml b/nearby/halfsheet/res/values-sq/strings.xml
new file mode 100644
index 0000000..9265d1f
--- /dev/null
+++ b/nearby/halfsheet/res/values-sq/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Po nis konfigurimin…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Konfiguro pajisjen"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Pajisja u lidh"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Nuk mund të lidhej"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"U krye"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Ruaj"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Lidh"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Konfiguro"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Cilësimet"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-sr/strings.xml b/nearby/halfsheet/res/values-sr/strings.xml
new file mode 100644
index 0000000..094be03
--- /dev/null
+++ b/nearby/halfsheet/res/values-sr/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Подешавање се покреће…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Подесите уређај"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Уређај је повезан"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Повезивање није успело"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Готово"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Сачувај"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Повежи"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Подеси"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Подешавања"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-sv/strings.xml b/nearby/halfsheet/res/values-sv/strings.xml
new file mode 100644
index 0000000..297b7bc
--- /dev/null
+++ b/nearby/halfsheet/res/values-sv/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Konfigureringen startas …"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Konfigurera enheten"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Enheten är ansluten"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Det gick inte att ansluta"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Klar"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Spara"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Anslut"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Konfigurera"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Inställningar"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-sw/strings.xml b/nearby/halfsheet/res/values-sw/strings.xml
new file mode 100644
index 0000000..bf0bfeb
--- /dev/null
+++ b/nearby/halfsheet/res/values-sw/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Inaanza Kuweka Mipangilio…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Weka mipangilio ya kifaa"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Kifaa kimeunganishwa"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Imeshindwa kuunganisha"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Imemaliza"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Hifadhi"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Unganisha"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Weka mipangilio"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Mipangilio"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-ta/strings.xml b/nearby/halfsheet/res/values-ta/strings.xml
new file mode 100644
index 0000000..dfd67a6
--- /dev/null
+++ b/nearby/halfsheet/res/values-ta/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"அமைவைத் தொடங்குகிறது…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"சாதனத்தை அமையுங்கள்"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"சாதனம் இணைக்கப்பட்டது"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"இணைக்க முடியவில்லை"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"முடிந்தது"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"சேமி"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"இணை"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"அமை"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"அமைப்புகள்"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-te/strings.xml b/nearby/halfsheet/res/values-te/strings.xml
new file mode 100644
index 0000000..87be145
--- /dev/null
+++ b/nearby/halfsheet/res/values-te/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"సెటప్ ప్రారంభమవుతోంది…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"పరికరాన్ని సెటప్ చేయండి"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"పరికరం కనెక్ట్ చేయబడింది"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"కనెక్ట్ చేయడం సాధ్యపడలేదు"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"పూర్తయింది"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"సేవ్ చేయండి"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"కనెక్ట్ చేయండి"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"సెటప్ చేయండి"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"సెట్టింగ్లు"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-th/strings.xml b/nearby/halfsheet/res/values-th/strings.xml
new file mode 100644
index 0000000..bc4296b
--- /dev/null
+++ b/nearby/halfsheet/res/values-th/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"กำลังเริ่มการตั้งค่า…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"ตั้งค่าอุปกรณ์"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"เชื่อมต่ออุปกรณ์แล้ว"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"เชื่อมต่อไม่ได้"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"เสร็จสิ้น"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"บันทึก"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"เชื่อมต่อ"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"ตั้งค่า"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"การตั้งค่า"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-tl/strings.xml b/nearby/halfsheet/res/values-tl/strings.xml
new file mode 100644
index 0000000..a6de0e8
--- /dev/null
+++ b/nearby/halfsheet/res/values-tl/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Sinisimulan ang Pag-set Up…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"I-set up ang device"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Naikonekta na ang device"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Hindi makakonekta"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Tapos na"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"I-save"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Kumonekta"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"I-set up"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Mga Setting"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-tr/strings.xml b/nearby/halfsheet/res/values-tr/strings.xml
new file mode 100644
index 0000000..cd5a6ea
--- /dev/null
+++ b/nearby/halfsheet/res/values-tr/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Kurulum Başlatılıyor…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Cihazı kur"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Cihaz bağlandı"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Bağlanamadı"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Bitti"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Kaydet"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Bağlan"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Kur"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Ayarlar"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-uk/strings.xml b/nearby/halfsheet/res/values-uk/strings.xml
new file mode 100644
index 0000000..242ca07
--- /dev/null
+++ b/nearby/halfsheet/res/values-uk/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Запуск налаштування…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Налаштуйте пристрій"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Пристрій підключено"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Не вдалося підключити"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Готово"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Зберегти"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Підключити"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Налаштувати"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Налаштування"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-ur/strings.xml b/nearby/halfsheet/res/values-ur/strings.xml
new file mode 100644
index 0000000..4a4a59c
--- /dev/null
+++ b/nearby/halfsheet/res/values-ur/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"سیٹ اپ شروع ہو رہا ہے…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"آلہ سیٹ اپ کریں"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"آلہ منسلک ہے"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"منسلک نہیں ہو سکا"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"ہو گیا"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"محفوظ کریں"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"منسلک کریں"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"سیٹ اپ کریں"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"ترتیبات"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-uz/strings.xml b/nearby/halfsheet/res/values-uz/strings.xml
new file mode 100644
index 0000000..420512d
--- /dev/null
+++ b/nearby/halfsheet/res/values-uz/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Sozlash boshlandi…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Qurilmani sozlash"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Qurilma ulandi"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Ulanmadi"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Tayyor"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Saqlash"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Ulanish"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Sozlash"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Sozlamalar"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-vi/strings.xml b/nearby/halfsheet/res/values-vi/strings.xml
new file mode 100644
index 0000000..9c1e052
--- /dev/null
+++ b/nearby/halfsheet/res/values-vi/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Đang bắt đầu thiết lập…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Thiết lập thiết bị"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Đã kết nối thiết bị"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Không kết nối được"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Xong"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Lưu"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Kết nối"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Thiết lập"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Cài đặt"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-zh-rCN/strings.xml b/nearby/halfsheet/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..482b5c4
--- /dev/null
+++ b/nearby/halfsheet/res/values-zh-rCN/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"正在启动设置…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"设置设备"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"设备已连接"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"无法连接"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"完成"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"保存"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"连接"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"设置"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"设置"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-zh-rHK/strings.xml b/nearby/halfsheet/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..3ca73e6
--- /dev/null
+++ b/nearby/halfsheet/res/values-zh-rHK/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"開始設定…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"設定裝置"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"已連接裝置"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"無法連接"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"完成"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"儲存"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"連接"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"設定"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"設定"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-zh-rTW/strings.xml b/nearby/halfsheet/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..b4e680d
--- /dev/null
+++ b/nearby/halfsheet/res/values-zh-rTW/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"正在啟動設定程序…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"設定裝置"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"裝置已連線"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"無法連線"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"完成"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"儲存"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"連線"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"設定"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"設定"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values-zu/strings.xml b/nearby/halfsheet/res/values-zu/strings.xml
new file mode 100644
index 0000000..33fb405
--- /dev/null
+++ b/nearby/halfsheet/res/values-zu/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iqalisa Ukusetha…"</string>
+ <string name="fast_pair_title_setup" msgid="2894360355540593246">"Setha idivayisi"</string>
+ <string name="fast_pair_device_ready" msgid="2903490346082833101">"Idivayisi ixhunyiwe"</string>
+ <string name="fast_pair_title_fail" msgid="5677174346601290232">"Ayikwazanga ukuxhuma"</string>
+ <string name="paring_action_done" msgid="6888875159174470731">"Kwenziwe"</string>
+ <string name="paring_action_save" msgid="6259357442067880136">"Londoloza"</string>
+ <string name="paring_action_connect" msgid="4801102939608129181">"Xhuma"</string>
+ <string name="paring_action_launch" msgid="8940808384126591230">"Setha"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Amasethingi"</string>
+</resources>
diff --git a/nearby/halfsheet/res/values/colors.xml b/nearby/halfsheet/res/values/colors.xml
new file mode 100644
index 0000000..b066665
--- /dev/null
+++ b/nearby/halfsheet/res/values/colors.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+ <!-- Use original background color -->
+ <color name="fast_pair_notification_background">#00000000</color>
+ <!-- Ignores NewApi as below system colors are available since API 31, and HalfSheet is always
+ running on T+ even though it has min_sdk 30 to match its containing APEX -->
+ <color name="fast_pair_half_sheet_button_color" tools:ignore="NewApi">@android:color/system_accent1_100</color>
+ <color name="fast_pair_half_sheet_button_text" tools:ignore="NewApi">@android:color/system_neutral1_900</color>
+ <color name="fast_pair_half_sheet_button_accent_text" tools:ignore="NewApi">@android:color/system_neutral1_900</color>
+ <color name="fast_pair_progress_color" tools:ignore="NewApi">@android:color/system_accent1_600</color>
+ <color name="fast_pair_half_sheet_subtitle_color" tools:ignore="NewApi">@android:color/system_neutral2_700</color>
+ <color name="fast_pair_half_sheet_text_color" tools:ignore="NewApi">@android:color/system_neutral1_900</color>
+
+ <!-- Nearby Discoverer -->
+ <color name="discovery_activity_accent">#4285F4</color>
+
+ <!-- Fast Pair -->
+ <color name="fast_pair_primary_text">#DE000000</color>
+ <color name="fast_pair_notification_image_outline">#24000000</color>
+ <color name="fast_pair_battery_level_low">#D93025</color>
+ <color name="fast_pair_battery_level_normal">#80868B</color>
+ <color name="fast_pair_half_sheet_background">#FFFFFF</color>
+ <color name="fast_pair_half_sheet_color_accent">#1A73E8</color>
+ <color name="fast_pair_fail_progress_color">#F44336</color>
+ <color name="fast_pair_progress_back_ground">#24000000</color>
+</resources>
diff --git a/nearby/halfsheet/res/values/dimens.xml b/nearby/halfsheet/res/values/dimens.xml
new file mode 100644
index 0000000..f843042
--- /dev/null
+++ b/nearby/halfsheet/res/values/dimens.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Fast Pair notification values -->
+ <dimen name="fast_pair_halfsheet_mid_image_size">160dp</dimen>
+ <dimen name="fast_pair_notification_text_size">14sp</dimen>
+ <dimen name="fast_pair_notification_text_size_small">11sp</dimen>
+ <dimen name="fast_pair_battery_notification_empty_view_height">4dp</dimen>
+ <dimen name="fast_pair_battery_notification_margin_top">8dp</dimen>
+ <dimen name="fast_pair_battery_notification_margin_bottom">8dp</dimen>
+ <dimen name="fast_pair_battery_notification_content_height">40dp</dimen>
+ <dimen name="fast_pair_battery_notification_content_height_v2">64dp</dimen>
+ <dimen name="fast_pair_battery_notification_image_size">32dp</dimen>
+ <dimen name="fast_pair_battery_notification_image_padding">3dp</dimen>
+ <dimen name="fast_pair_half_sheet_min_height">350dp</dimen>
+ <dimen name="fast_pair_half_sheet_image_size">215dp</dimen>
+ <dimen name="fast_pair_half_sheet_land_image_size">136dp</dimen>
+ <dimen name="fast_pair_connect_button_height">36dp</dimen>
+ <dimen name="accessibility_required_min_touch_target_size">48dp</dimen>
+ <dimen name="fast_pair_half_sheet_battery_case_image_size">152dp</dimen>
+ <dimen name="fast_pair_half_sheet_battery_bud_image_size">100dp</dimen>
+ <integer name="half_sheet_battery_case_width_dp">156</integer>
+ <integer name="half_sheet_battery_case_height_dp">182</integer>
+
+ <!-- Maximum height for SliceView, override on slices/view/src/main/res/values/dimens.xml -->
+ <dimen name="abc_slice_large_height">360dp</dimen>
+
+ <dimen name="action_dialog_content_margin_left">16dp</dimen>
+ <dimen name="action_dialog_content_margin_top">70dp</dimen>
+ <dimen name="action_button_focused_elevation">4dp</dimen>
+ <!-- Subsequent Notification -->
+ <dimen name="fast_pair_notification_padding">4dp</dimen>
+ <dimen name="fast_pair_notification_large_image_size">32dp</dimen>
+ <dimen name="fast_pair_notification_small_image_size">32dp</dimen>
+ <!-- Battery Notification -->
+ <dimen name="fast_pair_battery_notification_main_view_padding">0dp</dimen>
+ <dimen name="fast_pair_battery_notification_title_image_margin_start">0dp</dimen>
+ <dimen name="fast_pair_battery_notification_title_text_margin_start">0dp</dimen>
+ <dimen name="fast_pair_battery_notification_title_text_margin_start_v2">0dp</dimen>
+ <dimen name="fast_pair_battery_notification_image_margin_start">0dp</dimen>
+
+ <dimen name="fast_pair_half_sheet_bottom_button_height">48dp</dimen>
+</resources>
diff --git a/nearby/halfsheet/res/values/ints.xml b/nearby/halfsheet/res/values/ints.xml
new file mode 100644
index 0000000..07bf9d2
--- /dev/null
+++ b/nearby/halfsheet/res/values/ints.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <integer name="half_sheet_slide_in_duration">250</integer>
+ <integer name="half_sheet_fade_out_duration">250</integer>
+</resources>
diff --git a/nearby/halfsheet/res/values/overlayable.xml b/nearby/halfsheet/res/values/overlayable.xml
new file mode 100644
index 0000000..fffa2e3
--- /dev/null
+++ b/nearby/halfsheet/res/values/overlayable.xml
@@ -0,0 +1,24 @@
+<?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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <overlayable name="NearbyHalfSheetResourcesConfig">
+ <policy type="product|system|vendor">
+ <item type="color" name="fast_pair_half_sheet_background"/>
+ <item type="color" name="fast_pair_half_sheet_button_color"/>
+ </policy>
+ </overlayable>
+</resources>
\ No newline at end of file
diff --git a/nearby/halfsheet/res/values/strings.xml b/nearby/halfsheet/res/values/strings.xml
new file mode 100644
index 0000000..01a82e4
--- /dev/null
+++ b/nearby/halfsheet/res/values/strings.xml
@@ -0,0 +1,72 @@
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+
+ <!--
+ ============================================================
+ PAIRING FRAGMENT
+ ============================================================
+ -->
+
+ <!--
+ A button shown to remind user setup is in progress. [CHAR LIMIT=30]
+ -->
+ <string name="fast_pair_setup_in_progress">Starting Setup…</string>
+ <!--
+ Title text shown to remind user to setup a device through companion app. [CHAR LIMIT=40]
+ -->
+ <string name="fast_pair_title_setup">Set up device</string>
+ <!--
+ Title after we successfully pair with the audio device
+ [CHAR LIMIT=30]
+ -->
+ <string name="fast_pair_device_ready">Device connected</string>
+ <!-- Title text shown when peripheral device fail to connect to phone. [CHAR_LIMIT=30] -->
+ <string name="fast_pair_title_fail">Couldn\'t connect</string>
+
+ <!--
+ ============================================================
+ MISCELLANEOUS
+ ============================================================
+ -->
+
+ <!--
+ A button shown after paring process to dismiss the current activity.
+ [CHAR LIMIT=30]
+ -->
+ <string name="paring_action_done">Done</string>
+ <!--
+ A button shown for retroactive paring.
+ [CHAR LIMIT=30]
+ -->
+ <string name="paring_action_save">Save</string>
+ <!--
+ A button to start connecting process.
+ [CHAR LIMIT=30]
+ -->
+ <string name="paring_action_connect">Connect</string>
+ <!--
+ A button to launch a companion app.
+ [CHAR LIMIT=30]
+ -->
+ <string name="paring_action_launch">Set up</string>
+ <!--
+ A button to launch a bluetooth Settings page.
+ [CHAR LIMIT=20]
+ -->
+ <string name="paring_action_settings">Settings</string>
+</resources>
\ No newline at end of file
diff --git a/nearby/halfsheet/res/values/styles.xml b/nearby/halfsheet/res/values/styles.xml
new file mode 100644
index 0000000..917bb63
--- /dev/null
+++ b/nearby/halfsheet/res/values/styles.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <style name="HalfSheetStyle" parent="Theme.Material3.DayNight.NoActionBar">
+ <item name="android:windowFrame">@null</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowEnterAnimation">@anim/fast_pair_half_sheet_slide_in</item>
+ <item name="android:windowExitAnimation">@anim/fast_pair_half_sheet_slide_out</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:backgroundDimEnabled">true</item>
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:fitsSystemWindows">true</item>
+ <item name="android:windowTranslucentNavigation">true</item>
+ </style>
+
+ <style name="HalfSheetButton" parent="@style/Widget.Material3.Button.TonalButton">
+ <item name="android:textColor">@color/fast_pair_half_sheet_button_accent_text</item>
+ <item name="android:backgroundTint">@color/fast_pair_half_sheet_button_color</item>
+ <item name="android:textSize">@dimen/fast_pair_notification_text_size</item>
+ <item name="android:fontFamily">google-sans-medium</item>
+ <item name="android:textAlignment">center</item>
+ <item name="android:textAllCaps">false</item>
+ </style>
+
+ <style name="HalfSheetButtonBorderless" parent="@style/Widget.Material3.Button.OutlinedButton">
+ <item name="android:textColor">@color/fast_pair_half_sheet_button_text</item>
+ <item name="android:strokeColor">@color/fast_pair_half_sheet_button_color</item>
+ <item name="android:textAllCaps">false</item>
+ <item name="android:textSize">@dimen/fast_pair_notification_text_size</item>
+ <item name="android:fontFamily">google-sans-medium</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textAlignment">center</item>
+ <item name="android:minHeight">@dimen/accessibility_required_min_touch_target_size</item>
+ </style>
+
+</resources>
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/FastPairUiServiceClient.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/FastPairUiServiceClient.java
new file mode 100644
index 0000000..bec0c0a
--- /dev/null
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/FastPairUiServiceClient.java
@@ -0,0 +1,117 @@
+/*
+ * 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.halfsheet;
+
+import android.content.Context;
+import android.nearby.FastPairDevice;
+import android.nearby.FastPairStatusCallback;
+import android.nearby.PairStatusMetadata;
+import android.nearby.aidl.IFastPairStatusCallback;
+import android.nearby.aidl.IFastPairUiService;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.BinderThread;
+import androidx.annotation.UiThread;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * A utility class for connecting to the {@link IFastPairUiService} and receive callbacks.
+ *
+ * @hide
+ */
+@UiThread
+public class FastPairUiServiceClient {
+
+ private static final String TAG = "FastPairHalfSheet";
+
+ private final IBinder mBinder;
+ private final WeakReference<Context> mWeakContext;
+ IFastPairUiService mFastPairUiService;
+ PairStatusCallbackIBinder mPairStatusCallbackIBinder;
+
+ /**
+ * The Ibinder instance should be from
+ * {@link com.android.server.nearby.fastpair.halfsheet.FastPairUiServiceImpl} so that the client can
+ * talk with the service.
+ */
+ public FastPairUiServiceClient(Context context, IBinder binder) {
+ mBinder = binder;
+ mFastPairUiService = IFastPairUiService.Stub.asInterface(mBinder);
+ mWeakContext = new WeakReference<>(context);
+ }
+
+ /**
+ * Registers a callback at service to get UI updates.
+ */
+ public void registerHalfSheetStateCallBack(FastPairStatusCallback fastPairStatusCallback) {
+ if (mPairStatusCallbackIBinder != null) {
+ return;
+ }
+ mPairStatusCallbackIBinder = new PairStatusCallbackIBinder(fastPairStatusCallback);
+ try {
+ mFastPairUiService.registerCallback(mPairStatusCallbackIBinder);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to register fastPairStatusCallback", e);
+ }
+ }
+
+ /**
+ * Pairs the device at service.
+ */
+ public void connect(FastPairDevice fastPairDevice) {
+ try {
+ mFastPairUiService.connect(fastPairDevice);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to connect Fast Pair device" + fastPairDevice, e);
+ }
+ }
+
+ /**
+ * Cancels Fast Pair connection and dismisses half sheet.
+ */
+ public void cancel(FastPairDevice fastPairDevice) {
+ try {
+ mFastPairUiService.cancel(fastPairDevice);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to connect Fast Pair device" + fastPairDevice, e);
+ }
+ }
+
+ private class PairStatusCallbackIBinder extends IFastPairStatusCallback.Stub {
+ private final FastPairStatusCallback mStatusCallback;
+
+ private PairStatusCallbackIBinder(FastPairStatusCallback fastPairStatusCallback) {
+ mStatusCallback = fastPairStatusCallback;
+ }
+
+ @BinderThread
+ @Override
+ public synchronized void onPairUpdate(FastPairDevice fastPairDevice,
+ PairStatusMetadata pairStatusMetadata) {
+ Context context = mWeakContext.get();
+ if (context != null) {
+ Handler handler = new Handler(context.getMainLooper());
+ handler.post(() ->
+ mStatusCallback.onPairUpdate(fastPairDevice, pairStatusMetadata));
+ }
+ }
+ }
+}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java
new file mode 100644
index 0000000..2a38b8a
--- /dev/null
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.nearby.halfsheet;
+
+import static com.android.nearby.halfsheet.fragment.DevicePairingFragment.APP_LAUNCH_FRAGMENT_TYPE;
+import static com.android.server.nearby.common.bluetooth.fastpair.FastPairConstants.EXTRA_MODEL_ID;
+import static com.android.server.nearby.common.fastpair.service.UserActionHandlerBase.EXTRA_MAC_ADDRESS;
+import static com.android.server.nearby.fastpair.Constant.ACTION_FAST_PAIR_HALF_SHEET_CANCEL;
+import static com.android.server.nearby.fastpair.Constant.DEVICE_PAIRING_FRAGMENT_TYPE;
+import static com.android.server.nearby.fastpair.Constant.EXTRA_HALF_SHEET_INFO;
+import static com.android.server.nearby.fastpair.Constant.EXTRA_HALF_SHEET_TYPE;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.nearby.halfsheet.fragment.DevicePairingFragment;
+import com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment;
+import com.android.nearby.halfsheet.utils.BroadcastUtils;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.util.Locale;
+
+import service.proto.Cache;
+
+/**
+ * A class show Fast Pair related information in Half sheet format.
+ */
+public class HalfSheetActivity extends FragmentActivity {
+
+ public static final String TAG = "FastPairHalfSheet";
+
+ public static final String EXTRA_HALF_SHEET_CONTENT =
+ "com.android.nearby.halfsheet.HALF_SHEET_CONTENT";
+ public static final String EXTRA_TITLE =
+ "com.android.nearby.halfsheet.HALF_SHEET_TITLE";
+ public static final String EXTRA_DESCRIPTION =
+ "com.android.nearby.halfsheet.HALF_SHEET_DESCRIPTION";
+ public static final String EXTRA_HALF_SHEET_ID =
+ "com.android.nearby.halfsheet.HALF_SHEET_ID";
+ public static final String EXTRA_HALF_SHEET_IS_RETROACTIVE =
+ "com.android.nearby.halfsheet.HALF_SHEET_IS_RETROACTIVE";
+ public static final String EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR =
+ "com.android.nearby.halfsheet.HALF_SHEET_IS_SUBSEQUENT_PAIR";
+ public static final String EXTRA_HALF_SHEET_PAIRING_RESURFACE =
+ "com.android.nearby.halfsheet.EXTRA_HALF_SHEET_PAIRING_RESURFACE";
+ public static final String ACTION_HALF_SHEET_FOREGROUND_STATE =
+ "com.android.nearby.halfsheet.ACTION_HALF_SHEET_FOREGROUND_STATE";
+ // Intent extra contains the user gmail name eg. testaccount@gmail.com.
+ public static final String EXTRA_HALF_SHEET_ACCOUNT_NAME =
+ "com.android.nearby.halfsheet.HALF_SHEET_ACCOUNT_NAME";
+ public static final String EXTRA_HALF_SHEET_FOREGROUND =
+ "com.android.nearby.halfsheet.EXTRA_HALF_SHEET_FOREGROUND";
+ public static final String ARG_FRAGMENT_STATE = "ARG_FRAGMENT_STATE";
+ @Nullable
+ private HalfSheetModuleFragment mHalfSheetModuleFragment;
+ @Nullable
+ private Cache.ScanFastPairStoreItem mScanFastPairStoreItem;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ byte[] infoArray = getIntent().getByteArrayExtra(EXTRA_HALF_SHEET_INFO);
+ String fragmentType = getIntent().getStringExtra(EXTRA_HALF_SHEET_TYPE);
+ if (infoArray == null || fragmentType == null) {
+ Log.d(
+ "HalfSheetActivity",
+ "exit flag off or do not have enough half sheet information.");
+ finish();
+ return;
+ }
+
+ switch (fragmentType) {
+ case DEVICE_PAIRING_FRAGMENT_TYPE:
+ mHalfSheetModuleFragment = DevicePairingFragment.newInstance(getIntent(),
+ savedInstanceState);
+ if (mHalfSheetModuleFragment == null) {
+ Log.d(TAG, "device pairing fragment has error.");
+ finish();
+ return;
+ }
+ break;
+ case APP_LAUNCH_FRAGMENT_TYPE:
+ // currentFragment = AppLaunchFragment.newInstance(getIntent());
+ if (mHalfSheetModuleFragment == null) {
+ Log.v(TAG, "app launch fragment has error.");
+ finish();
+ return;
+ }
+ break;
+ default:
+ Log.w(TAG, "there is no valid type for half sheet");
+ finish();
+ return;
+ }
+ if (mHalfSheetModuleFragment != null) {
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.fragment_container, mHalfSheetModuleFragment)
+ .commit();
+ }
+ setContentView(R.layout.fast_pair_half_sheet);
+
+ // If the user taps on the background, then close the activity.
+ // Unless they tap on the card itself, then ignore the tap.
+ findViewById(R.id.background).setOnClickListener(v -> onCancelClicked());
+ findViewById(R.id.card)
+ .setOnClickListener(
+ v -> Log.v(TAG, "card view is clicked noop"));
+ try {
+ mScanFastPairStoreItem =
+ Cache.ScanFastPairStoreItem.parseFrom(infoArray);
+ } catch (InvalidProtocolBufferException e) {
+ Log.w(
+ TAG, "error happens when pass info to half sheet");
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ }
+
+ @Override
+ protected void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
+ super.onSaveInstanceState(savedInstanceState);
+ if (mHalfSheetModuleFragment != null) {
+ mHalfSheetModuleFragment.onSaveInstanceState(savedInstanceState);
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ sendHalfSheetCancelBroadcast();
+ }
+
+ @Override
+ protected void onUserLeaveHint() {
+ super.onUserLeaveHint();
+ sendHalfSheetCancelBroadcast();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ String fragmentType = getIntent().getStringExtra(EXTRA_HALF_SHEET_TYPE);
+ if (fragmentType == null) {
+ return;
+ }
+ if (fragmentType.equals(DEVICE_PAIRING_FRAGMENT_TYPE)
+ && intent.getExtras() != null
+ && intent.getByteArrayExtra(EXTRA_HALF_SHEET_INFO) != null) {
+ try {
+ Cache.ScanFastPairStoreItem testScanFastPairStoreItem =
+ Cache.ScanFastPairStoreItem.parseFrom(
+ intent.getByteArrayExtra(EXTRA_HALF_SHEET_INFO));
+ if (mScanFastPairStoreItem != null
+ && !testScanFastPairStoreItem.getAddress().equals(
+ mScanFastPairStoreItem.getAddress())
+ && testScanFastPairStoreItem.getModelId().equals(
+ mScanFastPairStoreItem.getModelId())) {
+ Log.d(TAG, "possible factory reset happens");
+ halfSheetStateChange();
+ }
+ } catch (InvalidProtocolBufferException | NullPointerException e) {
+ Log.w(TAG, "error happens when pass info to half sheet");
+ }
+ }
+ }
+
+ /** This function should be called when user click empty area and cancel button. */
+ public void onCancelClicked() {
+ Log.d(TAG, "Cancels the half sheet and paring.");
+ sendHalfSheetCancelBroadcast();
+ finish();
+ }
+
+ /** Changes the half sheet foreground state to false. */
+ public void halfSheetStateChange() {
+ BroadcastUtils.sendBroadcast(
+ this,
+ new Intent(ACTION_HALF_SHEET_FOREGROUND_STATE)
+ .putExtra(EXTRA_HALF_SHEET_FOREGROUND, false));
+ finish();
+ }
+
+ private void sendHalfSheetCancelBroadcast() {
+ BroadcastUtils.sendBroadcast(
+ this,
+ new Intent(ACTION_HALF_SHEET_FOREGROUND_STATE)
+ .putExtra(EXTRA_HALF_SHEET_FOREGROUND, false));
+ if (mScanFastPairStoreItem != null) {
+ BroadcastUtils.sendBroadcast(
+ this,
+ new Intent(ACTION_FAST_PAIR_HALF_SHEET_CANCEL)
+ .putExtra(EXTRA_MODEL_ID,
+ mScanFastPairStoreItem.getModelId().toLowerCase(Locale.ROOT))
+ .putExtra(EXTRA_HALF_SHEET_TYPE,
+ getIntent().getStringExtra(EXTRA_HALF_SHEET_TYPE))
+ .putExtra(
+ EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR,
+ getIntent().getBooleanExtra(EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR,
+ false))
+ .putExtra(
+ EXTRA_HALF_SHEET_IS_RETROACTIVE,
+ getIntent().getBooleanExtra(EXTRA_HALF_SHEET_IS_RETROACTIVE,
+ false))
+ .putExtra(EXTRA_MAC_ADDRESS, mScanFastPairStoreItem.getAddress()));
+ }
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ super.setTitle(title);
+ TextView toolbarTitle = findViewById(R.id.toolbar_title);
+ toolbarTitle.setText(title);
+ }
+}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/DevicePairingFragment.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/DevicePairingFragment.java
new file mode 100644
index 0000000..320965b
--- /dev/null
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/DevicePairingFragment.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.nearby.halfsheet.fragment;
+
+import static android.text.TextUtils.isEmpty;
+
+import static com.android.nearby.halfsheet.HalfSheetActivity.ARG_FRAGMENT_STATE;
+import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_DESCRIPTION;
+import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_HALF_SHEET_ACCOUNT_NAME;
+import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_HALF_SHEET_CONTENT;
+import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_HALF_SHEET_ID;
+import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_TITLE;
+import static com.android.nearby.halfsheet.HalfSheetActivity.TAG;
+import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.FAILED;
+import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.FOUND_DEVICE;
+import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.NOT_STARTED;
+import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.PAIRED_LAUNCHABLE;
+import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.PAIRED_UNLAUNCHABLE;
+import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.PAIRING;
+import static com.android.server.nearby.fastpair.Constant.EXTRA_BINDER;
+import static com.android.server.nearby.fastpair.Constant.EXTRA_BUNDLE;
+import static com.android.server.nearby.fastpair.Constant.EXTRA_HALF_SHEET_INFO;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.nearby.FastPairDevice;
+import android.nearby.FastPairStatusCallback;
+import android.nearby.NearbyDevice;
+import android.nearby.PairStatusMetadata;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import com.android.nearby.halfsheet.FastPairUiServiceClient;
+import com.android.nearby.halfsheet.HalfSheetActivity;
+import com.android.nearby.halfsheet.R;
+import com.android.nearby.halfsheet.utils.FastPairUtils;
+import com.android.nearby.halfsheet.utils.IconUtils;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.util.Objects;
+
+import service.proto.Cache.ScanFastPairStoreItem;
+
+/**
+ * Modularize half sheet for fast pair this fragment will show when half sheet does device pairing.
+ *
+ * <p>This fragment will handle initial pairing subsequent pairing and retroactive pairing.
+ */
+@SuppressWarnings("nullness")
+public class DevicePairingFragment extends HalfSheetModuleFragment implements
+ FastPairStatusCallback {
+ private TextView mTitleView;
+ private TextView mSubTitleView;
+ private ImageView mImage;
+
+ private Button mConnectButton;
+ private Button mSetupButton;
+ private Button mCancelButton;
+ // Opens Bluetooth Settings.
+ private Button mSettingsButton;
+ private ImageView mInfoIconButton;
+ private ProgressBar mConnectProgressBar;
+
+ private Bundle mBundle;
+
+ private ScanFastPairStoreItem mScanFastPairStoreItem;
+ private FastPairUiServiceClient mFastPairUiServiceClient;
+
+ private @PairStatusMetadata.Status int mPairStatus = PairStatusMetadata.Status.UNKNOWN;
+ // True when there is a companion app to open.
+ private boolean mIsLaunchable;
+ private boolean mIsConnecting;
+ // Indicates that the setup button is clicked before.
+ private boolean mSetupButtonClicked = false;
+
+ // Holds the new text while we transition between the two.
+ private static final int TAG_PENDING_TEXT = R.id.toolbar_title;
+ public static final String APP_LAUNCH_FRAGMENT_TYPE = "APP_LAUNCH";
+
+ private static final String ARG_SETUP_BUTTON_CLICKED = "SETUP_BUTTON_CLICKED";
+ private static final String ARG_PAIRING_RESULT = "PAIRING_RESULT";
+
+ /**
+ * Create certain fragment according to the intent.
+ */
+ @Nullable
+ public static HalfSheetModuleFragment newInstance(
+ Intent intent, @Nullable Bundle saveInstanceStates) {
+ Bundle args = new Bundle();
+ byte[] infoArray = intent.getByteArrayExtra(EXTRA_HALF_SHEET_INFO);
+
+ Bundle bundle = intent.getBundleExtra(EXTRA_BUNDLE);
+ String title = intent.getStringExtra(EXTRA_TITLE);
+ String description = intent.getStringExtra(EXTRA_DESCRIPTION);
+ String accountName = intent.getStringExtra(EXTRA_HALF_SHEET_ACCOUNT_NAME);
+ String result = intent.getStringExtra(EXTRA_HALF_SHEET_CONTENT);
+ int halfSheetId = intent.getIntExtra(EXTRA_HALF_SHEET_ID, 0);
+
+ args.putByteArray(EXTRA_HALF_SHEET_INFO, infoArray);
+ args.putString(EXTRA_HALF_SHEET_ACCOUNT_NAME, accountName);
+ args.putString(EXTRA_TITLE, title);
+ args.putString(EXTRA_DESCRIPTION, description);
+ args.putInt(EXTRA_HALF_SHEET_ID, halfSheetId);
+ args.putString(EXTRA_HALF_SHEET_CONTENT, result == null ? "" : result);
+ args.putBundle(EXTRA_BUNDLE, bundle);
+ if (saveInstanceStates != null) {
+ if (saveInstanceStates.containsKey(ARG_FRAGMENT_STATE)) {
+ args.putSerializable(
+ ARG_FRAGMENT_STATE, saveInstanceStates.getSerializable(ARG_FRAGMENT_STATE));
+ }
+ if (saveInstanceStates.containsKey(BluetoothDevice.EXTRA_DEVICE)) {
+ args.putParcelable(
+ BluetoothDevice.EXTRA_DEVICE,
+ saveInstanceStates.getParcelable(BluetoothDevice.EXTRA_DEVICE));
+ }
+ if (saveInstanceStates.containsKey(BluetoothDevice.EXTRA_PAIRING_KEY)) {
+ args.putInt(
+ BluetoothDevice.EXTRA_PAIRING_KEY,
+ saveInstanceStates.getInt(BluetoothDevice.EXTRA_PAIRING_KEY));
+ }
+ if (saveInstanceStates.containsKey(ARG_SETUP_BUTTON_CLICKED)) {
+ args.putBoolean(
+ ARG_SETUP_BUTTON_CLICKED,
+ saveInstanceStates.getBoolean(ARG_SETUP_BUTTON_CLICKED));
+ }
+ if (saveInstanceStates.containsKey(ARG_PAIRING_RESULT)) {
+ args.putBoolean(ARG_PAIRING_RESULT,
+ saveInstanceStates.getBoolean(ARG_PAIRING_RESULT));
+ }
+ }
+ DevicePairingFragment fragment = new DevicePairingFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ /* attachToRoot= */
+ View rootView = inflater.inflate(
+ R.layout.fast_pair_device_pairing_fragment, container, /* attachToRoot= */
+ false);
+ if (getContext() == null) {
+ Log.d(TAG, "can't find the attached activity");
+ return rootView;
+ }
+
+ Bundle args = getArguments();
+ byte[] storeFastPairItemBytesArray = args.getByteArray(EXTRA_HALF_SHEET_INFO);
+ mBundle = args.getBundle(EXTRA_BUNDLE);
+ if (mBundle != null) {
+ mFastPairUiServiceClient =
+ new FastPairUiServiceClient(getContext(), mBundle.getBinder(EXTRA_BINDER));
+ mFastPairUiServiceClient.registerHalfSheetStateCallBack(this);
+ }
+ if (args.containsKey(ARG_FRAGMENT_STATE)) {
+ mFragmentState = (HalfSheetFragmentState) args.getSerializable(ARG_FRAGMENT_STATE);
+ }
+ if (args.containsKey(ARG_SETUP_BUTTON_CLICKED)) {
+ mSetupButtonClicked = args.getBoolean(ARG_SETUP_BUTTON_CLICKED);
+ }
+ if (args.containsKey(ARG_PAIRING_RESULT)) {
+ mPairStatus = args.getInt(ARG_PAIRING_RESULT);
+ }
+
+ // Initiate views.
+ mTitleView = Objects.requireNonNull(getActivity()).findViewById(R.id.toolbar_title);
+ mSubTitleView = rootView.findViewById(R.id.header_subtitle);
+ mImage = rootView.findViewById(R.id.pairing_pic);
+ mConnectProgressBar = rootView.findViewById(R.id.connect_progressbar);
+ mConnectButton = rootView.findViewById(R.id.connect_btn);
+ mCancelButton = rootView.findViewById(R.id.cancel_btn);
+ mSettingsButton = rootView.findViewById(R.id.settings_btn);
+ mSetupButton = rootView.findViewById(R.id.setup_btn);
+ mInfoIconButton = rootView.findViewById(R.id.info_icon);
+ mInfoIconButton.setImageResource(R.drawable.fast_pair_ic_info);
+
+ try {
+ setScanFastPairStoreItem(ScanFastPairStoreItem.parseFrom(storeFastPairItemBytesArray));
+ } catch (InvalidProtocolBufferException e) {
+ Log.w(TAG,
+ "DevicePairingFragment: error happens when pass info to half sheet");
+ return rootView;
+ }
+
+ // Config for landscape mode
+ DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
+ if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ rootView.getLayoutParams().height = displayMetrics.heightPixels * 4 / 5;
+ rootView.getLayoutParams().width = displayMetrics.heightPixels * 4 / 5;
+ mImage.getLayoutParams().height = displayMetrics.heightPixels / 2;
+ mImage.getLayoutParams().width = displayMetrics.heightPixels / 2;
+ mConnectProgressBar.getLayoutParams().width = displayMetrics.heightPixels / 2;
+ mConnectButton.getLayoutParams().width = displayMetrics.heightPixels / 2;
+ //TODO(b/213373051): Add cancel button
+ }
+
+ Bitmap icon = IconUtils.getIcon(mScanFastPairStoreItem.getIconPng().toByteArray(),
+ mScanFastPairStoreItem.getIconPng().size());
+ if (icon != null) {
+ mImage.setImageBitmap(icon);
+ }
+ mConnectButton.setOnClickListener(v -> onConnectClick());
+ mCancelButton.setOnClickListener(v ->
+ ((HalfSheetActivity) getActivity()).onCancelClicked());
+ mSettingsButton.setOnClickListener(v -> onSettingsClicked());
+ mSetupButton.setOnClickListener(v -> onSetupClick());
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Get access to the activity's menu
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Log.v(TAG, "onStart: invalidate states");
+ invalidateState();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle savedInstanceState) {
+ super.onSaveInstanceState(savedInstanceState);
+
+ savedInstanceState.putSerializable(ARG_FRAGMENT_STATE, mFragmentState);
+ savedInstanceState.putBoolean(ARG_SETUP_BUTTON_CLICKED, mSetupButtonClicked);
+ savedInstanceState.putInt(ARG_PAIRING_RESULT, mPairStatus);
+ }
+
+ private void onSettingsClicked() {
+ startActivity(new Intent(Settings.ACTION_BLUETOOTH_SETTINGS));
+ }
+
+ private void onSetupClick() {
+ String companionApp =
+ FastPairUtils.getCompanionAppFromActionUrl(mScanFastPairStoreItem.getActionUrl());
+ Intent intent =
+ FastPairUtils.createCompanionAppIntent(
+ Objects.requireNonNull(getContext()),
+ companionApp,
+ mScanFastPairStoreItem.getAddress());
+ mSetupButtonClicked = true;
+ if (mFragmentState == PAIRED_LAUNCHABLE) {
+ if (intent != null) {
+ startActivity(intent);
+ }
+ } else {
+ Log.d(TAG, "onSetupClick: State is " + mFragmentState);
+ }
+ }
+
+ private void onConnectClick() {
+ if (mScanFastPairStoreItem == null) {
+ Log.w(TAG, "No pairing related information in half sheet");
+ return;
+ }
+ if (getFragmentState() == PAIRING) {
+ return;
+ }
+ mIsConnecting = true;
+ invalidateState();
+ mFastPairUiServiceClient.connect(
+ new FastPairDevice.Builder()
+ .addMedium(NearbyDevice.Medium.BLE)
+ .setBluetoothAddress(mScanFastPairStoreItem.getAddress())
+ .setData(FastPairUtils.convertFrom(mScanFastPairStoreItem)
+ .toByteArray())
+ .build());
+ }
+
+ // Receives callback from service.
+ @Override
+ public void onPairUpdate(FastPairDevice fastPairDevice, PairStatusMetadata pairStatusMetadata) {
+ @PairStatusMetadata.Status int status = pairStatusMetadata.getStatus();
+ if (status == PairStatusMetadata.Status.DISMISS && getActivity() != null) {
+ getActivity().finish();
+ }
+ mIsConnecting = false;
+ mPairStatus = status;
+ invalidateState();
+ }
+
+ @Override
+ public void invalidateState() {
+ HalfSheetFragmentState newState = NOT_STARTED;
+ if (mIsConnecting) {
+ newState = PAIRING;
+ } else {
+ switch (mPairStatus) {
+ case PairStatusMetadata.Status.SUCCESS:
+ newState = mIsLaunchable ? PAIRED_LAUNCHABLE : PAIRED_UNLAUNCHABLE;
+ break;
+ case PairStatusMetadata.Status.FAIL:
+ newState = FAILED;
+ break;
+ default:
+ if (mScanFastPairStoreItem != null) {
+ newState = FOUND_DEVICE;
+ }
+ }
+ }
+ if (newState == mFragmentState) {
+ return;
+ }
+ setState(newState);
+ }
+
+ @Override
+ public void setState(HalfSheetFragmentState state) {
+ super.setState(state);
+ invalidateTitles();
+ invalidateButtons();
+ }
+
+ private void setScanFastPairStoreItem(ScanFastPairStoreItem item) {
+ mScanFastPairStoreItem = item;
+ invalidateLaunchable();
+ }
+
+ private void invalidateLaunchable() {
+ String companionApp =
+ FastPairUtils.getCompanionAppFromActionUrl(mScanFastPairStoreItem.getActionUrl());
+ if (isEmpty(companionApp)) {
+ mIsLaunchable = false;
+ return;
+ }
+ mIsLaunchable =
+ FastPairUtils.isLaunchable(Objects.requireNonNull(getContext()), companionApp);
+ }
+
+ private void invalidateButtons() {
+ mConnectProgressBar.setVisibility(View.INVISIBLE);
+ mConnectButton.setVisibility(View.INVISIBLE);
+ mCancelButton.setVisibility(View.INVISIBLE);
+ mSetupButton.setVisibility(View.INVISIBLE);
+ mSettingsButton.setVisibility(View.INVISIBLE);
+ mInfoIconButton.setVisibility(View.INVISIBLE);
+
+ switch (mFragmentState) {
+ case FOUND_DEVICE:
+ mInfoIconButton.setVisibility(View.VISIBLE);
+ mConnectButton.setVisibility(View.VISIBLE);
+ break;
+ case PAIRING:
+ mConnectProgressBar.setVisibility(View.VISIBLE);
+ mCancelButton.setVisibility(View.VISIBLE);
+ setBackgroundClickable(false);
+ break;
+ case PAIRED_LAUNCHABLE:
+ mCancelButton.setVisibility(View.VISIBLE);
+ mSetupButton.setVisibility(View.VISIBLE);
+ setBackgroundClickable(true);
+ break;
+ case FAILED:
+ mSettingsButton.setVisibility(View.VISIBLE);
+ setBackgroundClickable(true);
+ break;
+ case NOT_STARTED:
+ case PAIRED_UNLAUNCHABLE:
+ default:
+ mCancelButton.setVisibility(View.VISIBLE);
+ setBackgroundClickable(true);
+ }
+ }
+
+ private void setBackgroundClickable(boolean isClickable) {
+ HalfSheetActivity activity = (HalfSheetActivity) getActivity();
+ if (activity == null) {
+ Log.w(TAG, "setBackgroundClickable: failed to set clickable to " + isClickable
+ + " because cannot get HalfSheetActivity.");
+ return;
+ }
+ View background = activity.findViewById(R.id.background);
+ if (background == null) {
+ Log.w(TAG, "setBackgroundClickable: failed to set clickable to " + isClickable
+ + " cannot find background at HalfSheetActivity.");
+ return;
+ }
+ Log.d(TAG, "setBackgroundClickable to " + isClickable);
+ background.setClickable(isClickable);
+ }
+
+ private void invalidateTitles() {
+ String newTitle = getTitle();
+ invalidateTextView(mTitleView, newTitle);
+ String newSubTitle = getSubTitle();
+ invalidateTextView(mSubTitleView, newSubTitle);
+ }
+
+ private void invalidateTextView(TextView textView, String newText) {
+ CharSequence oldText =
+ textView.getTag(TAG_PENDING_TEXT) != null
+ ? (CharSequence) textView.getTag(TAG_PENDING_TEXT)
+ : textView.getText();
+ if (TextUtils.equals(oldText, newText)) {
+ return;
+ }
+ if (TextUtils.isEmpty(oldText)) {
+ // First time run. Don't animate since there's nothing to animate from.
+ textView.setText(newText);
+ } else {
+ textView.setTag(TAG_PENDING_TEXT, newText);
+ textView
+ .animate()
+ .alpha(0f)
+ .setDuration(TEXT_ANIMATION_DURATION_MILLISECONDS)
+ .withEndAction(
+ () -> {
+ textView.setText(newText);
+ textView
+ .animate()
+ .alpha(1f)
+ .setDuration(TEXT_ANIMATION_DURATION_MILLISECONDS);
+ });
+ }
+ }
+
+ private String getTitle() {
+ switch (mFragmentState) {
+ case PAIRED_LAUNCHABLE:
+ return getString(R.string.fast_pair_title_setup);
+ case FAILED:
+ return getString(R.string.fast_pair_title_fail);
+ case FOUND_DEVICE:
+ case NOT_STARTED:
+ case PAIRED_UNLAUNCHABLE:
+ default:
+ return mScanFastPairStoreItem.getDeviceName();
+ }
+ }
+
+ private String getSubTitle() {
+ switch (mFragmentState) {
+ case PAIRED_LAUNCHABLE:
+ return String.format(
+ mScanFastPairStoreItem
+ .getFastPairStrings()
+ .getPairingFinishedCompanionAppInstalled(),
+ mScanFastPairStoreItem.getDeviceName());
+ case FAILED:
+ return mScanFastPairStoreItem.getFastPairStrings().getPairingFailDescription();
+ case PAIRED_UNLAUNCHABLE:
+ getString(R.string.fast_pair_device_ready);
+ // fall through
+ case FOUND_DEVICE:
+ case NOT_STARTED:
+ return mScanFastPairStoreItem.getFastPairStrings().getInitialPairingDescription();
+ default:
+ return "";
+ }
+ }
+}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/HalfSheetModuleFragment.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/HalfSheetModuleFragment.java
new file mode 100644
index 0000000..f1db4d0
--- /dev/null
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/HalfSheetModuleFragment.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.nearby.halfsheet.fragment;
+
+import static com.android.nearby.halfsheet.HalfSheetActivity.TAG;
+import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.NOT_STARTED;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+
+/** Base class for all of the half sheet fragment. */
+public abstract class HalfSheetModuleFragment extends Fragment {
+
+ static final int TEXT_ANIMATION_DURATION_MILLISECONDS = 200;
+
+ HalfSheetFragmentState mFragmentState = NOT_STARTED;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ /** UI states of the half-sheet fragment. */
+ public enum HalfSheetFragmentState {
+ NOT_STARTED, // Initial status
+ FOUND_DEVICE, // When a device is found found from Nearby scan service
+ PAIRING, // When user taps 'Connect' and Fast Pair stars pairing process
+ PAIRED_LAUNCHABLE, // When pair successfully
+ // and we found a launchable companion app installed
+ PAIRED_UNLAUNCHABLE, // When pair successfully
+ // but we cannot find a companion app to launch it
+ FAILED, // When paring was failed
+ FINISHED // When the activity is about to end finished.
+ }
+
+ /**
+ * Returns the {@link HalfSheetFragmentState} to the parent activity.
+ *
+ * <p>Overrides this method if the fragment's state needs to be preserved in the parent
+ * activity.
+ */
+ public HalfSheetFragmentState getFragmentState() {
+ return mFragmentState;
+ }
+
+ void setState(HalfSheetFragmentState state) {
+ Log.v(TAG, "Settings state from " + mFragmentState + " to " + state);
+ mFragmentState = state;
+ }
+
+ /**
+ * Populate data to UI widgets according to the latest {@link HalfSheetFragmentState}.
+ */
+ abstract void invalidateState();
+}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/BroadcastUtils.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/BroadcastUtils.java
new file mode 100644
index 0000000..467997c
--- /dev/null
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/BroadcastUtils.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.nearby.halfsheet.utils;
+
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Broadcast util class
+ */
+public class BroadcastUtils {
+
+ /**
+ * Helps send broadcast.
+ */
+ public static void sendBroadcast(Context context, Intent intent) {
+ context.sendBroadcast(intent);
+ }
+
+ private BroadcastUtils() {
+ }
+}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/FastPairUtils.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/FastPairUtils.java
new file mode 100644
index 0000000..00a365c
--- /dev/null
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/FastPairUtils.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.nearby.halfsheet.utils;
+
+import static com.android.server.nearby.common.fastpair.service.UserActionHandlerBase.EXTRA_COMPANION_APP;
+import static com.android.server.nearby.fastpair.UserActionHandler.ACTION_FAST_PAIR;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.net.URISyntaxException;
+
+import service.proto.Cache;
+
+/**
+ * Util class in half sheet apk
+ */
+public class FastPairUtils {
+
+ /** FastPair util method check certain app is install on the device or not. */
+ public static boolean isAppInstalled(Context context, String packageName) {
+ try {
+ context.getPackageManager().getPackageInfo(packageName, 0);
+ return true;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ /** FastPair util method to properly format the action url extra. */
+ @Nullable
+ public static String getCompanionAppFromActionUrl(String actionUrl) {
+ try {
+ Intent intent = Intent.parseUri(actionUrl, Intent.URI_INTENT_SCHEME);
+ if (!intent.getAction().equals(ACTION_FAST_PAIR)) {
+ Log.e("FastPairUtils", "Companion app launch attempted from malformed action url");
+ return null;
+ }
+ return intent.getStringExtra(EXTRA_COMPANION_APP);
+ } catch (URISyntaxException e) {
+ Log.e("FastPairUtils", "FastPair: fail to get companion app info from discovery item");
+ return null;
+ }
+ }
+
+ /**
+ * Converts {@link service.proto.Cache.StoredDiscoveryItem} from
+ * {@link service.proto.Cache.ScanFastPairStoreItem}
+ */
+ public static Cache.StoredDiscoveryItem convertFrom(Cache.ScanFastPairStoreItem item) {
+ return convertFrom(item, /* isSubsequentPair= */ false);
+ }
+
+ /**
+ * Converts a {@link service.proto.Cache.ScanFastPairStoreItem}
+ * to a {@link service.proto.Cache.StoredDiscoveryItem}.
+ *
+ * <p>This is needed to make the new Fast Pair scanning stack compatible with the rest of the
+ * legacy Fast Pair code.
+ */
+ public static Cache.StoredDiscoveryItem convertFrom(
+ Cache.ScanFastPairStoreItem item, boolean isSubsequentPair) {
+ return Cache.StoredDiscoveryItem.newBuilder()
+ .setId(item.getModelId())
+ .setFirstObservationTimestampMillis(item.getFirstObservationTimestampMillis())
+ .setLastObservationTimestampMillis(item.getLastObservationTimestampMillis())
+ .setActionUrl(item.getActionUrl())
+ .setActionUrlType(Cache.ResolvedUrlType.APP)
+ .setTitle(
+ isSubsequentPair
+ ? item.getFastPairStrings().getTapToPairWithoutAccount()
+ : item.getDeviceName())
+ .setMacAddress(item.getAddress())
+ .setState(Cache.StoredDiscoveryItem.State.STATE_ENABLED)
+ .setTriggerId(item.getModelId())
+ .setIconPng(item.getIconPng())
+ .setIconFifeUrl(item.getIconFifeUrl())
+ .setDescription(
+ isSubsequentPair
+ ? item.getDeviceName()
+ : item.getFastPairStrings().getTapToPairWithoutAccount())
+ .setAuthenticationPublicKeySecp256R1(item.getAntiSpoofingPublicKey())
+ .setCompanionDetail(item.getCompanionDetail())
+ .setFastPairStrings(item.getFastPairStrings())
+ .setFastPairInformation(
+ Cache.FastPairInformation.newBuilder()
+ .setDataOnlyConnection(item.getDataOnlyConnection())
+ .setTrueWirelessImages(item.getTrueWirelessImages())
+ .setAssistantSupported(item.getAssistantSupported())
+ .setCompanyName(item.getCompanyName()))
+ .build();
+ }
+
+ /**
+ * Returns true the application is installed and can be opened on device.
+ */
+ public static boolean isLaunchable(@NonNull Context context, String companionApp) {
+ return isAppInstalled(context, companionApp)
+ && createCompanionAppIntent(context, companionApp, null) != null;
+ }
+
+ /**
+ * Returns an intent to launch given the package name and bluetooth address (if provided).
+ * Returns null if no such an intent can be found.
+ */
+ @Nullable
+ public static Intent createCompanionAppIntent(@NonNull Context context, String packageName,
+ @Nullable String address) {
+ Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName);
+ if (intent == null) {
+ return null;
+ }
+ if (address != null) {
+ BluetoothAdapter adapter = getBluetoothAdapter(context);
+ if (adapter != null) {
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, adapter.getRemoteDevice(address));
+ }
+ }
+ return intent;
+ }
+
+ @Nullable
+ private static BluetoothAdapter getBluetoothAdapter(@NonNull Context context) {
+ BluetoothManager bluetoothManager = context.getSystemService(BluetoothManager.class);
+ return bluetoothManager == null ? null : bluetoothManager.getAdapter();
+ }
+
+ private FastPairUtils() {}
+}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/IconUtils.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/IconUtils.java
new file mode 100644
index 0000000..218c756
--- /dev/null
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/IconUtils.java
@@ -0,0 +1,133 @@
+/*
+ * 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.halfsheet.utils;
+
+import static com.android.nearby.halfsheet.HalfSheetActivity.TAG;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.core.graphics.ColorUtils;
+
+/**
+ * Utility class for icon size verification.
+ */
+public class IconUtils {
+
+ private static final float NOTIFICATION_BACKGROUND_PADDING_PERCENT = 0.125f;
+ private static final float NOTIFICATION_BACKGROUND_ALPHA = 0.7f;
+ private static final int MIN_ICON_SIZE = 16;
+ private static final int DESIRED_ICON_SIZE = 32;
+
+ /**
+ * Verify that the icon is non null and falls in the small bucket. Just because an icon isn't
+ * small doesn't guarantee it is large or exists.
+ */
+ public static boolean isIconSizedSmall(@Nullable Bitmap bitmap) {
+ if (bitmap == null) {
+ return false;
+ }
+ return bitmap.getWidth() >= MIN_ICON_SIZE
+ && bitmap.getWidth() < DESIRED_ICON_SIZE
+ && bitmap.getHeight() >= MIN_ICON_SIZE
+ && bitmap.getHeight() < DESIRED_ICON_SIZE;
+ }
+
+ /**
+ * Verify that the icon is non null and falls in the regular / default size bucket. Doesn't
+ * guarantee if not regular then it is small.
+ */
+ static boolean isIconSizedRegular(@Nullable Bitmap bitmap) {
+ if (bitmap == null) {
+ return false;
+ }
+ return bitmap.getWidth() >= DESIRED_ICON_SIZE && bitmap.getHeight() >= DESIRED_ICON_SIZE;
+ }
+
+ /**
+ * All icons that are sized correctly (larger than the MIN_ICON_SIZE icon size)
+ * are resize on the server to the DESIRED_ICON_SIZE icon size so that
+ * they appear correct.
+ */
+ public static boolean isIconSizeCorrect(@Nullable Bitmap bitmap) {
+ if (bitmap == null) {
+ return false;
+ }
+ return isIconSizedSmall(bitmap) || isIconSizedRegular(bitmap);
+ }
+
+ /**
+ * Returns the bitmap from the byte array. Returns null if cannot decode or not in correct size.
+ */
+ @Nullable
+ public static Bitmap getIcon(byte[] imageData, int size) {
+ try {
+ Bitmap icon =
+ BitmapFactory.decodeByteArray(imageData, /* offset= */ 0, size);
+ if (IconUtils.isIconSizeCorrect(icon)) {
+ // Do not add background for Half Sheet.
+ return IconUtils.addWhiteCircleBackground(icon);
+ }
+ } catch (OutOfMemoryError e) {
+ Log.w(TAG, "getIcon: Failed to decode icon, returning null.", e);
+ }
+ return null;
+ }
+
+ /** Adds a circular, white background to the bitmap. */
+ @Nullable
+ public static Bitmap addWhiteCircleBackground(Bitmap bitmap) {
+ if (bitmap == null) {
+ Log.w(TAG, "addWhiteCircleBackground: Bitmap is null, not adding background.");
+ return null;
+ }
+
+ if (bitmap.getWidth() != bitmap.getHeight()) {
+ Log.w(TAG, "addWhiteCircleBackground: Bitmap dimensions not square. Skipping"
+ + "adding background.");
+ return bitmap;
+ }
+
+ int padding = (int) (bitmap.getWidth() * NOTIFICATION_BACKGROUND_PADDING_PERCENT);
+ Bitmap bitmapWithBackground =
+ Bitmap.createBitmap(
+ bitmap.getWidth() + (2 * padding),
+ bitmap.getHeight() + (2 * padding),
+ bitmap.getConfig());
+ Canvas canvas = new Canvas(bitmapWithBackground);
+ Paint paint = new Paint();
+ paint.setColor(
+ ColorUtils.setAlphaComponent(
+ Color.WHITE, (int) (255 * NOTIFICATION_BACKGROUND_ALPHA)));
+ paint.setStyle(Paint.Style.FILL);
+ paint.setAntiAlias(true);
+ canvas.drawCircle(
+ bitmapWithBackground.getWidth() / 2,
+ bitmapWithBackground.getHeight() / 2,
+ bitmapWithBackground.getWidth() / 2,
+ paint);
+ canvas.drawBitmap(bitmap, padding, padding, null);
+
+ return bitmapWithBackground;
+ }
+}
+
diff --git a/nearby/service-src/com/android/server/nearby/NearbyService.java b/nearby/service-src/com/android/server/nearby/NearbyService.java
deleted file mode 100644
index 88752cc..0000000
--- a/nearby/service-src/com/android/server/nearby/NearbyService.java
+++ /dev/null
@@ -1,36 +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.server.nearby;
-
-import android.content.Context;
-import android.os.Binder;
-
-/**
- * Stub NearbyService class, used until NearbyService code is available in all branches.
- *
- * This can be published as an empty service in branches that use it.
- */
-public final class NearbyService extends Binder {
- public NearbyService(Context ctx) {
- throw new UnsupportedOperationException("This is a stub service");
- }
-
- /** Called by the service initializer on each boot phase */
- public void onBootPhase(int phase) {
- // Do nothing
- }
-}
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
new file mode 100644
index 0000000..7112bb1
--- /dev/null
+++ b/nearby/service/Android.bp
@@ -0,0 +1,131 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+ name: "nearby-service-srcs",
+ srcs: [
+ "java/**/*.java",
+ ":statslog-nearby-java-gen",
+ ],
+}
+
+filegroup {
+ name: "nearby-service-string-res",
+ srcs: [
+ "java/**/Constant.java",
+ "java/**/UserActionHandlerBase.java",
+ "java/**/UserActionHandler.java",
+ "java/**/FastPairConstants.java",
+ ],
+}
+
+java_library {
+ name: "nearby-service-string",
+ srcs: [":nearby-service-string-res"],
+ libs: ["framework-bluetooth"],
+ sdk_version: "module_current",
+}
+
+// Common lib for nearby end-to-end testing.
+java_library {
+ name: "nearby-common-lib",
+ srcs: [
+ "java/com/android/server/nearby/common/bloomfilter/*.java",
+ "java/com/android/server/nearby/common/bluetooth/*.java",
+ "java/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryption.java",
+ "java/com/android/server/nearby/common/bluetooth/fastpair/AesEcbSingleBlockEncryption.java",
+ "java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAddress.java",
+ "java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothUuids.java",
+ "java/com/android/server/nearby/common/bluetooth/fastpair/Bytes.java",
+ "java/com/android/server/nearby/common/bluetooth/fastpair/Constants.java",
+ "java/com/android/server/nearby/common/bluetooth/fastpair/EllipticCurveDiffieHellmanExchange.java",
+ "java/com/android/server/nearby/common/bluetooth/fastpair/HmacSha256.java",
+ "java/com/android/server/nearby/common/bluetooth/fastpair/Ltv.java",
+ "java/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoder.java",
+ "java/com/android/server/nearby/common/bluetooth/fastpair/NamingEncoder.java",
+ "java/com/android/server/nearby/common/bluetooth/testability/**/*.java",
+ "java/com/android/server/nearby/common/bluetooth/gatt/*.java",
+ "java/com/android/server/nearby/common/bluetooth/util/*.java",
+ ],
+ libs: [
+ "androidx.annotation_annotation",
+ "androidx.core_core",
+ "error_prone_annotations",
+ "framework-bluetooth",
+ "guava",
+ ],
+ sdk_version: "module_current",
+ visibility: [
+ "//packages/modules/Connectivity/nearby/tests/multidevices/clients/test_support/fastpair_provider",
+ ],
+}
+
+// Main lib for nearby services.
+java_library {
+ name: "service-nearby-pre-jarjar",
+ srcs: [":nearby-service-srcs"],
+
+ defaults: [
+ "framework-system-server-module-defaults"
+ ],
+ libs: [
+ "framework-bluetooth.stubs.module_lib", // TODO(b/215722418): Change to framework-bluetooth once fixed
+ "error_prone_annotations",
+ "framework-connectivity-t.impl",
+ "framework-statsd.stubs.module_lib",
+ ],
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.core_core",
+ "androidx.localbroadcastmanager_localbroadcastmanager",
+ "guava",
+ "libprotobuf-java-lite",
+ "fast-pair-lite-protos",
+ "modules-utils-build",
+ "modules-utils-handlerexecutor",
+ "modules-utils-preconditions",
+ "modules-utils-backgroundthread",
+ "presence-lite-protos",
+ ],
+ sdk_version: "system_server_current",
+ // This is included in service-connectivity which is 30+
+ // TODO: allow APEXes to have service jars with higher min_sdk than the APEX
+ // (service-connectivity is only used on 31+) and use 31 here
+ min_sdk_version: "30",
+
+ installable: true,
+ dex_preopt: {
+ enabled: false,
+ app_image: false,
+ },
+ visibility: [
+ "//packages/modules/Nearby/apex",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ ],
+}
+
+genrule {
+ name: "statslog-nearby-java-gen",
+ tools: ["stats-log-api-gen"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module nearby " +
+ " --javaPackage com.android.server.nearby.proto --javaClass NearbyStatsLog" +
+ " --minApiLevel 33",
+ out: ["com/android/server/nearby/proto/NearbyStatsLog.java"],
+}
diff --git a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
new file mode 100644
index 0000000..8fdac87
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby;
+
+import android.provider.DeviceConfig;
+
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * A utility class for encapsulating Nearby feature flag configurations.
+ */
+public class NearbyConfiguration {
+
+ /**
+ * Flag use to enable presence legacy broadcast.
+ */
+ public static final String NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY =
+ "nearby_enable_presence_broadcast_legacy";
+
+ private boolean mEnablePresenceBroadcastLegacy;
+
+ public NearbyConfiguration() {
+ mEnablePresenceBroadcastLegacy = getDeviceConfigBoolean(
+ NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, false /* defaultValue */);
+
+ }
+
+ /**
+ * Returns whether broadcasting legacy presence spec is enabled.
+ */
+ public boolean isPresenceBroadcastLegacyEnabled() {
+ return mEnablePresenceBroadcastLegacy;
+ }
+
+ private boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
+ final String value = getDeviceConfigProperty(name);
+ return value != null ? Boolean.parseBoolean(value) : defaultValue;
+ }
+
+ @VisibleForTesting
+ protected String getDeviceConfigProperty(String name) {
+ return DeviceConfig.getProperty(DeviceConfig.NAMESPACE_TETHERING, name);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
new file mode 100644
index 0000000..e3e5b5d
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby;
+
+import static com.android.server.SystemService.PHASE_BOOT_COMPLETED;
+import static com.android.server.SystemService.PHASE_THIRD_PARTY_APPS_CAN_START;
+
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.location.ContextHubManager;
+import android.nearby.BroadcastRequestParcelable;
+import android.nearby.IBroadcastListener;
+import android.nearby.INearbyManager;
+import android.nearby.IScanListener;
+import android.nearby.NearbyManager;
+import android.nearby.ScanRequest;
+import android.util.Log;
+
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.FastPairManager;
+import com.android.server.nearby.injector.ContextHubManagerAdapter;
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.presence.PresenceManager;
+import com.android.server.nearby.provider.BroadcastProviderManager;
+import com.android.server.nearby.provider.DiscoveryProviderManager;
+import com.android.server.nearby.provider.FastPairDataProvider;
+
+/** Service implementing nearby functionality. */
+public class NearbyService extends INearbyManager.Stub {
+ public static final String TAG = "NearbyService";
+
+ private final Context mContext;
+ private final SystemInjector mSystemInjector;
+ private final FastPairManager mFastPairManager;
+ private final PresenceManager mPresenceManager;
+ private final BroadcastReceiver mBluetoothReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int state =
+ intent.getIntExtra(
+ BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
+ if (state == BluetoothAdapter.STATE_ON) {
+ if (mSystemInjector != null) {
+ // Have to do this logic in listener. Even during PHASE_BOOT_COMPLETED
+ // phase, BluetoothAdapter is not null, the BleScanner is null.
+ Log.v(TAG, "Initiating BluetoothAdapter when Bluetooth is turned on.");
+ mSystemInjector.initializeBluetoothAdapter();
+ }
+ }
+ }
+ };
+ private DiscoveryProviderManager mProviderManager;
+ private BroadcastProviderManager mBroadcastProviderManager;
+
+ public NearbyService(Context context) {
+ mContext = context;
+ mSystemInjector = new SystemInjector(context);
+ mProviderManager = new DiscoveryProviderManager(context, mSystemInjector);
+ mBroadcastProviderManager = new BroadcastProviderManager(context, mSystemInjector);
+ final LocatorContextWrapper lcw = new LocatorContextWrapper(context, null);
+ mFastPairManager = new FastPairManager(lcw);
+ mPresenceManager = new PresenceManager(lcw);
+ }
+
+ @Override
+ @NearbyManager.ScanStatus
+ public int registerScanListener(ScanRequest scanRequest, IScanListener listener) {
+ if (mProviderManager.registerScanListener(scanRequest, listener)) {
+ return NearbyManager.ScanStatus.SUCCESS;
+ }
+ return NearbyManager.ScanStatus.ERROR;
+ }
+
+ @Override
+ public void unregisterScanListener(IScanListener listener) {
+ mProviderManager.unregisterScanListener(listener);
+ }
+
+ @Override
+ public void startBroadcast(BroadcastRequestParcelable broadcastRequestParcelable,
+ IBroadcastListener listener) {
+ mBroadcastProviderManager.startBroadcast(
+ broadcastRequestParcelable.getBroadcastRequest(),
+ listener);
+ }
+
+ @Override
+ public void stopBroadcast(IBroadcastListener listener) {
+ mBroadcastProviderManager.stopBroadcast(listener);
+ }
+
+ /**
+ * Called by the service initializer.
+ *
+ * <p>{@see com.android.server.SystemService#onBootPhase}.
+ */
+ public void onBootPhase(int phase) {
+ switch (phase) {
+ case PHASE_THIRD_PARTY_APPS_CAN_START:
+ // Ensures that a fast pair data provider exists which will work in direct boot.
+ FastPairDataProvider.init(mContext);
+ break;
+ case PHASE_BOOT_COMPLETED:
+ // The nearby service must be functioning after this boot phase.
+ mSystemInjector.initializeBluetoothAdapter();
+ mContext.registerReceiver(
+ mBluetoothReceiver,
+ new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
+ mFastPairManager.initiate();
+ // Initialize ContextManager for CHRE scan.
+ mSystemInjector.initializeContextHubManagerAdapter();
+ mPresenceManager.initiate();
+ break;
+ }
+ }
+
+ private static final class SystemInjector implements Injector {
+ private final Context mContext;
+ @Nullable private BluetoothAdapter mBluetoothAdapter;
+ @Nullable private ContextHubManagerAdapter mContextHubManagerAdapter;
+
+ SystemInjector(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ @Nullable
+ public BluetoothAdapter getBluetoothAdapter() {
+ return mBluetoothAdapter;
+ }
+
+ @Override
+ @Nullable
+ public ContextHubManagerAdapter getContextHubManagerAdapter() {
+ return mContextHubManagerAdapter;
+ }
+
+ synchronized void initializeBluetoothAdapter() {
+ if (mBluetoothAdapter != null) {
+ return;
+ }
+ BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
+ if (manager == null) {
+ return;
+ }
+ mBluetoothAdapter = manager.getAdapter();
+ }
+
+ synchronized void initializeContextHubManagerAdapter() {
+ if (mContextHubManagerAdapter != null) {
+ return;
+ }
+ ContextHubManager manager = mContext.getSystemService(ContextHubManager.class);
+ if (manager == null) {
+ return;
+ }
+ mContextHubManagerAdapter = new ContextHubManagerAdapter(manager);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/BleFilter.java b/nearby/service/java/com/android/server/nearby/common/ble/BleFilter.java
new file mode 100644
index 0000000..23d5170
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/ble/BleFilter.java
@@ -0,0 +1,746 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.le.ScanFilter;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import androidx.annotation.Nullable;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Criteria for filtering BLE devices. A {@link BleFilter} allows clients to restrict BLE devices to
+ * only those that are of interest to them.
+ *
+ *
+ * <p>Current filtering on the following fields are supported:
+ * <li>Service UUIDs which identify the bluetooth gatt services running on the device.
+ * <li>Name of remote Bluetooth LE device.
+ * <li>Mac address of the remote device.
+ * <li>Service data which is the data associated with a service.
+ * <li>Manufacturer specific data which is the data associated with a particular manufacturer.
+ *
+ * @see BleSighting
+ */
+public final class BleFilter implements Parcelable {
+
+ @Nullable
+ private String mDeviceName;
+
+ @Nullable
+ private String mDeviceAddress;
+
+ @Nullable
+ private ParcelUuid mServiceUuid;
+
+ @Nullable
+ private ParcelUuid mServiceUuidMask;
+
+ @Nullable
+ private ParcelUuid mServiceDataUuid;
+
+ @Nullable
+ private byte[] mServiceData;
+
+ @Nullable
+ private byte[] mServiceDataMask;
+
+ private int mManufacturerId;
+
+ @Nullable
+ private byte[] mManufacturerData;
+
+ @Nullable
+ private byte[] mManufacturerDataMask;
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ BleFilter() {
+ }
+
+ BleFilter(
+ @Nullable String deviceName,
+ @Nullable String deviceAddress,
+ @Nullable ParcelUuid serviceUuid,
+ @Nullable ParcelUuid serviceUuidMask,
+ @Nullable ParcelUuid serviceDataUuid,
+ @Nullable byte[] serviceData,
+ @Nullable byte[] serviceDataMask,
+ int manufacturerId,
+ @Nullable byte[] manufacturerData,
+ @Nullable byte[] manufacturerDataMask) {
+ this.mDeviceName = deviceName;
+ this.mDeviceAddress = deviceAddress;
+ this.mServiceUuid = serviceUuid;
+ this.mServiceUuidMask = serviceUuidMask;
+ this.mServiceDataUuid = serviceDataUuid;
+ this.mServiceData = serviceData;
+ this.mServiceDataMask = serviceDataMask;
+ this.mManufacturerId = manufacturerId;
+ this.mManufacturerData = manufacturerData;
+ this.mManufacturerDataMask = manufacturerDataMask;
+ }
+
+ public static final Parcelable.Creator<BleFilter> CREATOR = new Creator<BleFilter>() {
+ @Override
+ public BleFilter createFromParcel(Parcel source) {
+ BleFilter nBleFilter = new BleFilter();
+ nBleFilter.mDeviceName = source.readString();
+ nBleFilter.mDeviceAddress = source.readString();
+ nBleFilter.mManufacturerId = source.readInt();
+ nBleFilter.mManufacturerData = source.marshall();
+ nBleFilter.mManufacturerDataMask = source.marshall();
+ nBleFilter.mServiceDataUuid = source.readParcelable(null);
+ nBleFilter.mServiceData = source.marshall();
+ nBleFilter.mServiceDataMask = source.marshall();
+ nBleFilter.mServiceUuid = source.readParcelable(null);
+ nBleFilter.mServiceUuidMask = source.readParcelable(null);
+ return nBleFilter;
+ }
+
+ @Override
+ public BleFilter[] newArray(int size) {
+ return new BleFilter[size];
+ }
+ };
+
+
+ /** Returns the filter set on the device name field of Bluetooth advertisement data. */
+ @Nullable
+ public String getDeviceName() {
+ return mDeviceName;
+ }
+
+ /** Returns the filter set on the service uuid. */
+ @Nullable
+ public ParcelUuid getServiceUuid() {
+ return mServiceUuid;
+ }
+
+ /** Returns the mask for the service uuid. */
+ @Nullable
+ public ParcelUuid getServiceUuidMask() {
+ return mServiceUuidMask;
+ }
+
+ /** Returns the filter set on the device address. */
+ @Nullable
+ public String getDeviceAddress() {
+ return mDeviceAddress;
+ }
+
+ /** Returns the filter set on the service data. */
+ @Nullable
+ public byte[] getServiceData() {
+ return mServiceData;
+ }
+
+ /** Returns the mask for the service data. */
+ @Nullable
+ public byte[] getServiceDataMask() {
+ return mServiceDataMask;
+ }
+
+ /** Returns the filter set on the service data uuid. */
+ @Nullable
+ public ParcelUuid getServiceDataUuid() {
+ return mServiceDataUuid;
+ }
+
+ /** Returns the manufacturer id. -1 if the manufacturer filter is not set. */
+ public int getManufacturerId() {
+ return mManufacturerId;
+ }
+
+ /** Returns the filter set on the manufacturer data. */
+ @Nullable
+ public byte[] getManufacturerData() {
+ return mManufacturerData;
+ }
+
+ /** Returns the mask for the manufacturer data. */
+ @Nullable
+ public byte[] getManufacturerDataMask() {
+ return mManufacturerDataMask;
+ }
+
+ /**
+ * Check if the filter matches a {@code BleSighting}. A BLE sighting is considered as a match if
+ * it matches all the field filters.
+ */
+ public boolean matches(@Nullable BleSighting bleSighting) {
+ if (bleSighting == null) {
+ return false;
+ }
+ BluetoothDevice device = bleSighting.getDevice();
+ // Device match.
+ if (mDeviceAddress != null && (device == null || !mDeviceAddress.equals(
+ device.getAddress()))) {
+ return false;
+ }
+
+ BleRecord bleRecord = bleSighting.getBleRecord();
+
+ // Scan record is null but there exist filters on it.
+ if (bleRecord == null
+ && (mDeviceName != null
+ || mServiceUuid != null
+ || mManufacturerData != null
+ || mServiceData != null)) {
+ return false;
+ }
+
+ // Local name match.
+ if (mDeviceName != null && !mDeviceName.equals(bleRecord.getDeviceName())) {
+ return false;
+ }
+
+ // UUID match.
+ if (mServiceUuid != null
+ && !matchesServiceUuids(mServiceUuid, mServiceUuidMask,
+ bleRecord.getServiceUuids())) {
+ return false;
+ }
+
+ // Service data match
+ if (mServiceDataUuid != null
+ && !matchesPartialData(
+ mServiceData, mServiceDataMask, bleRecord.getServiceData(mServiceDataUuid))) {
+ return false;
+ }
+
+ // Manufacturer data match.
+ if (mManufacturerId >= 0
+ && !matchesPartialData(
+ mManufacturerData,
+ mManufacturerDataMask,
+ bleRecord.getManufacturerSpecificData(mManufacturerId))) {
+ return false;
+ }
+
+ // All filters match.
+ return true;
+ }
+
+ /**
+ * Determines if the characteristics of this filter are a superset of the characteristics of the
+ * given filter.
+ */
+ public boolean isSuperset(@Nullable BleFilter bleFilter) {
+ if (bleFilter == null) {
+ return false;
+ }
+
+ if (equals(bleFilter)) {
+ return true;
+ }
+
+ // Verify device address matches.
+ if (mDeviceAddress != null && !mDeviceAddress.equals(bleFilter.getDeviceAddress())) {
+ return false;
+ }
+
+ // Verify device name matches.
+ if (mDeviceName != null && !mDeviceName.equals(bleFilter.getDeviceName())) {
+ return false;
+ }
+
+ // Verify UUID is a superset.
+ if (mServiceUuid != null
+ && !serviceUuidIsSuperset(
+ mServiceUuid,
+ mServiceUuidMask,
+ bleFilter.getServiceUuid(),
+ bleFilter.getServiceUuidMask())) {
+ return false;
+ }
+
+ // Verify service data is a superset.
+ if (mServiceDataUuid != null
+ && (!mServiceDataUuid.equals(bleFilter.getServiceDataUuid())
+ || !partialDataIsSuperset(
+ mServiceData,
+ mServiceDataMask,
+ bleFilter.getServiceData(),
+ bleFilter.getServiceDataMask()))) {
+ return false;
+ }
+
+ // Verify manufacturer data is a superset.
+ if (mManufacturerId >= 0
+ && (mManufacturerId != bleFilter.getManufacturerId()
+ || !partialDataIsSuperset(
+ mManufacturerData,
+ mManufacturerDataMask,
+ bleFilter.getManufacturerData(),
+ bleFilter.getManufacturerDataMask()))) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /** Determines if the first uuid and mask are a superset of the second uuid and mask. */
+ private static boolean serviceUuidIsSuperset(
+ @Nullable ParcelUuid uuid1,
+ @Nullable ParcelUuid uuidMask1,
+ @Nullable ParcelUuid uuid2,
+ @Nullable ParcelUuid uuidMask2) {
+ // First uuid1 is null so it can match any service UUID.
+ if (uuid1 == null) {
+ return true;
+ }
+
+ // uuid2 is a superset of uuid1, but not the other way around.
+ if (uuid2 == null) {
+ return false;
+ }
+
+ // Without a mask, the uuids must match.
+ if (uuidMask1 == null) {
+ return uuid1.equals(uuid2);
+ }
+
+ // Mask2 should be at least as specific as mask1.
+ if (uuidMask2 != null) {
+ long uuid1MostSig = uuidMask1.getUuid().getMostSignificantBits();
+ long uuid1LeastSig = uuidMask1.getUuid().getLeastSignificantBits();
+ long uuid2MostSig = uuidMask2.getUuid().getMostSignificantBits();
+ long uuid2LeastSig = uuidMask2.getUuid().getLeastSignificantBits();
+ if (((uuid1MostSig & uuid2MostSig) != uuid1MostSig)
+ || ((uuid1LeastSig & uuid2LeastSig) != uuid1LeastSig)) {
+ return false;
+ }
+ }
+
+ if (!matchesServiceUuids(uuid1, uuidMask1, Arrays.asList(uuid2))) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /** Determines if the first data and mask are the superset of the second data and mask. */
+ private static boolean partialDataIsSuperset(
+ @Nullable byte[] data1,
+ @Nullable byte[] dataMask1,
+ @Nullable byte[] data2,
+ @Nullable byte[] dataMask2) {
+ if (Arrays.equals(data1, data2) && Arrays.equals(dataMask1, dataMask2)) {
+ return true;
+ }
+
+ if (data1 == null) {
+ return true;
+ }
+
+ if (data2 == null) {
+ return false;
+ }
+
+ // Mask2 should be at least as specific as mask1.
+ if (dataMask1 != null && dataMask2 != null) {
+ for (int i = 0, j = 0; i < dataMask1.length && j < dataMask2.length; i++, j++) {
+ if ((dataMask1[i] & dataMask2[j]) != dataMask1[i]) {
+ return false;
+ }
+ }
+ }
+
+ if (!matchesPartialData(data1, dataMask1, data2)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /** Check if the uuid pattern is contained in a list of parcel uuids. */
+ private static boolean matchesServiceUuids(
+ @Nullable ParcelUuid uuid, @Nullable ParcelUuid parcelUuidMask,
+ List<ParcelUuid> uuids) {
+ if (uuid == null) {
+ // No service uuid filter has been set, so there's a match.
+ return true;
+ }
+
+ UUID uuidMask = parcelUuidMask == null ? null : parcelUuidMask.getUuid();
+ for (ParcelUuid parcelUuid : uuids) {
+ if (matchesServiceUuid(uuid.getUuid(), uuidMask, parcelUuid.getUuid())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Check if the uuid pattern matches the particular service uuid. */
+ private static boolean matchesServiceUuid(UUID uuid, @Nullable UUID mask, UUID data) {
+ if (mask == null) {
+ return uuid.equals(data);
+ }
+ if ((uuid.getLeastSignificantBits() & mask.getLeastSignificantBits())
+ != (data.getLeastSignificantBits() & mask.getLeastSignificantBits())) {
+ return false;
+ }
+ return ((uuid.getMostSignificantBits() & mask.getMostSignificantBits())
+ == (data.getMostSignificantBits() & mask.getMostSignificantBits()));
+ }
+
+ /**
+ * Check whether the data pattern matches the parsed data. Assumes that {@code data} and {@code
+ * dataMask} have the same length.
+ */
+ /* package */
+ static boolean matchesPartialData(
+ @Nullable byte[] data, @Nullable byte[] dataMask, @Nullable byte[] parsedData) {
+ if (data == null || parsedData == null || parsedData.length < data.length) {
+ return false;
+ }
+ if (dataMask == null) {
+ for (int i = 0; i < data.length; ++i) {
+ if (parsedData[i] != data[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ for (int i = 0; i < data.length; ++i) {
+ if ((dataMask[i] & parsedData[i]) != (dataMask[i] & data[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "BleFilter [deviceName="
+ + mDeviceName
+ + ", deviceAddress="
+ + mDeviceAddress
+ + ", uuid="
+ + mServiceUuid
+ + ", uuidMask="
+ + mServiceUuidMask
+ + ", serviceDataUuid="
+ + mServiceDataUuid
+ + ", serviceData="
+ + Arrays.toString(mServiceData)
+ + ", serviceDataMask="
+ + Arrays.toString(mServiceDataMask)
+ + ", manufacturerId="
+ + mManufacturerId
+ + ", manufacturerData="
+ + Arrays.toString(mManufacturerData)
+ + ", manufacturerDataMask="
+ + Arrays.toString(mManufacturerDataMask)
+ + "]";
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mDeviceName);
+ out.writeString(mDeviceAddress);
+ out.writeInt(mManufacturerId);
+ out.writeByteArray(mManufacturerData);
+ out.writeByteArray(mManufacturerDataMask);
+ out.writeParcelable(mServiceDataUuid, flags);
+ out.writeByteArray(mServiceData);
+ out.writeByteArray(mServiceDataMask);
+ out.writeParcelable(mServiceUuid, flags);
+ out.writeParcelable(mServiceUuidMask, flags);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mDeviceName,
+ mDeviceAddress,
+ mManufacturerId,
+ Arrays.hashCode(mManufacturerData),
+ Arrays.hashCode(mManufacturerDataMask),
+ mServiceDataUuid,
+ Arrays.hashCode(mServiceData),
+ Arrays.hashCode(mServiceDataMask),
+ mServiceUuid,
+ mServiceUuidMask);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ BleFilter other = (BleFilter) obj;
+ return mDeviceName.equals(other.mDeviceName)
+ && mDeviceAddress.equals(other.mDeviceAddress)
+ && mManufacturerId == other.mManufacturerId
+ && Arrays.equals(mManufacturerData, other.mManufacturerData)
+ && Arrays.equals(mManufacturerDataMask, other.mManufacturerDataMask)
+ && mServiceDataUuid.equals(other.mServiceDataUuid)
+ && Arrays.equals(mServiceData, other.mServiceData)
+ && Arrays.equals(mServiceDataMask, other.mServiceDataMask)
+ && mServiceUuid.equals(other.mServiceUuid)
+ && mServiceUuidMask.equals(other.mServiceUuidMask);
+ }
+
+ /** Builder class for {@link BleFilter}. */
+ public static final class Builder {
+
+ private String mDeviceName;
+ private String mDeviceAddress;
+
+ @Nullable
+ private ParcelUuid mServiceUuid;
+ @Nullable
+ private ParcelUuid mUuidMask;
+
+ private ParcelUuid mServiceDataUuid;
+ @Nullable
+ private byte[] mServiceData;
+ @Nullable
+ private byte[] mServiceDataMask;
+
+ private int mManufacturerId = -1;
+ private byte[] mManufacturerData;
+ @Nullable
+ private byte[] mManufacturerDataMask;
+
+ /** Set filter on device name. */
+ public Builder setDeviceName(String deviceName) {
+ this.mDeviceName = deviceName;
+ return this;
+ }
+
+ /**
+ * Set filter on device address.
+ *
+ * @param deviceAddress The device Bluetooth address for the filter. It needs to be in the
+ * format of "01:02:03:AB:CD:EF". The device address can be validated
+ * using {@link
+ * BluetoothAdapter#checkBluetoothAddress}.
+ * @throws IllegalArgumentException If the {@code deviceAddress} is invalid.
+ */
+ public Builder setDeviceAddress(String deviceAddress) {
+ if (!BluetoothAdapter.checkBluetoothAddress(deviceAddress)) {
+ throw new IllegalArgumentException("invalid device address " + deviceAddress);
+ }
+ this.mDeviceAddress = deviceAddress;
+ return this;
+ }
+
+ /** Set filter on service uuid. */
+ public Builder setServiceUuid(@Nullable ParcelUuid serviceUuid) {
+ this.mServiceUuid = serviceUuid;
+ mUuidMask = null; // clear uuid mask
+ return this;
+ }
+
+ /**
+ * Set filter on partial service uuid. The {@code uuidMask} is the bit mask for the {@code
+ * serviceUuid}. Set any bit in the mask to 1 to indicate a match is needed for the bit in
+ * {@code serviceUuid}, and 0 to ignore that bit.
+ *
+ * @throws IllegalArgumentException If {@code serviceUuid} is {@code null} but {@code
+ * uuidMask}
+ * is not {@code null}.
+ */
+ public Builder setServiceUuid(@Nullable ParcelUuid serviceUuid,
+ @Nullable ParcelUuid uuidMask) {
+ if (uuidMask != null && serviceUuid == null) {
+ throw new IllegalArgumentException("uuid is null while uuidMask is not null!");
+ }
+ this.mServiceUuid = serviceUuid;
+ this.mUuidMask = uuidMask;
+ return this;
+ }
+
+ /**
+ * Set filtering on service data.
+ */
+ public Builder setServiceData(ParcelUuid serviceDataUuid, @Nullable byte[] serviceData) {
+ this.mServiceDataUuid = serviceDataUuid;
+ this.mServiceData = serviceData;
+ mServiceDataMask = null; // clear service data mask
+ return this;
+ }
+
+ /**
+ * Set partial filter on service data. For any bit in the mask, set it to 1 if it needs to
+ * match
+ * the one in service data, otherwise set it to 0 to ignore that bit.
+ *
+ * <p>The {@code serviceDataMask} must have the same length of the {@code serviceData}.
+ *
+ * @throws IllegalArgumentException If {@code serviceDataMask} is {@code null} while {@code
+ * serviceData} is not or {@code serviceDataMask} and
+ * {@code serviceData} has different
+ * length.
+ */
+ public Builder setServiceData(
+ ParcelUuid serviceDataUuid,
+ @Nullable byte[] serviceData,
+ @Nullable byte[] serviceDataMask) {
+ if (serviceDataMask != null) {
+ if (serviceData == null) {
+ throw new IllegalArgumentException(
+ "serviceData is null while serviceDataMask is not null");
+ }
+ // Since the serviceDataMask is a bit mask for serviceData, the lengths of the two
+ // byte array need to be the same.
+ if (serviceData.length != serviceDataMask.length) {
+ throw new IllegalArgumentException(
+ "size mismatch for service data and service data mask");
+ }
+ }
+ this.mServiceDataUuid = serviceDataUuid;
+ this.mServiceData = serviceData;
+ this.mServiceDataMask = serviceDataMask;
+ return this;
+ }
+
+ /**
+ * Set filter on on manufacturerData. A negative manufacturerId is considered as invalid id.
+ *
+ * <p>Note the first two bytes of the {@code manufacturerData} is the manufacturerId.
+ *
+ * @throws IllegalArgumentException If the {@code manufacturerId} is invalid.
+ */
+ public Builder setManufacturerData(int manufacturerId, @Nullable byte[] manufacturerData) {
+ return setManufacturerData(manufacturerId, manufacturerData, null /* mask */);
+ }
+
+ /**
+ * Set filter on partial manufacture data. For any bit in the mask, set it to 1 if it needs
+ * to
+ * match the one in manufacturer data, otherwise set it to 0.
+ *
+ * <p>The {@code manufacturerDataMask} must have the same length of {@code
+ * manufacturerData}.
+ *
+ * @throws IllegalArgumentException If the {@code manufacturerId} is invalid, or {@code
+ * manufacturerData} is null while {@code
+ * manufacturerDataMask} is not, or {@code
+ * manufacturerData} and {@code manufacturerDataMask} have
+ * different length.
+ */
+ public Builder setManufacturerData(
+ int manufacturerId,
+ @Nullable byte[] manufacturerData,
+ @Nullable byte[] manufacturerDataMask) {
+ if (manufacturerData != null && manufacturerId < 0) {
+ throw new IllegalArgumentException("invalid manufacture id");
+ }
+ if (manufacturerDataMask != null) {
+ if (manufacturerData == null) {
+ throw new IllegalArgumentException(
+ "manufacturerData is null while manufacturerDataMask is not null");
+ }
+ // Since the manufacturerDataMask is a bit mask for manufacturerData, the lengths
+ // of the two byte array need to be the same.
+ if (manufacturerData.length != manufacturerDataMask.length) {
+ throw new IllegalArgumentException(
+ "size mismatch for manufacturerData and manufacturerDataMask");
+ }
+ }
+ this.mManufacturerId = manufacturerId;
+ this.mManufacturerData = manufacturerData == null ? new byte[0] : manufacturerData;
+ this.mManufacturerDataMask = manufacturerDataMask;
+ return this;
+ }
+
+
+ /**
+ * Builds the filter.
+ *
+ * @throws IllegalArgumentException If the filter cannot be built.
+ */
+ public BleFilter build() {
+ return new BleFilter(
+ mDeviceName,
+ mDeviceAddress,
+ mServiceUuid,
+ mUuidMask,
+ mServiceDataUuid,
+ mServiceData,
+ mServiceDataMask,
+ mManufacturerId,
+ mManufacturerData,
+ mManufacturerDataMask);
+ }
+ }
+
+ /**
+ * Changes ble filter to os filter
+ */
+ public ScanFilter toOsFilter() {
+ ScanFilter.Builder osFilterBuilder = new ScanFilter.Builder();
+ if (!TextUtils.isEmpty(getDeviceAddress())) {
+ osFilterBuilder.setDeviceAddress(getDeviceAddress());
+ }
+ if (!TextUtils.isEmpty(getDeviceName())) {
+ osFilterBuilder.setDeviceName(getDeviceName());
+ }
+
+ byte[] manufacturerData = getManufacturerData();
+ if (getManufacturerId() != -1 && manufacturerData != null) {
+ byte[] manufacturerDataMask = getManufacturerDataMask();
+ if (manufacturerDataMask != null) {
+ osFilterBuilder.setManufacturerData(
+ getManufacturerId(), manufacturerData, manufacturerDataMask);
+ } else {
+ osFilterBuilder.setManufacturerData(getManufacturerId(), manufacturerData);
+ }
+ }
+
+ ParcelUuid serviceDataUuid = getServiceDataUuid();
+ byte[] serviceData = getServiceData();
+ if (serviceDataUuid != null && serviceData != null) {
+ byte[] serviceDataMask = getServiceDataMask();
+ if (serviceDataMask != null) {
+ osFilterBuilder.setServiceData(serviceDataUuid, serviceData, serviceDataMask);
+ } else {
+ osFilterBuilder.setServiceData(serviceDataUuid, serviceData);
+ }
+ }
+
+ ParcelUuid serviceUuid = getServiceUuid();
+ if (serviceUuid != null) {
+ ParcelUuid serviceUuidMask = getServiceUuidMask();
+ if (serviceUuidMask != null) {
+ osFilterBuilder.setServiceUuid(serviceUuid, serviceUuidMask);
+ } else {
+ osFilterBuilder.setServiceUuid(serviceUuid);
+ }
+ }
+ return osFilterBuilder.build();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/BleRecord.java b/nearby/service/java/com/android/server/nearby/common/ble/BleRecord.java
new file mode 100644
index 0000000..103a27f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/ble/BleRecord.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble;
+
+import android.os.ParcelUuid;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.Nullable;
+
+import com.android.server.nearby.common.ble.util.StringUtils;
+
+import com.google.common.collect.ImmutableList;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Represents a BLE record from Bluetooth LE scan.
+ */
+public final class BleRecord {
+
+ // The following data type values are assigned by Bluetooth SIG.
+ // For more details refer to Bluetooth 4.1 specification, Volume 3, Part C, Section 18.
+ private static final int DATA_TYPE_FLAGS = 0x01;
+ private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL = 0x02;
+ private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE = 0x03;
+ private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL = 0x04;
+ private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE = 0x05;
+ private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL = 0x06;
+ private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE = 0x07;
+ private static final int DATA_TYPE_LOCAL_NAME_SHORT = 0x08;
+ private static final int DATA_TYPE_LOCAL_NAME_COMPLETE = 0x09;
+ private static final int DATA_TYPE_TX_POWER_LEVEL = 0x0A;
+ private static final int DATA_TYPE_SERVICE_DATA = 0x16;
+ private static final int DATA_TYPE_MANUFACTURER_SPECIFIC_DATA = 0xFF;
+
+ /** The base 128-bit UUID representation of a 16-bit UUID. */
+ private static final ParcelUuid BASE_UUID =
+ ParcelUuid.fromString("00000000-0000-1000-8000-00805F9B34FB");
+ /** Length of bytes for 16 bit UUID. */
+ private static final int UUID_BYTES_16_BIT = 2;
+ /** Length of bytes for 32 bit UUID. */
+ private static final int UUID_BYTES_32_BIT = 4;
+ /** Length of bytes for 128 bit UUID. */
+ private static final int UUID_BYTES_128_BIT = 16;
+
+ // Flags of the advertising data.
+ // -1 when the scan record is not valid.
+ private final int mAdvertiseFlags;
+
+ private final ImmutableList<ParcelUuid> mServiceUuids;
+
+ // null when the scan record is not valid.
+ @Nullable
+ private final SparseArray<byte[]> mManufacturerSpecificData;
+
+ // null when the scan record is not valid.
+ @Nullable
+ private final Map<ParcelUuid, byte[]> mServiceData;
+
+ // Transmission power level(in dB).
+ // Integer.MIN_VALUE when the scan record is not valid.
+ private final int mTxPowerLevel;
+
+ // Local name of the Bluetooth LE device.
+ // null when the scan record is not valid.
+ @Nullable
+ private final String mDeviceName;
+
+ // Raw bytes of scan record.
+ // Never null, whether valid or not.
+ private final byte[] mBytes;
+
+ // If the raw scan record byte[] cannot be parsed, all non-primitive args here other than the
+ // raw scan record byte[] and serviceUudis will be null. See parsefromBytes().
+ private BleRecord(
+ List<ParcelUuid> serviceUuids,
+ @Nullable SparseArray<byte[]> manufacturerData,
+ @Nullable Map<ParcelUuid, byte[]> serviceData,
+ int advertiseFlags,
+ int txPowerLevel,
+ @Nullable String deviceName,
+ byte[] bytes) {
+ this.mServiceUuids = ImmutableList.copyOf(serviceUuids);
+ mManufacturerSpecificData = manufacturerData;
+ this.mServiceData = serviceData;
+ this.mDeviceName = deviceName;
+ this.mAdvertiseFlags = advertiseFlags;
+ this.mTxPowerLevel = txPowerLevel;
+ this.mBytes = bytes;
+ }
+
+ /**
+ * Returns a list of service UUIDs within the advertisement that are used to identify the
+ * bluetooth GATT services.
+ */
+ public ImmutableList<ParcelUuid> getServiceUuids() {
+ return mServiceUuids;
+ }
+
+ /**
+ * Returns a sparse array of manufacturer identifier and its corresponding manufacturer specific
+ * data.
+ */
+ @Nullable
+ public SparseArray<byte[]> getManufacturerSpecificData() {
+ return mManufacturerSpecificData;
+ }
+
+ /**
+ * Returns the manufacturer specific data associated with the manufacturer id. Returns {@code
+ * null} if the {@code manufacturerId} is not found.
+ */
+ @Nullable
+ public byte[] getManufacturerSpecificData(int manufacturerId) {
+ if (mManufacturerSpecificData == null) {
+ return null;
+ }
+ return mManufacturerSpecificData.get(manufacturerId);
+ }
+
+ /** Returns a map of service UUID and its corresponding service data. */
+ @Nullable
+ public Map<ParcelUuid, byte[]> getServiceData() {
+ return mServiceData;
+ }
+
+ /**
+ * Returns the service data byte array associated with the {@code serviceUuid}. Returns {@code
+ * null} if the {@code serviceDataUuid} is not found.
+ */
+ @Nullable
+ public byte[] getServiceData(ParcelUuid serviceDataUuid) {
+ if (serviceDataUuid == null || mServiceData == null) {
+ return null;
+ }
+ return mServiceData.get(serviceDataUuid);
+ }
+
+ /**
+ * Returns the transmission power level of the packet in dBm. Returns {@link Integer#MIN_VALUE}
+ * if
+ * the field is not set. This value can be used to calculate the path loss of a received packet
+ * using the following equation:
+ *
+ * <p><code>pathloss = txPowerLevel - rssi</code>
+ */
+ public int getTxPowerLevel() {
+ return mTxPowerLevel;
+ }
+
+ /** Returns the local name of the BLE device. The is a UTF-8 encoded string. */
+ @Nullable
+ public String getDeviceName() {
+ return mDeviceName;
+ }
+
+ /** Returns raw bytes of scan record. */
+ public byte[] getBytes() {
+ return mBytes;
+ }
+
+ /**
+ * Parse scan record bytes to {@link BleRecord}.
+ *
+ * <p>The format is defined in Bluetooth 4.1 specification, Volume 3, Part C, Section 11 and 18.
+ *
+ * <p>All numerical multi-byte entities and values shall use little-endian <strong>byte</strong>
+ * order.
+ *
+ * @param scanRecord The scan record of Bluetooth LE advertisement and/or scan response.
+ */
+ public static BleRecord parseFromBytes(byte[] scanRecord) {
+ int currentPos = 0;
+ int advertiseFlag = -1;
+ List<ParcelUuid> serviceUuids = new ArrayList<>();
+ String localName = null;
+ int txPowerLevel = Integer.MIN_VALUE;
+
+ SparseArray<byte[]> manufacturerData = new SparseArray<>();
+ Map<ParcelUuid, byte[]> serviceData = new HashMap<>();
+
+ try {
+ while (currentPos < scanRecord.length) {
+ // length is unsigned int.
+ int length = scanRecord[currentPos++] & 0xFF;
+ if (length == 0) {
+ break;
+ }
+ // Note the length includes the length of the field type itself.
+ int dataLength = length - 1;
+ // fieldType is unsigned int.
+ int fieldType = scanRecord[currentPos++] & 0xFF;
+ switch (fieldType) {
+ case DATA_TYPE_FLAGS:
+ advertiseFlag = scanRecord[currentPos] & 0xFF;
+ break;
+ case DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL:
+ case DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE:
+ parseServiceUuid(scanRecord, currentPos, dataLength, UUID_BYTES_16_BIT,
+ serviceUuids);
+ break;
+ case DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL:
+ case DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE:
+ parseServiceUuid(scanRecord, currentPos, dataLength, UUID_BYTES_32_BIT,
+ serviceUuids);
+ break;
+ case DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL:
+ case DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE:
+ parseServiceUuid(scanRecord, currentPos, dataLength, UUID_BYTES_128_BIT,
+ serviceUuids);
+ break;
+ case DATA_TYPE_LOCAL_NAME_SHORT:
+ case DATA_TYPE_LOCAL_NAME_COMPLETE:
+ localName = new String(extractBytes(scanRecord, currentPos, dataLength));
+ break;
+ case DATA_TYPE_TX_POWER_LEVEL:
+ txPowerLevel = scanRecord[currentPos];
+ break;
+ case DATA_TYPE_SERVICE_DATA:
+ // The first two bytes of the service data are service data UUID in little
+ // endian. The rest bytes are service data.
+ int serviceUuidLength = UUID_BYTES_16_BIT;
+ byte[] serviceDataUuidBytes = extractBytes(scanRecord, currentPos,
+ serviceUuidLength);
+ ParcelUuid serviceDataUuid = parseUuidFrom(serviceDataUuidBytes);
+ byte[] serviceDataArray =
+ extractBytes(
+ scanRecord, currentPos + serviceUuidLength,
+ dataLength - serviceUuidLength);
+ serviceData.put(serviceDataUuid, serviceDataArray);
+ break;
+ case DATA_TYPE_MANUFACTURER_SPECIFIC_DATA:
+ // The first two bytes of the manufacturer specific data are
+ // manufacturer ids in little endian.
+ int manufacturerId =
+ ((scanRecord[currentPos + 1] & 0xFF) << 8) + (scanRecord[currentPos]
+ & 0xFF);
+ byte[] manufacturerDataBytes = extractBytes(scanRecord, currentPos + 2,
+ dataLength - 2);
+ manufacturerData.put(manufacturerId, manufacturerDataBytes);
+ break;
+ default:
+ // Just ignore, we don't handle such data type.
+ break;
+ }
+ currentPos += dataLength;
+ }
+
+ return new BleRecord(
+ serviceUuids,
+ manufacturerData,
+ serviceData,
+ advertiseFlag,
+ txPowerLevel,
+ localName,
+ scanRecord);
+ } catch (Exception e) {
+ Log.w("BleRecord", "Unable to parse scan record: " + Arrays.toString(scanRecord), e);
+ // As the record is invalid, ignore all the parsed results for this packet
+ // and return an empty record with raw scanRecord bytes in results
+ // check at the top of this method does? Maybe we expect callers to use the
+ // scanRecord part in
+ // some fallback. But if that's the reason, it would seem we still can return null.
+ // They still
+ // have the raw scanRecord in hand, 'cause they passed it to us. It seems too easy for a
+ // caller to misuse this "empty" BleRecord (as in b/22693067).
+ return new BleRecord(ImmutableList.of(), null, null, -1, Integer.MIN_VALUE, null,
+ scanRecord);
+ }
+ }
+
+ // Parse service UUIDs.
+ private static int parseServiceUuid(
+ byte[] scanRecord,
+ int currentPos,
+ int dataLength,
+ int uuidLength,
+ List<ParcelUuid> serviceUuids) {
+ while (dataLength > 0) {
+ byte[] uuidBytes = extractBytes(scanRecord, currentPos, uuidLength);
+ serviceUuids.add(parseUuidFrom(uuidBytes));
+ dataLength -= uuidLength;
+ currentPos += uuidLength;
+ }
+ return currentPos;
+ }
+
+ // Helper method to extract bytes from byte array.
+ private static byte[] extractBytes(byte[] scanRecord, int start, int length) {
+ byte[] bytes = new byte[length];
+ System.arraycopy(scanRecord, start, bytes, 0, length);
+ return bytes;
+ }
+
+ @Override
+ public String toString() {
+ return "BleRecord [advertiseFlags="
+ + mAdvertiseFlags
+ + ", serviceUuids="
+ + mServiceUuids
+ + ", manufacturerSpecificData="
+ + StringUtils.toString(mManufacturerSpecificData)
+ + ", serviceData="
+ + StringUtils.toString(mServiceData)
+ + ", txPowerLevel="
+ + mTxPowerLevel
+ + ", deviceName="
+ + mDeviceName
+ + "]";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof BleRecord)) {
+ return false;
+ }
+ BleRecord record = (BleRecord) obj;
+ // BleRecord objects are built from bytes, so we only need that field.
+ return Arrays.equals(mBytes, record.mBytes);
+ }
+
+ @Override
+ public int hashCode() {
+ // BleRecord objects are built from bytes, so we only need that field.
+ return Arrays.hashCode(mBytes);
+ }
+
+ /**
+ * Parse UUID from bytes. The {@code uuidBytes} can represent a 16-bit, 32-bit or 128-bit UUID,
+ * but the returned UUID is always in 128-bit format. Note UUID is little endian in Bluetooth.
+ *
+ * @param uuidBytes Byte representation of uuid.
+ * @return {@link ParcelUuid} parsed from bytes.
+ * @throws IllegalArgumentException If the {@code uuidBytes} cannot be parsed.
+ */
+ private static ParcelUuid parseUuidFrom(byte[] uuidBytes) {
+ if (uuidBytes == null) {
+ throw new IllegalArgumentException("uuidBytes cannot be null");
+ }
+ int length = uuidBytes.length;
+ if (length != UUID_BYTES_16_BIT
+ && length != UUID_BYTES_32_BIT
+ && length != UUID_BYTES_128_BIT) {
+ throw new IllegalArgumentException("uuidBytes length invalid - " + length);
+ }
+ // Construct a 128 bit UUID.
+ if (length == UUID_BYTES_128_BIT) {
+ ByteBuffer buf = ByteBuffer.wrap(uuidBytes).order(ByteOrder.LITTLE_ENDIAN);
+ long msb = buf.getLong(8);
+ long lsb = buf.getLong(0);
+ return new ParcelUuid(new UUID(msb, lsb));
+ }
+ // For 16 bit and 32 bit UUID we need to convert them to 128 bit value.
+ // 128_bit_value = uuid * 2^96 + BASE_UUID
+ long shortUuid;
+ if (length == UUID_BYTES_16_BIT) {
+ shortUuid = uuidBytes[0] & 0xFF;
+ shortUuid += (uuidBytes[1] & 0xFF) << 8;
+ } else {
+ shortUuid = uuidBytes[0] & 0xFF;
+ shortUuid += (uuidBytes[1] & 0xFF) << 8;
+ shortUuid += (uuidBytes[2] & 0xFF) << 16;
+ shortUuid += (uuidBytes[3] & 0xFF) << 24;
+ }
+ long msb = BASE_UUID.getUuid().getMostSignificantBits() + (shortUuid << 32);
+ long lsb = BASE_UUID.getUuid().getLeastSignificantBits();
+ return new ParcelUuid(new UUID(msb, lsb));
+ }
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/BleSighting.java b/nearby/service/java/com/android/server/nearby/common/ble/BleSighting.java
new file mode 100644
index 0000000..71ec10c
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/ble/BleSighting.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.os.Build.VERSION_CODES;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A sighting of a BLE device found in a Bluetooth LE scan.
+ */
+
+public class BleSighting implements Parcelable {
+
+ public static final Parcelable.Creator<BleSighting> CREATOR = new Creator<BleSighting>() {
+ @Override
+ public BleSighting createFromParcel(Parcel source) {
+ BleSighting nBleSighting = new BleSighting(source.readParcelable(null),
+ source.marshall(), source.readInt(), source.readLong());
+ return null;
+ }
+
+ @Override
+ public BleSighting[] newArray(int size) {
+ return new BleSighting[size];
+ }
+ };
+
+ // Max and min rssi value which is from {@link android.bluetooth.le.ScanResult#getRssi()}.
+ @VisibleForTesting
+ public static final int MAX_RSSI_VALUE = 126;
+ @VisibleForTesting
+ public static final int MIN_RSSI_VALUE = -127;
+
+ /** Remote bluetooth device. */
+ private final BluetoothDevice mDevice;
+
+ /**
+ * BLE record, including advertising data and response data. BleRecord is not parcelable, so
+ * this
+ * is created from bleRecordBytes.
+ */
+ private final BleRecord mBleRecord;
+
+ /** The bytes of a BLE record. */
+ private final byte[] mBleRecordBytes;
+
+ /** Received signal strength. */
+ private final int mRssi;
+
+ /** Nanos timestamp when the ble device was observed (epoch time). */
+ private final long mTimestampEpochNanos;
+
+ /**
+ * Constructor of a BLE sighting.
+ *
+ * @param device Remote bluetooth device that is found.
+ * @param bleRecordBytes The bytes that will create a BleRecord.
+ * @param rssi Received signal strength.
+ * @param timestampEpochNanos Nanos timestamp when the BLE device was observed (epoch time).
+ */
+ public BleSighting(BluetoothDevice device, byte[] bleRecordBytes, int rssi,
+ long timestampEpochNanos) {
+ this.mDevice = device;
+ this.mBleRecordBytes = bleRecordBytes;
+ this.mRssi = rssi;
+ this.mTimestampEpochNanos = timestampEpochNanos;
+ mBleRecord = BleRecord.parseFromBytes(bleRecordBytes);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Returns the remote bluetooth device identified by the bluetooth device address. */
+ public BluetoothDevice getDevice() {
+ return mDevice;
+ }
+
+ /** Returns the BLE record, which is a combination of advertisement and scan response. */
+ public BleRecord getBleRecord() {
+ return mBleRecord;
+ }
+
+ /** Returns the bytes of the BLE record. */
+ public byte[] getBleRecordBytes() {
+ return mBleRecordBytes;
+ }
+
+ /** Returns the received signal strength in dBm. The valid range is [-127, 127]. */
+ public int getRssi() {
+ return mRssi;
+ }
+
+ /**
+ * Returns the received signal strength normalized with the offset specific to the given device.
+ * 3 is the rssi offset to calculate fast init distance.
+ * <p>This method utilized the rssi offset maintained by Nearby Sharing.
+ *
+ * @return normalized rssi which is between [-127, 126] according to {@link
+ * android.bluetooth.le.ScanResult#getRssi()}.
+ */
+ public int getNormalizedRSSI() {
+ int adjustedRssi = mRssi + 3;
+ if (adjustedRssi < MIN_RSSI_VALUE) {
+ return MIN_RSSI_VALUE;
+ } else if (adjustedRssi > MAX_RSSI_VALUE) {
+ return MAX_RSSI_VALUE;
+ } else {
+ return adjustedRssi;
+ }
+ }
+
+ /** Returns timestamp in epoch time when the scan record was observed. */
+ public long getTimestampNanos() {
+ return mTimestampEpochNanos;
+ }
+
+ /** Returns timestamp in epoch time when the scan record was observed, in millis. */
+ public long getTimestampMillis() {
+ return TimeUnit.NANOSECONDS.toMillis(mTimestampEpochNanos);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mDevice, flags);
+ dest.writeByteArray(mBleRecordBytes);
+ dest.writeInt(mRssi);
+ dest.writeLong(mTimestampEpochNanos);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDevice, mRssi, mTimestampEpochNanos, Arrays.hashCode(mBleRecordBytes));
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof BleSighting)) {
+ return false;
+ }
+ BleSighting other = (BleSighting) obj;
+ return Objects.equals(mDevice, other.mDevice)
+ && mRssi == other.mRssi
+ && Arrays.equals(mBleRecordBytes, other.mBleRecordBytes)
+ && mTimestampEpochNanos == other.mTimestampEpochNanos;
+ }
+
+ @Override
+ public String toString() {
+ return "BleSighting{"
+ + "device="
+ + mDevice
+ + ", bleRecord="
+ + mBleRecord
+ + ", rssi="
+ + mRssi
+ + ", timestampNanos="
+ + mTimestampEpochNanos
+ + "}";
+ }
+
+ /** Creates {@link BleSighting} using the {@link ScanResult}. */
+ @RequiresApi(api = VERSION_CODES.LOLLIPOP)
+ @Nullable
+ public static BleSighting createFromOsScanResult(ScanResult osResult) {
+ ScanRecord osScanRecord = osResult.getScanRecord();
+ if (osScanRecord == null) {
+ return null;
+ }
+
+ return new BleSighting(
+ osResult.getDevice(),
+ osScanRecord.getBytes(),
+ osResult.getRssi(),
+ // The timestamp from ScanResult is 'nanos since boot', Beacon lib will change it
+ // as 'nanos
+ // since epoch', but Nearby never reference this field, just pass it as 'nanos
+ // since boot'.
+ // ref to beacon/scan/impl/LBluetoothLeScannerCompat.fromOs for beacon design
+ // about how to
+ // convert nanos since boot to epoch.
+ osResult.getTimestampNanos());
+ }
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/decode/BeaconDecoder.java b/nearby/service/java/com/android/server/nearby/common/ble/decode/BeaconDecoder.java
new file mode 100644
index 0000000..9e795ac
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/ble/decode/BeaconDecoder.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble.decode;
+
+import androidx.annotation.Nullable;
+
+import com.android.server.nearby.common.ble.BleRecord;
+
+/**
+ * This class encapsulates the logic specific to each manufacturer for parsing formats for beacons,
+ * and presents a common API to access important ADV/EIR packet fields such as:
+ *
+ * <ul>
+ * <li><b>UUID (universally unique identifier)</b>, a value uniquely identifying a group of one or
+ * more beacons as belonging to an organization or of a certain type, up to 128 bits.
+ * <li><b>Instance</b> a 32-bit unsigned integer that can be used to group related beacons that
+ * have the same UUID.
+ * <li>the mathematics of <b>TX signal strength</b>, used for proximity calculations.
+ * </ul>
+ *
+ * ...and others.
+ *
+ * @see <a href="http://go/ble-glossary">BLE Glossary</a>
+ * @see <a href="https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=245130">Bluetooth
+ * Data Types Specification</a>
+ */
+public abstract class BeaconDecoder {
+ /**
+ * Returns true if the bleRecord corresponds to a beacon format that contains sufficient
+ * information to construct a BeaconId and contains the Tx power.
+ */
+ public boolean supportsBeaconIdAndTxPower(@SuppressWarnings("unused") BleRecord bleRecord) {
+ return true;
+ }
+
+ /**
+ * Returns true if this decoder supports returning TxPower via {@link
+ * #getCalibratedBeaconTxPower(BleRecord)}.
+ */
+ public boolean supportsTxPower() {
+ return true;
+ }
+
+ /**
+ * Reads the calibrated transmitted power at 1 meter of the beacon in dBm. This value is
+ * contained
+ * in the scan record, as set by the transmitting beacon. Suitable for use in computing path
+ * loss,
+ * distance, and related derived values.
+ *
+ * @param bleRecord the parsed payload contained in the beacon packet
+ * @return integer value of the calibrated Tx power in dBm or null if the bleRecord doesn't
+ * contain sufficient information to calculate the Tx power.
+ */
+ @Nullable
+ public abstract Integer getCalibratedBeaconTxPower(BleRecord bleRecord);
+
+ /**
+ * Extract telemetry information from the beacon. Byte 0 of the returned telemetry block should
+ * encode the telemetry format.
+ *
+ * @return telemetry block for this beacon, or null if no telemetry data is found in the scan
+ * record.
+ */
+ @Nullable
+ public byte[] getTelemetry(@SuppressWarnings("unused") BleRecord bleRecord) {
+ return null;
+ }
+
+ /** Returns the appropriate type for this scan record. */
+ public abstract int getBeaconIdType();
+
+ /**
+ * Returns an array of bytes which uniquely identify this beacon, for beacons from any of the
+ * supported beacon types. This unique identifier is the indexing key for various internal
+ * services. Returns null if the bleRecord doesn't contain sufficient information to construct
+ * the
+ * ID.
+ */
+ @Nullable
+ public abstract byte[] getBeaconIdBytes(BleRecord bleRecord);
+
+ /**
+ * Returns the URL of the beacon. Returns null if the bleRecord doesn't contain a URL or
+ * contains
+ * a malformed URL.
+ */
+ @Nullable
+ public String getUrl(BleRecord bleRecord) {
+ return null;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/decode/FastPairDecoder.java b/nearby/service/java/com/android/server/nearby/common/ble/decode/FastPairDecoder.java
new file mode 100644
index 0000000..c1ff9fd
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/ble/decode/FastPairDecoder.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble.decode;
+
+import android.bluetooth.le.ScanRecord;
+import android.os.ParcelUuid;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.Nullable;
+
+import com.android.server.nearby.common.ble.BleFilter;
+import com.android.server.nearby.common.ble.BleRecord;
+
+import java.util.Arrays;
+
+/**
+ * Parses Fast Pair information out of {@link BleRecord}s.
+ *
+ * <p>There are 2 different packet formats that are supported, which is used can be determined by
+ * packet length:
+ *
+ * <p>For 3-byte packets, the full packet is the model ID.
+ *
+ * <p>For all other packets, the first byte is the header, followed by the model ID, followed by
+ * zero or more extra fields. Each field has its own header byte followed by the field value. The
+ * packet header is formatted as 0bVVVLLLLR (V = version, L = model ID length, R = reserved) and
+ * each extra field header is 0bLLLLTTTT (L = field length, T = field type).
+ *
+ * @see <a href="http://go/fast-pair-2-service-data">go/fast-pair-2-service-data</a>
+ */
+public class FastPairDecoder extends BeaconDecoder {
+
+ private static final int FIELD_TYPE_BLOOM_FILTER = 0;
+ private static final int FIELD_TYPE_BLOOM_FILTER_SALT = 1;
+ private static final int FIELD_TYPE_BLOOM_FILTER_NO_NOTIFICATION = 2;
+ private static final int FIELD_TYPE_BATTERY = 3;
+ private static final int FIELD_TYPE_BATTERY_NO_NOTIFICATION = 4;
+ public static final int FIELD_TYPE_CONNECTION_STATE = 5;
+ private static final int FIELD_TYPE_RANDOM_RESOLVABLE_DATA = 6;
+
+ /** FE2C is the 16-bit Service UUID. The rest is the base UUID. See BluetoothUuid (hidden). */
+ private static final ParcelUuid FAST_PAIR_SERVICE_PARCEL_UUID =
+ ParcelUuid.fromString("0000FE2C-0000-1000-8000-00805F9B34FB");
+
+ /** The filter you use to scan for Fast Pair BLE advertisements. */
+ public static final BleFilter FILTER =
+ new BleFilter.Builder().setServiceData(FAST_PAIR_SERVICE_PARCEL_UUID,
+ new byte[0]).build();
+
+ // NOTE: Ensure that all bitmasks are always ints, not bytes so that bitshifting works correctly
+ // without needing worry about signing errors.
+ private static final int HEADER_VERSION_BITMASK = 0b11100000;
+ private static final int HEADER_LENGTH_BITMASK = 0b00011110;
+ private static final int HEADER_VERSION_OFFSET = 5;
+ private static final int HEADER_LENGTH_OFFSET = 1;
+
+ private static final int EXTRA_FIELD_LENGTH_BITMASK = 0b11110000;
+ private static final int EXTRA_FIELD_TYPE_BITMASK = 0b00001111;
+ private static final int EXTRA_FIELD_LENGTH_OFFSET = 4;
+ private static final int EXTRA_FIELD_TYPE_OFFSET = 0;
+
+ private static final int MIN_ID_LENGTH = 3;
+ private static final int MAX_ID_LENGTH = 14;
+ private static final int HEADER_INDEX = 0;
+ private static final int HEADER_LENGTH = 1;
+ private static final int FIELD_HEADER_LENGTH = 1;
+
+ // Not using java.util.IllegalFormatException because it is unchecked.
+ private static class IllegalFormatException extends Exception {
+ private IllegalFormatException(String message) {
+ super(message);
+ }
+ }
+
+ @Nullable
+ @Override
+ public Integer getCalibratedBeaconTxPower(BleRecord bleRecord) {
+ return null;
+ }
+
+ // TODO(b/205320613) create beacon type
+ @Override
+ public int getBeaconIdType() {
+ return 1;
+ }
+
+ /** Returns the Model ID from our service data, if present. */
+ @Nullable
+ @Override
+ public byte[] getBeaconIdBytes(BleRecord bleRecord) {
+ return getModelId(bleRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID));
+ }
+
+ /** Returns the Model ID from our service data, if present. */
+ @Nullable
+ public static byte[] getModelId(@Nullable byte[] serviceData) {
+ if (serviceData == null) {
+ return null;
+ }
+
+ if (serviceData.length >= MIN_ID_LENGTH) {
+ if (serviceData.length == MIN_ID_LENGTH) {
+ // If the length == 3, all bytes are the ID. See flag docs for more about
+ // endianness.
+ return serviceData;
+ } else {
+ // Otherwise, the first byte is a header which contains the length of the
+ // big-endian model
+ // ID that follows. The model ID will be trimmed if it contains leading zeros.
+ int idIndex = 1;
+ int end = idIndex + getIdLength(serviceData);
+ while (serviceData[idIndex] == 0 && end - idIndex > MIN_ID_LENGTH) {
+ idIndex++;
+ }
+ return Arrays.copyOfRange(serviceData, idIndex, end);
+ }
+ }
+ return null;
+ }
+
+ /** Gets the FastPair service data array if available, otherwise returns null. */
+ @Nullable
+ public static byte[] getServiceDataArray(BleRecord bleRecord) {
+ return bleRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
+ }
+
+ /** Gets the FastPair service data array if available, otherwise returns null. */
+ @Nullable
+ public static byte[] getServiceDataArray(ScanRecord scanRecord) {
+ return scanRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
+ }
+
+ /** Gets the bloom filter from the extra fields if available, otherwise returns null. */
+ @Nullable
+ public static byte[] getBloomFilter(@Nullable byte[] serviceData) {
+ return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER);
+ }
+
+ /** Gets the bloom filter salt from the extra fields if available, otherwise returns null. */
+ @Nullable
+ public static byte[] getBloomFilterSalt(byte[] serviceData) {
+ return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER_SALT);
+ }
+
+ /**
+ * Gets the suppress notification with bloom filter from the extra fields if available,
+ * otherwise
+ * returns null.
+ */
+ @Nullable
+ public static byte[] getBloomFilterNoNotification(@Nullable byte[] serviceData) {
+ return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER_NO_NOTIFICATION);
+ }
+
+ /** Gets the battery level from extra fields if available, otherwise return null. */
+ @Nullable
+ public static byte[] getBatteryLevel(byte[] serviceData) {
+ return getExtraField(serviceData, FIELD_TYPE_BATTERY);
+ }
+
+ /**
+ * Gets the suppress notification with battery level from extra fields if available, otherwise
+ * return null.
+ */
+ @Nullable
+ public static byte[] getBatteryLevelNoNotification(byte[] serviceData) {
+ return getExtraField(serviceData, FIELD_TYPE_BATTERY_NO_NOTIFICATION);
+ }
+
+ /**
+ * Gets the random resolvable data from extra fields if available, otherwise
+ * return null.
+ */
+ @Nullable
+ public static byte[] getRandomResolvableData(byte[] serviceData) {
+ return getExtraField(serviceData, FIELD_TYPE_RANDOM_RESOLVABLE_DATA);
+ }
+
+ @Nullable
+ private static byte[] getExtraField(@Nullable byte[] serviceData, int fieldId) {
+ if (serviceData == null || serviceData.length < HEADER_INDEX + HEADER_LENGTH) {
+ return null;
+ }
+ try {
+ return getExtraFields(serviceData).get(fieldId);
+ } catch (IllegalFormatException e) {
+ Log.v("FastPairDecode", "Extra fields incorrectly formatted.");
+ return null;
+ }
+ }
+
+ /** Gets extra field data at the end of the packet, defined by the extra field header. */
+ private static SparseArray<byte[]> getExtraFields(byte[] serviceData)
+ throws IllegalFormatException {
+ SparseArray<byte[]> extraFields = new SparseArray<>();
+ if (getVersion(serviceData) != 0) {
+ return extraFields;
+ }
+ int headerIndex = getFirstExtraFieldHeaderIndex(serviceData);
+ while (headerIndex < serviceData.length) {
+ int length = getExtraFieldLength(serviceData, headerIndex);
+ int index = headerIndex + FIELD_HEADER_LENGTH;
+ int type = getExtraFieldType(serviceData, headerIndex);
+ int end = index + length;
+ if (extraFields.get(type) == null) {
+ if (end <= serviceData.length) {
+ extraFields.put(type, Arrays.copyOfRange(serviceData, index, end));
+ } else {
+ throw new IllegalFormatException(
+ "Invalid length, " + end + " is longer than service data size "
+ + serviceData.length);
+ }
+ }
+ headerIndex = end;
+ }
+ return extraFields;
+ }
+
+ /** Checks whether or not a valid ID is included in the service data packet. */
+ public static boolean hasBeaconIdBytes(BleRecord bleRecord) {
+ byte[] serviceData = bleRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
+ return checkModelId(serviceData);
+ }
+
+ /** Check whether byte array is FastPair model id or not. */
+ public static boolean checkModelId(@Nullable byte[] scanResult) {
+ return scanResult != null
+ // The 3-byte format has no header byte (all bytes are the ID).
+ && (scanResult.length == MIN_ID_LENGTH
+ // Header byte exists. We support only format version 0. (A different version
+ // indicates
+ // a breaking change in the format.)
+ || (scanResult.length > MIN_ID_LENGTH
+ && getVersion(scanResult) == 0
+ && isIdLengthValid(scanResult)));
+ }
+
+ /** Checks whether or not bloom filter is included in the service data packet. */
+ public static boolean hasBloomFilter(BleRecord bleRecord) {
+ return (getBloomFilter(getServiceDataArray(bleRecord)) != null
+ || getBloomFilterNoNotification(getServiceDataArray(bleRecord)) != null);
+ }
+
+ /** Checks whether or not bloom filter is included in the service data packet. */
+ public static boolean hasBloomFilter(ScanRecord scanRecord) {
+ return (getBloomFilter(getServiceDataArray(scanRecord)) != null
+ || getBloomFilterNoNotification(getServiceDataArray(scanRecord)) != null);
+ }
+
+ private static int getVersion(byte[] serviceData) {
+ return serviceData.length == MIN_ID_LENGTH
+ ? 0
+ : (serviceData[HEADER_INDEX] & HEADER_VERSION_BITMASK) >> HEADER_VERSION_OFFSET;
+ }
+
+ private static int getIdLength(byte[] serviceData) {
+ return serviceData.length == MIN_ID_LENGTH
+ ? MIN_ID_LENGTH
+ : (serviceData[HEADER_INDEX] & HEADER_LENGTH_BITMASK) >> HEADER_LENGTH_OFFSET;
+ }
+
+ private static int getFirstExtraFieldHeaderIndex(byte[] serviceData) {
+ return HEADER_INDEX + HEADER_LENGTH + getIdLength(serviceData);
+ }
+
+ private static int getExtraFieldLength(byte[] serviceData, int extraFieldIndex) {
+ return (serviceData[extraFieldIndex] & EXTRA_FIELD_LENGTH_BITMASK)
+ >> EXTRA_FIELD_LENGTH_OFFSET;
+ }
+
+ private static int getExtraFieldType(byte[] serviceData, int extraFieldIndex) {
+ return (serviceData[extraFieldIndex] & EXTRA_FIELD_TYPE_BITMASK) >> EXTRA_FIELD_TYPE_OFFSET;
+ }
+
+ private static boolean isIdLengthValid(byte[] serviceData) {
+ int idLength = getIdLength(serviceData);
+ return MIN_ID_LENGTH <= idLength
+ && idLength <= MAX_ID_LENGTH
+ && idLength + HEADER_LENGTH <= serviceData.length;
+ }
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/testing/FastPairTestData.java b/nearby/service/java/com/android/server/nearby/common/ble/testing/FastPairTestData.java
new file mode 100644
index 0000000..f27899f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/ble/testing/FastPairTestData.java
@@ -0,0 +1,141 @@
+/*
+ * 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.ble.testing;
+
+import com.android.server.nearby.util.ArrayUtils;
+import com.android.server.nearby.util.Hex;
+
+/**
+ * Test class to provide example to unit test.
+ */
+public class FastPairTestData {
+ private static final byte[] FAST_PAIR_RECORD_BIG_ENDIAN =
+ Hex.stringToBytes("02011E020AF006162CFEAABBCC");
+
+ /**
+ * A Fast Pair frame, Note: The service UUID is FE2C, but in the
+ * packet it's 2CFE, since the core Bluetooth data types are little-endian.
+ *
+ * <p>However, the model ID is big-endian (multi-byte values in our spec are now big-endian, aka
+ * network byte order).
+ *
+ * @see {http://go/fast-pair-service-data}
+ */
+ public static byte[] getFastPairRecord() {
+ return FAST_PAIR_RECORD_BIG_ENDIAN;
+ }
+
+ /** A Fast Pair frame, with a shared account key. */
+ public static final byte[] FAST_PAIR_SHARED_ACCOUNT_KEY_RECORD =
+ Hex.stringToBytes("02011E020AF00C162CFE007011223344556677");
+
+ /** Model ID in {@link #getFastPairRecord()}. */
+ public static final byte[] FAST_PAIR_MODEL_ID = Hex.stringToBytes("AABBCC");
+
+ /** @see #getFastPairRecord() */
+ public static byte[] newFastPairRecord(byte header, byte[] modelId) {
+ return newFastPairRecord(
+ modelId.length == 3 ? modelId : ArrayUtils.concatByteArrays(new byte[] {header},
+ modelId));
+ }
+
+ /** @see #getFastPairRecord() */
+ public static byte[] newFastPairRecord(byte[] serviceData) {
+ int length = /* length of type and service UUID = */ 3 + serviceData.length;
+ return Hex.stringToBytes(
+ String.format("02011E020AF0%02X162CFE%s", length,
+ Hex.bytesToStringUppercase(serviceData)));
+ }
+
+ // This is an example of advertising data with AD types
+ public static byte[] adv_1 = {
+ 0x02, // Length of this Data
+ 0x01, // <<Flags>>
+ 0x01, // LE Limited Discoverable Mode
+ 0x0A, // Length of this Data
+ 0x09, // <<Complete local name>>
+ 'P', 'e', 'd', 'o', 'm', 'e', 't', 'e', 'r'
+ };
+
+ // This is an example of advertising data with positive TX Power
+ // Level.
+ public static byte[] adv_2 = {
+ 0x02, // Length of this Data
+ 0x0a, // <<TX Power Level>>
+ 127 // Level = 127
+ };
+
+ // Example data including a service data block
+ public static byte[] sd1 = {
+ 0x02, // Length of this Data
+ 0x01, // <<Flags>>
+ 0x04, // BR/EDR Not Supported.
+ 0x03, // Length of this Data
+ 0x02, // <<Incomplete List of 16-bit Service UUIDs>>
+ 0x04,
+ 0x18, // TX Power Service UUID
+ 0x1e, // Length of this Data
+ (byte) 0x16, // <<Service Specific Data>>
+ // Service UUID
+ (byte) 0xe0,
+ 0x00,
+ // gBeacon Header
+ 0x15,
+ // Running time ENCRYPT
+ (byte) 0xd2,
+ 0x77,
+ 0x01,
+ 0x00,
+ // Scan Freq ENCRYPT
+ 0x32,
+ 0x05,
+ // Time in slow mode
+ 0x00,
+ 0x00,
+ // Time in fast mode
+ 0x7f,
+ 0x17,
+ // Subset of UID
+ 0x56,
+ 0x00,
+ // ID Mask
+ (byte) 0xd4,
+ 0x7c,
+ 0x18,
+ // RFU (reserved)
+ 0x00,
+ // GUID = decimal 1297482358
+ 0x76,
+ 0x02,
+ 0x56,
+ 0x4d,
+ 0x00,
+ // Ranging Payload Header
+ 0x24,
+ // MAC of scanning address
+ (byte) 0xa4,
+ (byte) 0xbb,
+ // NORM RX RSSI -67dBm
+ (byte) 0xb0,
+ // NORM TX POWER -77dBm, so actual TX POWER = -36dBm
+ (byte) 0xb3,
+ // Note based on the values aboves PATH LOSS = (-36) - (-67) = 31dBm
+ // Below zero padding added to test it is handled correctly
+ 0x00
+ };
+
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/util/RangingUtils.java b/nearby/service/java/com/android/server/nearby/common/ble/util/RangingUtils.java
new file mode 100644
index 0000000..eec52ad
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/ble/util/RangingUtils.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble.util;
+
+
+/**
+ * Ranging utilities embody the physics of converting RF path loss to distance. The free space path
+ * loss is proportional to the square of the distance from transmitter to receiver, and to the
+ * square of the frequency of the propagation signal.
+ */
+public final class RangingUtils {
+ private static final int MAX_RSSI_VALUE = 126;
+ private static final int MIN_RSSI_VALUE = -127;
+
+ private RangingUtils() {
+ }
+
+ /* This was original derived in {@link com.google.android.gms.beacon.util.RangingUtils} from
+ * <a href="http://en.wikipedia.org/wiki/Free-space_path_loss">Free-space_path_loss</a>.
+ * Duplicated here for easy reference.
+ *
+ * c = speed of light (2.9979 x 10^8 m/s);
+ * f = frequency (Bluetooth center frequency is 2.44175GHz = 2.44175x10^9 Hz);
+ * l = wavelength (in meters);
+ * d = distance (from transmitter to receiver in meters);
+ * dB = decibels
+ * dBm = decibel milliwatts
+ *
+ *
+ * Free-space path loss (FSPL) is proportional to the square of the distance between the
+ * transmitter and the receiver, and also proportional to the square of the frequency of the
+ * radio signal.
+ *
+ * FSPL = (4 * pi * d / l)^2 = (4 * pi * d * f / c)^2
+ *
+ * FSPL (dB) = 10 * log10((4 * pi * d * f / c)^2)
+ * = 20 * log10(4 * pi * d * f / c)
+ * = (20 * log10(d)) + (20 * log10(f)) + (20 * log10(4 * pi/c))
+ *
+ * Calculating constants:
+ *
+ * FSPL_FREQ = 20 * log10(f)
+ * = 20 * log10(2.44175 * 10^9)
+ * = 187.75
+ *
+ * FSPL_LIGHT = 20 * log10(4 * pi/c)
+ * = 20 * log10(4 * pi/(2.9979 * 10^8))
+ * = 20 * log10(4 * pi/(2.9979 * 10^8))
+ * = 20 * log10(41.9172441s * 10^-9)
+ * = -147.55
+ *
+ * FSPL_DISTANCE_1M = 20 * log10(1)
+ * = 0
+ *
+ * PATH_LOSS_AT_1M = FSPL_DISTANCE_1M + FSPL_FREQ + FSPL_LIGHT
+ * = 0 + 187.75 + (-147.55)
+ * = 40.20db [round to 41db]
+ *
+ * Note: Rounding up makes us "closer" and makes us more aggressive at showing notifications.
+ */
+ private static final int RSSI_DROP_OFF_AT_1_M = 41;
+
+ /**
+ * Convert target distance and txPower to a RSSI value using the Log-distance path loss model
+ * with Path Loss at 1m of 41db.
+ *
+ * @return RSSI expected at distanceInMeters with device broadcasting at txPower.
+ */
+ public static int rssiFromTargetDistance(double distanceInMeters, int txPower) {
+ /*
+ * See <a href="https://en.wikipedia.org/wiki/Log-distance_path_loss_model">
+ * Log-distance path loss model</a>.
+ *
+ * PL = total path loss in db
+ * txPower = TxPower in dbm
+ * rssi = Received signal strength in dbm
+ * PL_0 = Path loss at reference distance d_0 {@link RSSI_DROP_OFF_AT_1_M} dbm
+ * d = length of path
+ * d_0 = reference distance (1 m)
+ * gamma = path loss exponent (2 in free space)
+ *
+ * Log-distance path loss (LDPL) formula:
+ *
+ * PL = txPower - rssi = PL_0 + 10 * gamma * log_10(d / d_0)
+ * txPower - rssi = RSSI_DROP_OFF_AT_1_M + 10 * 2 * log_10
+ * (distanceInMeters / 1)
+ * - rssi = -txPower + RSSI_DROP_OFF_AT_1_M + 20 * log_10(distanceInMeters)
+ * rssi = txPower - RSSI_DROP_OFF_AT_1_M - 20 * log_10(distanceInMeters)
+ */
+ txPower = adjustPower(txPower);
+ return distanceInMeters == 0
+ ? txPower
+ : (int) Math.floor((txPower - RSSI_DROP_OFF_AT_1_M)
+ - 20 * Math.log10(distanceInMeters));
+ }
+
+ /**
+ * Convert RSSI and txPower to a distance value using the Log-distance path loss model with Path
+ * Loss at 1m of 41db.
+ *
+ * @return distance in meters with device broadcasting at txPower and given RSSI.
+ */
+ public static double distanceFromRssiAndTxPower(int rssi, int txPower) {
+ /*
+ * See <a href="https://en.wikipedia.org/wiki/Log-distance_path_loss_model">Log-distance
+ * path
+ * loss model</a>.
+ *
+ * PL = total path loss in db
+ * txPower = TxPower in dbm
+ * rssi = Received signal strength in dbm
+ * PL_0 = Path loss at reference distance d_0 {@link RSSI_DROP_OFF_AT_1_M} dbm
+ * d = length of path
+ * d_0 = reference distance (1 m)
+ * gamma = path loss exponent (2 in free space)
+ *
+ * Log-distance path loss (LDPL) formula:
+ *
+ * PL = txPower - rssi = PL_0 + 10 * gamma * log_10(d /
+ * d_0)
+ * txPower - rssi = RSSI_DROP_OFF_AT_1_M + 10 * gamma * log_10(d /
+ * d_0)
+ * txPower - rssi - RSSI_DROP_OFF_AT_1_M = 10 * 2 * log_10
+ * (distanceInMeters / 1)
+ * txPower - rssi - RSSI_DROP_OFF_AT_1_M = 20 * log_10(distanceInMeters / 1)
+ * (txPower - rssi - RSSI_DROP_OFF_AT_1_M) / 20 = log_10(distanceInMeters)
+ * 10 ^ ((txPower - rssi - RSSI_DROP_OFF_AT_1_M) / 20) = distanceInMeters
+ */
+ txPower = adjustPower(txPower);
+ rssi = adjustPower(rssi);
+ return Math.pow(10, (txPower - rssi - RSSI_DROP_OFF_AT_1_M) / 20.0);
+ }
+
+ /**
+ * Prevents the power from becoming too large or too small.
+ */
+ private static int adjustPower(int power) {
+ if (power > MAX_RSSI_VALUE) {
+ return MAX_RSSI_VALUE;
+ }
+ if (power < MIN_RSSI_VALUE) {
+ return MIN_RSSI_VALUE;
+ }
+ return power;
+ }
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/util/StringUtils.java b/nearby/service/java/com/android/server/nearby/common/ble/util/StringUtils.java
new file mode 100644
index 0000000..4d90b6d
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/ble/util/StringUtils.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble.util;
+
+import android.annotation.Nullable;
+import android.util.SparseArray;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+
+/** Helper class for Bluetooth LE utils. */
+public final class StringUtils {
+ private StringUtils() {
+ }
+
+ /** Returns a string composed from a {@link SparseArray}. */
+ public static String toString(@Nullable SparseArray<byte[]> array) {
+ if (array == null) {
+ return "null";
+ }
+ if (array.size() == 0) {
+ return "{}";
+ }
+ StringBuilder buffer = new StringBuilder();
+ buffer.append('{');
+ for (int i = 0; i < array.size(); ++i) {
+ buffer.append(array.keyAt(i)).append("=").append(Arrays.toString(array.valueAt(i)));
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+
+ /** Returns a string composed from a {@link Map}. */
+ public static <T> String toString(@Nullable Map<T, byte[]> map) {
+ if (map == null) {
+ return "null";
+ }
+ if (map.isEmpty()) {
+ return "{}";
+ }
+ StringBuilder buffer = new StringBuilder();
+ buffer.append('{');
+ Iterator<Map.Entry<T, byte[]>> it = map.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<T, byte[]> entry = it.next();
+ Object key = entry.getKey();
+ buffer.append(key).append("=").append(Arrays.toString(map.get(key)));
+ if (it.hasNext()) {
+ buffer.append(", ");
+ }
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bloomfilter/BloomFilter.java b/nearby/service/java/com/android/server/nearby/common/bloomfilter/BloomFilter.java
new file mode 100644
index 0000000..6d4275f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bloomfilter/BloomFilter.java
@@ -0,0 +1,108 @@
+/*
+ * 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.bloomfilter;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.primitives.UnsignedInts;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.BitSet;
+
+/**
+ * A bloom filter that gives access to the underlying BitSet.
+ */
+public class BloomFilter {
+ private static final Charset CHARSET = UTF_8;
+
+ /**
+ * Receives a value and converts it into an array of ints that will be converted to indexes for
+ * the filter.
+ */
+ public interface Hasher {
+ /**
+ * Generate hash value.
+ */
+ int[] getHashes(byte[] value);
+ }
+
+ // The backing data for this bloom filter. As additions are made, they're OR'd until it
+ // eventually reaches 0xFF.
+ private final BitSet mBits;
+ // The max length of bits.
+ private final int mBitLength;
+ // The hasher to use for converting a value into an array of hashes.
+ private final Hasher mHasher;
+
+ public BloomFilter(byte[] bytes, Hasher hasher) {
+ this.mBits = BitSet.valueOf(bytes);
+ this.mBitLength = bytes.length * 8;
+ this.mHasher = hasher;
+ }
+
+ /**
+ * Return the bloom filter check bit set as byte array.
+ */
+ public byte[] asBytes() {
+ // BitSet.toByteArray() truncates all the unset bits after the last set bit (eg. [0,0,1,0]
+ // becomes [0,0,1]) so we re-add those bytes if needed with Arrays.copy().
+ byte[] b = mBits.toByteArray();
+ if (b.length == mBitLength / 8) {
+ return b;
+ }
+ return Arrays.copyOf(b, mBitLength / 8);
+ }
+
+ /**
+ * Add string value to bloom filter hash.
+ */
+ public void add(String s) {
+ add(s.getBytes(CHARSET));
+ }
+
+ /**
+ * Adds value to bloom filter hash.
+ */
+ public void add(byte[] value) {
+ int[] hashes = mHasher.getHashes(value);
+ for (int hash : hashes) {
+ mBits.set(UnsignedInts.remainder(hash, mBitLength));
+ }
+ }
+
+ /**
+ * Check if the string format has collision.
+ */
+ public boolean possiblyContains(String s) {
+ return possiblyContains(s.getBytes(CHARSET));
+ }
+
+ /**
+ * Checks if value after hash will have collision.
+ */
+ public boolean possiblyContains(byte[] value) {
+ int[] hashes = mHasher.getHashes(value);
+ for (int hash : hashes) {
+ if (!mBits.get(UnsignedInts.remainder(hash, mBitLength))) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasher.java b/nearby/service/java/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasher.java
new file mode 100644
index 0000000..0ccee97
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasher.java
@@ -0,0 +1,41 @@
+/*
+ * 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.bloomfilter;
+
+import com.google.common.hash.Hashing;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Hasher which hashes a value using SHA-256 and splits it into parts, each of which can be
+ * converted to an index.
+ */
+public class FastPairBloomFilterHasher implements BloomFilter.Hasher {
+
+ private static final int NUM_INDEXES = 8;
+
+ @Override
+ public int[] getHashes(byte[] value) {
+ byte[] hash = Hashing.sha256().hashBytes(value).asBytes();
+ ByteBuffer buffer = ByteBuffer.wrap(hash);
+ int[] hashes = new int[NUM_INDEXES];
+ for (int i = 0; i < NUM_INDEXES; i++) {
+ hashes[i] = buffer.getInt();
+ }
+ return hashes;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothConsts.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothConsts.java
new file mode 100644
index 0000000..3a02b18
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothConsts.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth;
+
+import java.util.UUID;
+
+/**
+ * Bluetooth constants.
+ */
+public class BluetoothConsts {
+
+ /**
+ * Default MTU when value is unknown.
+ */
+ public static final int DEFAULT_MTU = 23;
+
+ // The following random uuids are used to indicate that the device has dynamic services.
+ /**
+ * UUID of dynamic service.
+ */
+ public static final UUID SERVICE_DYNAMIC_SERVICE =
+ UUID.fromString("00000100-0af3-11e5-a6c0-1697f925ec7b");
+
+ /**
+ * UUID of dynamic characteristic.
+ */
+ public static final UUID SERVICE_DYNAMIC_CHARACTERISTIC =
+ UUID.fromString("00002A05-0af3-11e5-a6c0-1697f925ec7b");
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothException.java
new file mode 100644
index 0000000..db2e1cc
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth;
+
+/**
+ * {@link Exception} thrown during a Bluetooth operation.
+ */
+public class BluetoothException extends Exception {
+ /** Constructor. */
+ public BluetoothException(String message) {
+ super(message);
+ }
+
+ /** Constructor. */
+ public BluetoothException(String message, Throwable throwable) {
+ super(message, throwable);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothGattException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothGattException.java
new file mode 100644
index 0000000..5ac4882
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothGattException.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth;
+
+/**
+ * Exception for Bluetooth GATT operations.
+ */
+public class BluetoothGattException extends BluetoothException {
+ private final int mErrorCode;
+
+ /** Constructor. */
+ public BluetoothGattException(String message, int errorCode) {
+ super(message);
+ mErrorCode = errorCode;
+ }
+
+ /** Constructor. */
+ public BluetoothGattException(String message, int errorCode, Throwable cause) {
+ super(message, cause);
+ mErrorCode = errorCode;
+ }
+
+ /** Returns Gatt error code. */
+ public int getGattErrorCode() {
+ return mErrorCode;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothTimeoutException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothTimeoutException.java
new file mode 100644
index 0000000..30fd188
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/BluetoothTimeoutException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth;
+
+/**
+ * {@link Exception} thrown during a Bluetooth operation when a timeout occurs.
+ */
+public class BluetoothTimeoutException extends BluetoothException {
+
+ /** Constructor. */
+ public BluetoothTimeoutException(String message) {
+ super(message);
+ }
+
+ /** Constructor. */
+ public BluetoothTimeoutException(String message, Throwable throwable) {
+ super(message, throwable);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/ReservedUuids.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/ReservedUuids.java
new file mode 100644
index 0000000..249011a
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/ReservedUuids.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth;
+
+import java.util.UUID;
+
+/**
+ * Reserved UUIDS by BT SIG.
+ * <p>
+ * See https://developer.bluetooth.org for more details.
+ */
+public class ReservedUuids {
+ /** UUIDs reserved for services. */
+ public static class Services {
+ /**
+ * The Device Information Service exposes manufacturer and/or vendor info about a device.
+ * <p>
+ * See reserved UUID org.bluetooth.service.device_information.
+ */
+ public static final UUID DEVICE_INFORMATION = fromShortUuid((short) 0x180A);
+
+ /**
+ * Generic attribute service.
+ * <p>
+ * See reserved UUID org.bluetooth.service.generic_attribute.
+ */
+ public static final UUID GENERIC_ATTRIBUTE = fromShortUuid((short) 0x1801);
+ }
+
+ /** UUIDs reserved for characteristics. */
+ public static class Characteristics {
+ /**
+ * The value of this characteristic is a UTF-8 string representing the firmware revision for
+ * the firmware within the device.
+ * <p>
+ * See reserved UUID org.bluetooth.characteristic.firmware_revision_string.
+ */
+ public static final UUID FIRMWARE_REVISION_STRING = fromShortUuid((short) 0x2A26);
+
+ /**
+ * Service change characteristic.
+ * <p>
+ * See reserved UUID org.bluetooth.characteristic.gatt.service_changed.
+ */
+ public static final UUID SERVICE_CHANGE = fromShortUuid((short) 0x2A05);
+ }
+
+ /** UUIDs reserved for descriptors. */
+ public static class Descriptors {
+ /**
+ * This descriptor shall be persistent across connections for bonded devices. The Client
+ * Characteristic Configuration descriptor is unique for each client. A client may read and
+ * write this descriptor to determine and set the configuration for that client.
+ * Authentication and authorization may be required by the server to write this descriptor.
+ * The default value for the Client Characteristic Configuration descriptor is 0x00. Upon
+ * connection of non-binded clients, this descriptor is set to the default value.
+ * <p>
+ * See reserved UUID org.bluetooth.descriptor.gatt.client_characteristic_configuration.
+ */
+ public static final UUID CLIENT_CHARACTERISTIC_CONFIGURATION =
+ fromShortUuid((short) 0x2902);
+ }
+
+ /** The base 128-bit UUID representation of a 16-bit UUID */
+ public static final UUID BASE_16_BIT_UUID =
+ UUID.fromString("00000000-0000-1000-8000-00805F9B34FB");
+
+ /** Converts from short UUId to UUID. */
+ public static UUID fromShortUuid(short shortUuid) {
+ return new UUID(((((long) shortUuid) << 32) & 0x0000FFFF00000000L)
+ | ReservedUuids.BASE_16_BIT_UUID.getMostSignificantBits(),
+ ReservedUuids.BASE_16_BIT_UUID.getLeastSignificantBits());
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGenerator.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGenerator.java
new file mode 100644
index 0000000..28a9c33
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGenerator.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.generateKey;
+
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AccountKeyCharacteristic;
+
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * This is to generate account key with fast-pair style.
+ */
+public final class AccountKeyGenerator {
+
+ // Generate a key where the first byte is always defined as the type, 0x04. This maintains 15
+ // bytes of entropy in the key while also allowing providers to verify that they have received
+ // a properly formatted key and decrypted it correctly, minimizing the risk of replay attacks.
+
+ /**
+ * Creates account key.
+ */
+ public static byte[] createAccountKey() throws NoSuchAlgorithmException {
+ byte[] accountKey = generateKey();
+ accountKey[0] = AccountKeyCharacteristic.TYPE;
+ return accountKey;
+ }
+
+ private AccountKeyGenerator() {
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AdditionalDataEncoder.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AdditionalDataEncoder.java
new file mode 100644
index 0000000..c9ccfd5
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AdditionalDataEncoder.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.NONCE_SIZE;
+
+import static com.google.common.primitives.Bytes.concat;
+
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+/**
+ * Utilities for encoding/decoding the additional data packet and verifying both the data integrity
+ * and the authentication.
+ *
+ * <p>Additional Data packet is:
+ *
+ * <ol>
+ * <li>AdditionalData_Packet[0 - 7]: the first 8-byte of HMAC.
+ * <li>AdditionalData_Packet[8 - var]: the encrypted message by AES-CTR, with 8-byte nonce
+ * appended to the front.
+ * </ol>
+ *
+ * See https://developers.google.com/nearby/fast-pair/spec#AdditionalData.
+ */
+public final class AdditionalDataEncoder {
+
+ static final int EXTRACT_HMAC_SIZE = 8;
+ static final int MAX_LENGTH_OF_DATA = 64;
+
+ /**
+ * Encodes the given data to additional data packet by the given secret.
+ */
+ static byte[] encodeAdditionalDataPacket(byte[] secret, byte[] additionalData)
+ throws GeneralSecurityException {
+ if (secret == null || secret.length != AesCtrMultipleBlockEncryption.KEY_LENGTH) {
+ throw new GeneralSecurityException(
+ "Incorrect secret for encoding additional data packet, secret.length = "
+ + (secret == null ? "NULL" : secret.length));
+ }
+
+ if ((additionalData == null)
+ || (additionalData.length == 0)
+ || (additionalData.length > MAX_LENGTH_OF_DATA)) {
+ throw new GeneralSecurityException(
+ "Invalid data for encoding additional data packet, data = "
+ + (additionalData == null ? "NULL" : additionalData.length));
+ }
+
+ byte[] encryptedData = AesCtrMultipleBlockEncryption.encrypt(secret, additionalData);
+ byte[] extractedHmac =
+ Arrays.copyOf(HmacSha256.build(secret, encryptedData), EXTRACT_HMAC_SIZE);
+
+ return concat(extractedHmac, encryptedData);
+ }
+
+ /**
+ * Decodes additional data packet by the given secret.
+ *
+ * @param secret AES-128 key used in the encryption to decrypt data
+ * @param additionalDataPacket additional data packet which is encoded by the given secret
+ * @return the data byte array decoded from the given packet
+ * @throws GeneralSecurityException if the given key or additional data packet is invalid for
+ * decoding
+ */
+ static byte[] decodeAdditionalDataPacket(byte[] secret, byte[] additionalDataPacket)
+ throws GeneralSecurityException {
+ if (secret == null || secret.length != AesCtrMultipleBlockEncryption.KEY_LENGTH) {
+ throw new GeneralSecurityException(
+ "Incorrect secret for decoding additional data packet, secret.length = "
+ + (secret == null ? "NULL" : secret.length));
+ }
+ if (additionalDataPacket == null
+ || additionalDataPacket.length <= EXTRACT_HMAC_SIZE
+ || additionalDataPacket.length
+ > (MAX_LENGTH_OF_DATA + EXTRACT_HMAC_SIZE + NONCE_SIZE)) {
+ throw new GeneralSecurityException(
+ "Additional data packet size is incorrect, additionalDataPacket.length is "
+ + (additionalDataPacket == null ? "NULL"
+ : additionalDataPacket.length));
+ }
+
+ if (!verifyHmac(secret, additionalDataPacket)) {
+ throw new GeneralSecurityException(
+ "Verify HMAC failed, could be incorrect key or packet.");
+ }
+ byte[] encryptedData =
+ Arrays.copyOfRange(
+ additionalDataPacket, EXTRACT_HMAC_SIZE, additionalDataPacket.length);
+ return AesCtrMultipleBlockEncryption.decrypt(secret, encryptedData);
+ }
+
+ // Computes the HMAC of the given key and additional data, and compares the first 8-byte of the
+ // HMAC result with the one from additional data packet.
+ // Must call constant-time comparison to prevent a possible timing attack, e.g. time the same
+ // MAC with all different first byte for a given ciphertext, the right one will take longer as
+ // it will fail on the second byte's verification.
+ private static boolean verifyHmac(byte[] key, byte[] additionalDataPacket)
+ throws GeneralSecurityException {
+ byte[] packetHmac =
+ Arrays.copyOfRange(additionalDataPacket, /* from= */ 0, EXTRACT_HMAC_SIZE);
+ byte[] encryptedData =
+ Arrays.copyOfRange(
+ additionalDataPacket, EXTRACT_HMAC_SIZE, additionalDataPacket.length);
+ byte[] computedHmac = Arrays.copyOf(
+ HmacSha256.build(key, encryptedData), EXTRACT_HMAC_SIZE);
+
+ return HmacSha256.compareTwoHMACs(packetHmac, computedHmac);
+ }
+
+ private AdditionalDataEncoder() {
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryption.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryption.java
new file mode 100644
index 0000000..50a818b
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryption.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.primitives.Bytes.concat;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+/**
+ * AES-CTR utilities used for encrypting and decrypting Fast Pair packets that contain multiple
+ * blocks. Encrypts input data by:
+ *
+ * <ol>
+ * <li>encryptedBlock[i] = clearBlock[i] ^ AES(counter), and
+ * <li>concat(encryptedBlock[0], encryptedBlock[1],...) to create the encrypted result, where
+ * <li>counter: the 16-byte input of AES. counter = iv + block_index.
+ * <li>iv: extend 8-byte nonce to 16 bytes with zero padding. i.e. concat(0x0000000000000000,
+ * nonce).
+ * <li>nonce: the cryptographically random 8 bytes, must never be reused with the same key.
+ * </ol>
+ */
+final class AesCtrMultipleBlockEncryption {
+
+ /** Length for AES-128 key. */
+ static final int KEY_LENGTH = AesEcbSingleBlockEncryption.KEY_LENGTH;
+
+ @VisibleForTesting
+ static final int AES_BLOCK_LENGTH = AesEcbSingleBlockEncryption.AES_BLOCK_LENGTH;
+
+ /** Length of the nonce, a byte array of cryptographically random bytes. */
+ static final int NONCE_SIZE = 8;
+
+ private static final int IV_SIZE = AES_BLOCK_LENGTH;
+ private static final int MAX_NUMBER_OF_BLOCKS = 4;
+
+ private AesCtrMultipleBlockEncryption() {}
+
+ /** Generates a 16-byte AES key. */
+ static byte[] generateKey() throws NoSuchAlgorithmException {
+ return AesEcbSingleBlockEncryption.generateKey();
+ }
+
+ /**
+ * Encrypts data using AES-CTR by the given secret.
+ *
+ * @param secret AES-128 key.
+ * @param data the plaintext to be encrypted.
+ * @return the encrypted data with the 8-byte nonce appended to the front.
+ */
+ static byte[] encrypt(byte[] secret, byte[] data) throws GeneralSecurityException {
+ byte[] nonce = generateNonce();
+ return concat(nonce, doAesCtr(secret, data, nonce));
+ }
+
+ /**
+ * Decrypts data using AES-CTR by the given secret and nonce.
+ *
+ * @param secret AES-128 key.
+ * @param data the first 8 bytes is the nonce, and the remaining is the encrypted data to be
+ * decrypted.
+ * @return the decrypted data.
+ */
+ static byte[] decrypt(byte[] secret, byte[] data) throws GeneralSecurityException {
+ if (data == null || data.length <= NONCE_SIZE) {
+ throw new GeneralSecurityException(
+ "Incorrect data length "
+ + (data == null ? "NULL" : data.length)
+ + " to decrypt, the data should contain nonce.");
+ }
+ byte[] nonce = Arrays.copyOf(data, NONCE_SIZE);
+ byte[] encryptedData = Arrays.copyOfRange(data, NONCE_SIZE, data.length);
+ return doAesCtr(secret, encryptedData, nonce);
+ }
+
+ /**
+ * Generates cryptographically random NONCE_SIZE bytes nonce. This nonce can be used only once.
+ * Always call this function to generate a new nonce before a new encryption.
+ */
+ // Suppression for a warning for potentially insecure random numbers on Android 4.3 and older.
+ // Fast Pair service is only for Android 6.0+ devices.
+ static byte[] generateNonce() {
+ SecureRandom random = new SecureRandom();
+ byte[] nonce = new byte[NONCE_SIZE];
+ random.nextBytes(nonce);
+
+ return nonce;
+ }
+
+ // AES-CTR implementation.
+ @VisibleForTesting
+ static byte[] doAesCtr(byte[] secret, byte[] data, byte[] nonce)
+ throws GeneralSecurityException {
+ if (secret.length != KEY_LENGTH) {
+ throw new IllegalArgumentException(
+ "Incorrect key length for encryption, only supports 16-byte AES Key.");
+ }
+ if (nonce.length != NONCE_SIZE) {
+ throw new IllegalArgumentException(
+ "Incorrect nonce length for encryption, "
+ + "Fast Pair naming scheme only supports 8-byte nonce.");
+ }
+
+ // Keeps the following operations on this byte[], returns it as the final AES-CTR result.
+ byte[] aesCtrResult = new byte[data.length];
+ System.arraycopy(data, /*srcPos=*/ 0, aesCtrResult, /*destPos=*/ 0, data.length);
+
+ // Initializes counter as IV.
+ byte[] counter = createIv(nonce);
+ // The length of the given data is permitted to non-align block size.
+ int numberOfBlocks =
+ (data.length / AES_BLOCK_LENGTH) + ((data.length % AES_BLOCK_LENGTH == 0) ? 0 : 1);
+
+ if (numberOfBlocks > MAX_NUMBER_OF_BLOCKS) {
+ throw new IllegalArgumentException(
+ "Incorrect data size, Fast Pair naming scheme only supports 4 blocks.");
+ }
+
+ for (int i = 0; i < numberOfBlocks; i++) {
+ // Performs the operation: encryptedBlock[i] = clearBlock[i] ^ AES(counter).
+ counter[0] = (byte) (i & 0xFF);
+ byte[] aesOfCounter = doAesSingleBlock(secret, counter);
+ int start = i * AES_BLOCK_LENGTH;
+ // The size of the last block of data may not be 16 bytes. If not, still do xor to the
+ // last byte of data.
+ int end = Math.min(start + AES_BLOCK_LENGTH, data.length);
+ for (int j = 0; start < end; j++, start++) {
+ aesCtrResult[start] ^= aesOfCounter[j];
+ }
+ }
+ return aesCtrResult;
+ }
+
+ private static byte[] doAesSingleBlock(byte[] secret, byte[] counter)
+ throws GeneralSecurityException {
+ return AesEcbSingleBlockEncryption.encrypt(secret, counter);
+ }
+
+ /** Extends 8-byte nonce to 16 bytes with zero padding to create IV. */
+ private static byte[] createIv(byte[] nonce) {
+ return concat(new byte[IV_SIZE - NONCE_SIZE], nonce);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AesEcbSingleBlockEncryption.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AesEcbSingleBlockEncryption.java
new file mode 100644
index 0000000..547931e
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AesEcbSingleBlockEncryption.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import android.annotation.SuppressLint;
+
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Utilities used for encrypting and decrypting Fast Pair packets.
+ */
+// SuppressLint for ""ecb encryption mode should not be used".
+// Reasons:
+// 1. FastPair data is guaranteed to be only 1 AES block in size, ECB is secure.
+// 2. In each case, the encrypted data is less than 16-bytes and is
+// padded up to 16-bytes using random data to fill the rest of the byte array,
+// so the plaintext will never be the same.
+@SuppressLint("GetInstance")
+public final class AesEcbSingleBlockEncryption {
+
+ public static final int AES_BLOCK_LENGTH = 16;
+ public static final int KEY_LENGTH = 16;
+
+ private AesEcbSingleBlockEncryption() {
+ }
+
+ /**
+ * Generates a 16-byte AES key.
+ */
+ public static byte[] generateKey() throws NoSuchAlgorithmException {
+ KeyGenerator generator = KeyGenerator.getInstance("AES");
+ generator.init(KEY_LENGTH * 8); // Ensure a 16-byte key is always used.
+ return generator.generateKey().getEncoded();
+ }
+
+ /**
+ * Encrypts data with the provided secret.
+ */
+ public static byte[] encrypt(byte[] secret, byte[] data) throws GeneralSecurityException {
+ return doEncryption(Cipher.ENCRYPT_MODE, secret, data);
+ }
+
+ /**
+ * Decrypts data with the provided secret.
+ */
+ public static byte[] decrypt(byte[] secret, byte[] data) throws GeneralSecurityException {
+ return doEncryption(Cipher.DECRYPT_MODE, secret, data);
+ }
+
+ private static byte[] doEncryption(int mode, byte[] secret, byte[] data)
+ throws GeneralSecurityException {
+ if (data.length != AES_BLOCK_LENGTH) {
+ throw new IllegalArgumentException("This encrypter only supports 16-byte inputs.");
+ }
+ Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
+ cipher.init(mode, new SecretKeySpec(secret, "AES"));
+ return cipher.doFinal(data);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAddress.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAddress.java
new file mode 100644
index 0000000..9bb5a86
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAddress.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.provider.Settings;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.google.common.base.Ascii;
+import com.google.common.io.BaseEncoding;
+
+import java.util.Locale;
+
+/** Utils for dealing with Bluetooth addresses. */
+public final class BluetoothAddress {
+
+ private static final BaseEncoding ENCODING = base16().upperCase().withSeparator(":", 2);
+
+ @VisibleForTesting
+ static final String SECURE_SETTINGS_KEY_BLUETOOTH_ADDRESS = "bluetooth_address";
+
+ /**
+ * @return The string format used by e.g. {@link android.bluetooth.BluetoothDevice}. Upper case.
+ * Example: "AA:BB:CC:11:22:33"
+ */
+ public static String encode(byte[] address) {
+ return ENCODING.encode(address);
+ }
+
+ /**
+ * @param address The string format used by e.g. {@link android.bluetooth.BluetoothDevice}.
+ * Case-insensitive. Example: "AA:BB:CC:11:22:33"
+ */
+ public static byte[] decode(String address) {
+ return ENCODING.decode(address.toUpperCase(Locale.US));
+ }
+
+ /**
+ * Get public bluetooth address.
+ *
+ * @param context a valid {@link Context} instance.
+ */
+ public static @Nullable byte[] getPublicAddress(Context context) {
+ String publicAddress =
+ Settings.Secure.getString(
+ context.getContentResolver(), SECURE_SETTINGS_KEY_BLUETOOTH_ADDRESS);
+ return publicAddress != null && BluetoothAdapter.checkBluetoothAddress(publicAddress)
+ ? decode(publicAddress)
+ : null;
+ }
+
+ /**
+ * Hides partial information of Bluetooth address.
+ * ex1: input is null, output should be empty string
+ * ex2: input is String(AA:BB:CC), output should be AA:BB:CC
+ * ex3: input is String(AA:BB:CC:DD:EE:FF), output should be XX:XX:XX:XX:EE:FF
+ * ex4: input is String(Aa:Bb:Cc:Dd:Ee:Ff), output should be XX:XX:XX:XX:EE:FF
+ * ex5: input is BluetoothDevice(AA:BB:CC:DD:EE:FF), output should be XX:XX:XX:XX:EE:FF
+ */
+ public static String maskBluetoothAddress(@Nullable Object address) {
+ if (address == null) {
+ return "";
+ }
+
+ if (address instanceof String) {
+ String originalAddress = (String) address;
+ String upperCasedAddress = Ascii.toUpperCase(originalAddress);
+ if (!BluetoothAdapter.checkBluetoothAddress(upperCasedAddress)) {
+ return originalAddress;
+ }
+ return convert(upperCasedAddress);
+ } else if (address instanceof BluetoothDevice) {
+ return convert(((BluetoothDevice) address).getAddress());
+ }
+
+ // For others, returns toString().
+ return address.toString();
+ }
+
+ private static String convert(String address) {
+ return "XX:XX:XX:XX:" + address.substring(12);
+ }
+
+ private BluetoothAddress() {}
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairer.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairer.java
new file mode 100644
index 0000000..07306c1
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairer.java
@@ -0,0 +1,774 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static android.bluetooth.BluetoothDevice.BOND_BONDED;
+import static android.bluetooth.BluetoothDevice.BOND_BONDING;
+import static android.bluetooth.BluetoothDevice.BOND_NONE;
+import static android.bluetooth.BluetoothDevice.ERROR;
+import static android.bluetooth.BluetoothProfile.A2DP;
+import static android.bluetooth.BluetoothProfile.HEADSET;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
+
+import static java.util.concurrent.Executors.newSingleThreadExecutor;
+
+import android.Manifest.permission;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+import androidx.core.content.ContextCompat;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.PasskeyCharacteristic;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.Profile;
+import com.android.server.nearby.common.bluetooth.fastpair.TimingLogger.ScopedTiming;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection.ChangeObserver;
+import com.android.server.nearby.intdefs.FastPairEventIntDefs.ConnectErrorCode;
+import com.android.server.nearby.intdefs.FastPairEventIntDefs.CreateBondErrorCode;
+import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode;
+
+import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Pairs to Bluetooth audio devices.
+ */
+public class BluetoothAudioPairer {
+
+ private static final String TAG = BluetoothAudioPairer.class.getSimpleName();
+
+ /**
+ * Hidden, see {@link BluetoothDevice}.
+ */
+ // TODO(b/202549655): remove Hidden usage.
+ private static final String EXTRA_REASON = "android.bluetooth.device.extra.REASON";
+
+ /**
+ * Hidden, see {@link BluetoothDevice}.
+ */
+ // TODO(b/202549655): remove Hidden usage.
+ private static final int PAIRING_VARIANT_CONSENT = 3;
+
+ /**
+ * Hidden, see {@link BluetoothDevice}.
+ */
+ // TODO(b/202549655): remove Hidden usage.
+ public static final int PAIRING_VARIANT_DISPLAY_PASSKEY = 4;
+
+ private static final int DISCOVERY_STATE_CHANGE_TIMEOUT_MS = 3000;
+
+ private final Context mContext;
+ private final Preferences mPreferences;
+ private final EventLoggerWrapper mEventLogger;
+ private final BluetoothDevice mDevice;
+ @Nullable
+ private final KeyBasedPairingInfo mKeyBasedPairingInfo;
+ @Nullable
+ private final PasskeyConfirmationHandler mPasskeyConfirmationHandler;
+ private final TimingLogger mTimingLogger;
+
+ private static boolean sTestMode = false;
+
+ static void enableTestMode() {
+ sTestMode = true;
+ }
+
+ static class KeyBasedPairingInfo {
+
+ private final byte[] mSecret;
+ private final GattConnectionManager mGattConnectionManager;
+ private final boolean mProviderInitiatesBonding;
+
+ /**
+ * @param secret The secret negotiated during the initial BLE handshake for Key-based
+ * Pairing. See {@link FastPairConnection#handshake}.
+ * @param gattConnectionManager A manager that knows how to get and create Gatt connections
+ * to the remote device.
+ */
+ KeyBasedPairingInfo(
+ byte[] secret,
+ GattConnectionManager gattConnectionManager,
+ boolean providerInitiatesBonding) {
+ this.mSecret = secret;
+ this.mGattConnectionManager = gattConnectionManager;
+ this.mProviderInitiatesBonding = providerInitiatesBonding;
+ }
+ }
+
+ public BluetoothAudioPairer(
+ Context context,
+ BluetoothDevice device,
+ Preferences preferences,
+ EventLoggerWrapper eventLogger,
+ @Nullable KeyBasedPairingInfo keyBasedPairingInfo,
+ @Nullable PasskeyConfirmationHandler passkeyConfirmationHandler,
+ TimingLogger timingLogger)
+ throws PairingException {
+ this.mContext = context;
+ this.mDevice = device;
+ this.mPreferences = preferences;
+ this.mEventLogger = eventLogger;
+ this.mKeyBasedPairingInfo = keyBasedPairingInfo;
+ this.mPasskeyConfirmationHandler = passkeyConfirmationHandler;
+ this.mTimingLogger = timingLogger;
+
+ // TODO(b/203455314): follow up with the following comments.
+ // The OS should give the user some UI to choose if they want to allow access, but there
+ // seems to be a bug where if we don't reject access, it's auto-granted in some cases
+ // (Plantronics headset gets contacts access when pairing with my Taimen via Bluetooth
+ // Settings, without me seeing any UI about it). b/64066631
+ //
+ // If that OS bug doesn't get fixed, we can flip these flags to force-reject the
+ // permissions.
+ if (preferences.getRejectPhonebookAccess() && (sTestMode ? false :
+ !device.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED))) {
+ throw new PairingException("Failed to deny contacts (phonebook) access.");
+ }
+ if (preferences.getRejectMessageAccess()
+ && (sTestMode ? false :
+ !device.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED))) {
+ throw new PairingException("Failed to deny message access.");
+ }
+ if (preferences.getRejectSimAccess()
+ && (sTestMode ? false :
+ !device.setSimAccessPermission(BluetoothDevice.ACCESS_REJECTED))) {
+ throw new PairingException("Failed to deny SIM access.");
+ }
+ }
+
+ boolean isPaired() {
+ return (sTestMode ? false : mDevice.getBondState() == BOND_BONDED);
+ }
+
+ /**
+ * Unpairs from the device. Throws an exception if any error occurs.
+ */
+ @WorkerThread
+ void unpair()
+ throws InterruptedException, ExecutionException, TimeoutException, PairingException {
+ int bondState = sTestMode ? BOND_NONE : mDevice.getBondState();
+ try (UnbondedReceiver unbondedReceiver = new UnbondedReceiver();
+ ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
+ "Unpair for state: " + bondState)) {
+ // We'll only get a state change broadcast if we're actually unbonding (method returns
+ // true).
+ if (bondState == BluetoothDevice.BOND_BONDED) {
+ mEventLogger.setCurrentEvent(EventCode.REMOVE_BOND);
+ Log.i(TAG, "removeBond with " + maskBluetoothAddress(mDevice));
+ mDevice.removeBond();
+ unbondedReceiver.await(
+ mPreferences.getRemoveBondTimeoutSeconds(), TimeUnit.SECONDS);
+ } else if (bondState == BluetoothDevice.BOND_BONDING) {
+ mEventLogger.setCurrentEvent(EventCode.CANCEL_BOND);
+ Log.i(TAG, "cancelBondProcess with " + maskBluetoothAddress(mDevice));
+ mDevice.cancelBondProcess();
+ unbondedReceiver.await(
+ mPreferences.getRemoveBondTimeoutSeconds(), TimeUnit.SECONDS);
+ } else {
+ // The OS may have beaten us in a race, unbonding before we called the method. So if
+ // we're (somehow) in the desired state then we're happy, if not then bail.
+ if (bondState != BluetoothDevice.BOND_NONE) {
+ throw new PairingException("returned false, state=%s", bondState);
+ }
+ }
+ }
+
+ // This seems to improve the probability that createBond will succeed after removeBond.
+ SystemClock.sleep(mPreferences.getRemoveBondSleepMillis());
+ mEventLogger.logCurrentEventSucceeded();
+ }
+
+ /**
+ * Pairs with the device. Throws an exception if any error occurs.
+ */
+ @WorkerThread
+ void pair()
+ throws InterruptedException, ExecutionException, TimeoutException, PairingException {
+ // Unpair first, because if we have a bond, but the other device has forgotten its bond,
+ // it can send us a pairing request that we're not ready for (which can pop up a dialog).
+ // Or, if we're in the middle of a (too-long) bonding attempt, we want to cancel.
+ unpair();
+
+ mEventLogger.setCurrentEvent(EventCode.CREATE_BOND);
+ try (BondedReceiver bondedReceiver = new BondedReceiver();
+ ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Create bond")) {
+ // If the provider's initiating the bond, we do nothing but wait for broadcasts.
+ if (mKeyBasedPairingInfo == null || !mKeyBasedPairingInfo.mProviderInitiatesBonding) {
+ if (!sTestMode) {
+ Log.i(TAG, "createBond with " + maskBluetoothAddress(mDevice) + ", type="
+ + mDevice.getType());
+ if (mPreferences.getSpecifyCreateBondTransportType()) {
+ mDevice.createBond(mPreferences.getCreateBondTransportType());
+ } else {
+ mDevice.createBond();
+ }
+ }
+ }
+ try {
+ bondedReceiver.await(mPreferences.getCreateBondTimeoutSeconds(), TimeUnit.SECONDS);
+ } catch (TimeoutException e) {
+ Log.w(TAG, "bondedReceiver time out after " + mPreferences
+ .getCreateBondTimeoutSeconds() + " seconds");
+ if (mPreferences.getIgnoreUuidTimeoutAfterBonded() && isPaired()) {
+ Log.w(TAG, "Created bond but never received UUIDs, attempting to continue.");
+ } else {
+ // Rethrow e to cause the pairing to fail and be retried if necessary.
+ throw e;
+ }
+ }
+ }
+ mEventLogger.logCurrentEventSucceeded();
+ }
+
+ /**
+ * Connects to the given profile. Throws an exception if any error occurs.
+ *
+ * <p>If remote device clears the link key, the BOND_BONDED state would transit to BOND_BONDING
+ * (and go through the pairing process again) when directly connecting the profile. By enabling
+ * enablePairingBehavior, we provide both pairing and connecting behaviors at the same time. See
+ * b/145699390 for more details.
+ */
+ // Suppression for possible null from ImmutableMap#get. See go/lsc-get-nullable
+ @SuppressWarnings("nullness:argument")
+ @WorkerThread
+ public void connect(short profileUuid, boolean enablePairingBehavior)
+ throws InterruptedException, ReflectionException, TimeoutException, ExecutionException,
+ ConnectException {
+ if (!mPreferences.isSupportedProfile(profileUuid)) {
+ throw new ConnectException(
+ ConnectErrorCode.UNSUPPORTED_PROFILE, "Unsupported profile=%s", profileUuid);
+ }
+ Profile profile = Constants.PROFILES.get(profileUuid);
+ Log.i(TAG,
+ "Connecting to profile=" + profile + " on device=" + maskBluetoothAddress(mDevice));
+ try (BondedReceiver bondedReceiver = enablePairingBehavior ? new BondedReceiver() : null;
+ ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
+ "Connect: " + profile)) {
+ connectByProfileProxy(profile);
+ }
+ }
+
+ private void connectByProfileProxy(Profile profile)
+ throws ReflectionException, InterruptedException, ExecutionException, TimeoutException,
+ ConnectException {
+ try (BluetoothProfileWrapper autoClosingProxy = new BluetoothProfileWrapper(profile);
+ ConnectedReceiver connectedReceiver = new ConnectedReceiver(profile)) {
+ BluetoothProfile proxy = autoClosingProxy.mProxy;
+
+ // Try to connect via reflection
+ Log.v(TAG, "Connect to proxy=" + proxy);
+
+ if (!sTestMode) {
+ if (!(Boolean) Reflect.on(proxy).withMethod("connect", BluetoothDevice.class)
+ .get(mDevice)) {
+ // If we're already connecting, connect() may return false. :/
+ Log.w(TAG, "connect returned false, expected if connecting, state="
+ + proxy.getConnectionState(mDevice));
+ }
+ }
+
+ // If we're already connected, the OS may not send the connection state broadcast, so
+ // return immediately for that case.
+ if (!sTestMode) {
+ if (proxy.getConnectionState(mDevice) == BluetoothProfile.STATE_CONNECTED) {
+ Log.v(TAG, "connectByProfileProxy: already connected to device="
+ + maskBluetoothAddress(mDevice));
+ return;
+ }
+ }
+
+ try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Wait connection")) {
+ // Wait for connecting to succeed or fail (via event or timeout).
+ connectedReceiver
+ .await(mPreferences.getCreateBondTimeoutSeconds(), TimeUnit.SECONDS);
+ }
+ }
+ }
+
+ private class BluetoothProfileWrapper implements AutoCloseable {
+
+ // incompatible types in assignment.
+ @SuppressWarnings("nullness:assignment")
+ private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+
+ private final Profile mProfile;
+ private final BluetoothProfile mProxy;
+
+ /**
+ * Blocks until we get the proxy. Throws on error.
+ */
+ private BluetoothProfileWrapper(Profile profile)
+ throws InterruptedException, ExecutionException, TimeoutException,
+ ConnectException {
+ this.mProfile = profile;
+ mProxy = getProfileProxy(profile);
+ }
+
+ @Override
+ public void close() {
+ try (ScopedTiming scopedTiming =
+ new ScopedTiming(mTimingLogger, "Close profile: " + mProfile)) {
+ if (!sTestMode) {
+ mBluetoothAdapter.closeProfileProxy(mProfile.type, mProxy);
+ }
+ }
+ }
+
+ private BluetoothProfile getProfileProxy(BluetoothProfileWrapper this, Profile profile)
+ throws InterruptedException, ExecutionException, TimeoutException,
+ ConnectException {
+ if (profile.type != A2DP && profile.type != HEADSET) {
+ throw new IllegalArgumentException("Unsupported profile type=" + profile.type);
+ }
+
+ SettableFuture<BluetoothProfile> proxyFuture = SettableFuture.create();
+ BluetoothProfile.ServiceListener listener =
+ new BluetoothProfile.ServiceListener() {
+ @UiThread
+ @Override
+ public void onServiceConnected(int profileType, BluetoothProfile proxy) {
+ proxyFuture.set(proxy);
+ }
+
+ @Override
+ public void onServiceDisconnected(int profileType) {
+ Log.v(TAG, "proxy disconnected for profile=" + profile);
+ }
+ };
+
+ if (!mBluetoothAdapter.getProfileProxy(mContext, listener, profile.type)) {
+ throw new ConnectException(
+ ConnectErrorCode.GET_PROFILE_PROXY_FAILED,
+ "getProfileProxy failed immediately");
+ }
+
+ return proxyFuture.get(mPreferences.getProxyTimeoutSeconds(), TimeUnit.SECONDS);
+ }
+ }
+
+ private class UnbondedReceiver extends DeviceIntentReceiver {
+
+ private UnbondedReceiver() {
+ super(mContext, mPreferences, mDevice, BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ }
+
+ @Override
+ protected void onReceiveDeviceIntent(Intent intent) throws Exception {
+ if (mDevice.getBondState() == BOND_NONE) {
+ try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
+ "Close UnbondedReceiver")) {
+ close();
+ }
+ }
+ }
+ }
+
+ /**
+ * Receiver that closes after bonding has completed.
+ */
+ class BondedReceiver extends DeviceIntentReceiver {
+
+ private boolean mReceivedUuids = false;
+ private boolean mReceivedPasskey = false;
+
+ private BondedReceiver() {
+ super(
+ mContext,
+ mPreferences,
+ mDevice,
+ BluetoothDevice.ACTION_PAIRING_REQUEST,
+ BluetoothDevice.ACTION_BOND_STATE_CHANGED,
+ BluetoothDevice.ACTION_UUID);
+ }
+
+ // switching on a possibly-null value (intent.getAction())
+ // incompatible types in argument.
+ @SuppressWarnings({"nullness:switching.nullable", "nullness:argument"})
+ @Override
+ protected void onReceiveDeviceIntent(Intent intent)
+ throws PairingException, InterruptedException, ExecutionException, TimeoutException,
+ BluetoothException, GeneralSecurityException {
+ switch (intent.getAction()) {
+ case BluetoothDevice.ACTION_PAIRING_REQUEST:
+ int variant = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, ERROR);
+ int passkey = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, ERROR);
+ handlePairingRequest(variant, passkey);
+ break;
+ case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
+ // Use the state in the intent, not device.getBondState(), to avoid a race where
+ // we log the wrong failure reason during a rapid transition.
+ int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, ERROR);
+ int reason = intent.getIntExtra(EXTRA_REASON, ERROR);
+ handleBondStateChanged(bondState, reason);
+ break;
+ case BluetoothDevice.ACTION_UUID:
+ // According to eisenbach@ and pavlin@, there's always a UUID broadcast when
+ // pairing (it can happen either before or after the transition to BONDED).
+ if (mPreferences.getWaitForUuidsAfterBonding()) {
+ Parcelable[] uuids = intent
+ .getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID);
+ handleUuids(uuids);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void handlePairingRequest(int variant, int passkey) {
+ Log.i(TAG, "Pairing request, variant=" + variant + ", passkey=" + (passkey == ERROR
+ ? "(none)" : String.valueOf(passkey)));
+ if (mPreferences.getMoreEventLogForQuality()) {
+ mEventLogger.setCurrentEvent(EventCode.HANDLE_PAIRING_REQUEST);
+ }
+
+ if (mPreferences.getSupportHidDevice() && variant == PAIRING_VARIANT_DISPLAY_PASSKEY) {
+ mReceivedPasskey = true;
+ extendAwaitSecond(
+ mPreferences.getHidCreateBondTimeoutSeconds()
+ - mPreferences.getCreateBondTimeoutSeconds());
+ triggerDiscoverStateChange();
+ if (mPreferences.getMoreEventLogForQuality()) {
+ mEventLogger.logCurrentEventSucceeded();
+ }
+ return;
+
+ } else {
+ // Prevent Bluetooth Settings from getting the pairing request and showing its own
+ // UI.
+ abortBroadcast();
+
+ if (variant == PAIRING_VARIANT_CONSENT
+ && mKeyBasedPairingInfo == null // Fast Pair 1.0 device
+ && mPreferences.getAcceptConsentForFastPairOne()) {
+ // Previously, if Bluetooth decided to use the Just Works variant (e.g. Fast
+ // Pair 1.0), we don't get a pairing request broadcast at all.
+ // However, after CVE-2019-2225, Bluetooth will decide to ask consent from
+ // users. Details:
+ // https://source.android.com/security/bulletin/2019-12-01#system
+ // Since we've certified the Fast Pair 1.0 devices, and user taps to pair it
+ // (with the device's image), we could help user to accept the consent.
+ if (!sTestMode) {
+ mDevice.setPairingConfirmation(true);
+ }
+ if (mPreferences.getMoreEventLogForQuality()) {
+ mEventLogger.logCurrentEventSucceeded();
+ }
+ return;
+ } else if (variant != BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION) {
+ if (!sTestMode) {
+ mDevice.setPairingConfirmation(false);
+ }
+ if (mPreferences.getMoreEventLogForQuality()) {
+ mEventLogger.logCurrentEventFailed(
+ new CreateBondException(
+ CreateBondErrorCode.INCORRECT_VARIANT, 0,
+ "Incorrect variant for FastPair"));
+ }
+ return;
+ }
+ mReceivedPasskey = true;
+
+ if (mKeyBasedPairingInfo == null) {
+ if (mPreferences.getAcceptPasskey()) {
+ // Must be the simulator using FP 1.0 (no Key-based Pairing). Real
+ // headphones using FP 1.0 use Just Works instead (and maybe we should
+ // disable this flag for them).
+ if (!sTestMode) {
+ mDevice.setPairingConfirmation(true);
+ }
+ }
+ if (mPreferences.getMoreEventLogForQuality()) {
+ if (!sTestMode) {
+ mEventLogger.logCurrentEventSucceeded();
+ }
+ }
+ return;
+ }
+ }
+
+ if (mPreferences.getMoreEventLogForQuality()) {
+ mEventLogger.logCurrentEventSucceeded();
+ }
+
+ newSingleThreadExecutor()
+ .execute(
+ () -> {
+ try (ScopedTiming scopedTiming1 =
+ new ScopedTiming(mTimingLogger, "Exchange passkey")) {
+ mEventLogger.setCurrentEvent(EventCode.PASSKEY_EXCHANGE);
+
+ // We already check above, but the static analyzer's not
+ // convinced without this.
+ Preconditions.checkNotNull(mKeyBasedPairingInfo);
+ BluetoothGattConnection connection =
+ mKeyBasedPairingInfo.mGattConnectionManager
+ .getConnection();
+ UUID characteristicUuid =
+ PasskeyCharacteristic.getId(connection);
+ ChangeObserver remotePasskeyObserver =
+ connection.enableNotification(FastPairService.ID,
+ characteristicUuid);
+ Log.i(TAG, "Sending local passkey.");
+ byte[] encryptedData;
+ try (ScopedTiming scopedTiming2 =
+ new ScopedTiming(mTimingLogger, "Encrypt passkey")) {
+ encryptedData =
+ PasskeyCharacteristic.encrypt(
+ PasskeyCharacteristic.Type.SEEKER,
+ mKeyBasedPairingInfo.mSecret, passkey);
+ }
+ try (ScopedTiming scopedTiming3 =
+ new ScopedTiming(mTimingLogger,
+ "Send passkey to remote")) {
+ connection.writeCharacteristic(
+ FastPairService.ID, characteristicUuid,
+ encryptedData);
+ }
+ Log.i(TAG, "Waiting for remote passkey.");
+ byte[] encryptedRemotePasskey;
+ try (ScopedTiming scopedTiming4 =
+ new ScopedTiming(mTimingLogger,
+ "Wait for remote passkey")) {
+ encryptedRemotePasskey =
+ remotePasskeyObserver.waitForUpdate(
+ TimeUnit.SECONDS.toMillis(mPreferences
+ .getGattOperationTimeoutSeconds()));
+ }
+ int remotePasskey;
+ try (ScopedTiming scopedTiming5 =
+ new ScopedTiming(mTimingLogger, "Decrypt passkey")) {
+ remotePasskey =
+ PasskeyCharacteristic.decrypt(
+ PasskeyCharacteristic.Type.PROVIDER,
+ mKeyBasedPairingInfo.mSecret,
+ encryptedRemotePasskey);
+ }
+
+ // We log success if we made it through with no exceptions.
+ // If the passkey was wrong, pairing will fail and we'll log
+ // BOND_BROKEN with reason = AUTH_FAILED.
+ mEventLogger.logCurrentEventSucceeded();
+
+ boolean isPasskeyCorrect = passkey == remotePasskey;
+ if (isPasskeyCorrect) {
+ Log.i(TAG, "Passkey correct.");
+ } else {
+ Log.e(TAG, "Passkey incorrect, local= " + passkey
+ + ", remote=" + remotePasskey);
+ }
+
+ // Don't estimate the {@code ScopedTiming} because the
+ // passkey confirmation is done by UI.
+ if (isPasskeyCorrect
+ && mPreferences.getHandlePasskeyConfirmationByUi()
+ && mPasskeyConfirmationHandler != null) {
+ Log.i(TAG, "Callback the passkey to UI for confirmation.");
+ mPasskeyConfirmationHandler
+ .onPasskeyConfirmation(mDevice, passkey);
+ } else {
+ try (ScopedTiming scopedTiming6 =
+ new ScopedTiming(
+ mTimingLogger, "Confirm the pairing: "
+ + isPasskeyCorrect)) {
+ mDevice.setPairingConfirmation(isPasskeyCorrect);
+ }
+ }
+ } catch (BluetoothException
+ | GeneralSecurityException
+ | InterruptedException
+ | ExecutionException
+ | TimeoutException e) {
+ mEventLogger.logCurrentEventFailed(e);
+ closeWithError(e);
+ }
+ });
+ }
+
+ /**
+ * Workaround to let Settings popup a pairing dialog instead of notification. When pairing
+ * request intent passed to Settings, it'll check several conditions to decide that it
+ * should show a dialog or a notification. One of those conditions is to check if the device
+ * is in discovery mode recently, which can be fulfilled by calling {@link
+ * BluetoothAdapter#startDiscovery()}. This method aims to fulfill the condition, and block
+ * the pairing broadcast for at most
+ * {@link BluetoothAudioPairer#DISCOVERY_STATE_CHANGE_TIMEOUT_MS}
+ * to make sure that we fulfill the condition first and successful.
+ */
+ // dereference of possibly-null reference bluetoothAdapter
+ @SuppressWarnings("nullness:dereference.of.nullable")
+ private void triggerDiscoverStateChange() {
+ BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+
+ if (bluetoothAdapter.isDiscovering()) {
+ return;
+ }
+
+ HandlerThread backgroundThread = new HandlerThread("TriggerDiscoverStateChangeThread");
+ backgroundThread.start();
+
+ AtomicBoolean result = new AtomicBoolean(false);
+ SimpleBroadcastReceiver receiver =
+ new SimpleBroadcastReceiver(
+ mContext,
+ mPreferences,
+ new Handler(backgroundThread.getLooper()),
+ BluetoothAdapter.ACTION_DISCOVERY_STARTED,
+ BluetoothAdapter.ACTION_DISCOVERY_FINISHED) {
+
+ @Override
+ protected void onReceive(Intent intent) throws Exception {
+ result.set(true);
+ close();
+ }
+ };
+
+ Log.i(TAG, "triggerDiscoverStateChange call startDiscovery.");
+ // Uses startDiscovery to trigger Settings show pairing dialog instead of notification.
+ if (!sTestMode) {
+ bluetoothAdapter.startDiscovery();
+ bluetoothAdapter.cancelDiscovery();
+ }
+ try {
+ receiver.await(DISCOVERY_STATE_CHANGE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ Log.w(TAG, "triggerDiscoverStateChange failed!");
+ }
+
+ backgroundThread.quitSafely();
+ try {
+ backgroundThread.join();
+ } catch (InterruptedException e) {
+ Log.i(TAG, "triggerDiscoverStateChange backgroundThread.join meet exception!", e);
+ }
+
+ if (result.get()) {
+ Log.i(TAG, "triggerDiscoverStateChange successful.");
+ }
+ }
+
+ private void handleBondStateChanged(int bondState, int reason)
+ throws PairingException, InterruptedException, ExecutionException,
+ TimeoutException {
+ Log.i(TAG, "Bond state changed to " + bondState + ", reason=" + reason);
+ switch (bondState) {
+ case BOND_BONDED:
+ if (mKeyBasedPairingInfo != null && !mReceivedPasskey) {
+ // The device bonded with Just Works, although we did the Key-based Pairing
+ // GATT handshake and agreed on a pairing secret. It might be a Person In
+ // The Middle Attack!
+ try (ScopedTiming scopedTiming =
+ new ScopedTiming(mTimingLogger,
+ "Close BondedReceiver: POSSIBLE_MITM")) {
+ closeWithError(
+ new CreateBondException(
+ CreateBondErrorCode.POSSIBLE_MITM,
+ reason,
+ "Unexpectedly bonded without a passkey. It might be a "
+ + "Person In The Middle Attack! Unbonding!"));
+ }
+ unpair();
+ } else if (!mPreferences.getWaitForUuidsAfterBonding()
+ || (mPreferences.getReceiveUuidsAndBondedEventBeforeClose()
+ && mReceivedUuids)) {
+ try (ScopedTiming scopedTiming =
+ new ScopedTiming(mTimingLogger, "Close BondedReceiver")) {
+ close();
+ }
+ }
+ break;
+ case BOND_NONE:
+ throw new CreateBondException(
+ CreateBondErrorCode.BOND_BROKEN, reason, "Bond broken, reason=%d",
+ reason);
+ case BOND_BONDING:
+ default:
+ break;
+ }
+ }
+
+ private void handleUuids(Parcelable[] uuids) {
+ Log.i(TAG, "Got UUIDs for " + maskBluetoothAddress(mDevice) + ": "
+ + Arrays.toString(uuids));
+ mReceivedUuids = true;
+ if (!mPreferences.getReceiveUuidsAndBondedEventBeforeClose() || isPaired()) {
+ try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
+ "Close BondedReceiver")) {
+ close();
+ }
+ }
+ }
+ }
+
+ private class ConnectedReceiver extends DeviceIntentReceiver {
+
+ private ConnectedReceiver(Profile profile) throws ConnectException {
+ super(mContext, mPreferences, mDevice, profile.connectionStateAction);
+ }
+
+ @Override
+ public void onReceiveDeviceIntent(Intent intent) throws PairingException {
+ int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, ERROR);
+ Log.i(TAG, "Connection state changed to " + state);
+ switch (state) {
+ case BluetoothAdapter.STATE_CONNECTED:
+ try (ScopedTiming scopedTiming =
+ new ScopedTiming(mTimingLogger, "Close ConnectedReceiver")) {
+ close();
+ }
+ break;
+ case BluetoothAdapter.STATE_DISCONNECTED:
+ throw new ConnectException(ConnectErrorCode.DISCONNECTED, "Disconnected");
+ case BluetoothAdapter.STATE_CONNECTING:
+ case BluetoothAdapter.STATE_DISCONNECTING:
+ default:
+ break;
+ }
+ }
+ }
+
+ private boolean hasPermission(String permission) {
+ return ContextCompat.checkSelfPermission(mContext, permission) == PERMISSION_GRANTED;
+ }
+
+ public BluetoothDevice getDevice() {
+ return mDevice;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairer.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairer.java
new file mode 100644
index 0000000..6c467d3
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairer.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static android.bluetooth.BluetoothDevice.BOND_BONDED;
+import static android.bluetooth.BluetoothDevice.BOND_BONDING;
+import static android.bluetooth.BluetoothDevice.BOND_NONE;
+import static android.bluetooth.BluetoothDevice.ERROR;
+import static android.bluetooth.BluetoothDevice.EXTRA_DEVICE;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.annotation.SuppressLint;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import androidx.annotation.WorkerThread;
+
+import com.google.common.base.Strings;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Pairs to Bluetooth classic devices with passkey confirmation.
+ */
+// TODO(b/202524672): Add class unit test.
+public class BluetoothClassicPairer {
+
+ private static final String TAG = BluetoothClassicPairer.class.getSimpleName();
+ /**
+ * Hidden, see {@link BluetoothDevice}.
+ */
+ private static final String EXTRA_REASON = "android.bluetooth.device.extra.REASON";
+
+ private final Context mContext;
+ private final BluetoothDevice mDevice;
+ private final Preferences mPreferences;
+ private final PasskeyConfirmationHandler mPasskeyConfirmationHandler;
+
+ public BluetoothClassicPairer(
+ Context context,
+ BluetoothDevice device,
+ Preferences preferences,
+ PasskeyConfirmationHandler passkeyConfirmationHandler) {
+ this.mContext = context;
+ this.mDevice = device;
+ this.mPreferences = preferences;
+ this.mPasskeyConfirmationHandler = passkeyConfirmationHandler;
+ }
+
+ /**
+ * Pairs with the device. Throws a {@link PairingException} if any error occurs.
+ */
+ @WorkerThread
+ public void pair() throws PairingException {
+ Log.i(TAG, "BluetoothClassicPairer, createBond with " + maskBluetoothAddress(mDevice)
+ + ", type=" + mDevice.getType());
+ try (BondedReceiver bondedReceiver = new BondedReceiver()) {
+ if (mDevice.createBond()) {
+ bondedReceiver.await(mPreferences.getCreateBondTimeoutSeconds(), SECONDS);
+ } else {
+ throw new PairingException(
+ "BluetoothClassicPairer, createBond got immediate error");
+ }
+ } catch (TimeoutException | InterruptedException | ExecutionException e) {
+ throw new PairingException("BluetoothClassicPairer, createBond failed", e);
+ }
+ }
+
+ protected boolean isPaired() {
+ return mDevice.getBondState() == BOND_BONDED;
+ }
+
+ /**
+ * Receiver that closes after bonding has completed.
+ */
+ private class BondedReceiver extends DeviceIntentReceiver {
+
+ private BondedReceiver() {
+ super(
+ mContext,
+ mPreferences,
+ mDevice,
+ BluetoothDevice.ACTION_PAIRING_REQUEST,
+ BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ }
+
+ /**
+ * Called with ACTION_PAIRING_REQUEST and ACTION_BOND_STATE_CHANGED about the interesting
+ * device (see {@link DeviceIntentReceiver}).
+ *
+ * <p>The ACTION_PAIRING_REQUEST intent provides the passkey which will be sent to the
+ * {@link PasskeyConfirmationHandler} for showing the UI, and the ACTION_BOND_STATE_CHANGED
+ * will provide the result of the bonding.
+ */
+ @Override
+ protected void onReceiveDeviceIntent(Intent intent) {
+ String intentAction = intent.getAction();
+ BluetoothDevice remoteDevice = intent.getParcelableExtra(EXTRA_DEVICE);
+ if (Strings.isNullOrEmpty(intentAction)
+ || remoteDevice == null
+ || !remoteDevice.getAddress().equals(mDevice.getAddress())) {
+ Log.w(TAG,
+ "BluetoothClassicPairer, receives " + intentAction
+ + " from unexpected device " + maskBluetoothAddress(remoteDevice));
+ return;
+ }
+ switch (intentAction) {
+ case BluetoothDevice.ACTION_PAIRING_REQUEST:
+ handlePairingRequest(
+ remoteDevice,
+ intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, ERROR),
+ intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, ERROR));
+ break;
+ case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
+ handleBondStateChanged(
+ intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, ERROR),
+ intent.getIntExtra(EXTRA_REASON, ERROR));
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void handlePairingRequest(BluetoothDevice device, int variant, int passkey) {
+ Log.i(TAG,
+ "BluetoothClassicPairer, pairing request, " + device + ", " + variant + ", "
+ + passkey);
+ // Prevent Bluetooth Settings from getting the pairing request and showing its own UI.
+ abortBroadcast();
+ mPasskeyConfirmationHandler.onPasskeyConfirmation(device, passkey);
+ }
+
+ private void handleBondStateChanged(int bondState, int reason) {
+ Log.i(TAG,
+ "BluetoothClassicPairer, bond state changed to " + bondState + ", reason="
+ + reason);
+ switch (bondState) {
+ case BOND_BONDING:
+ // Don't close!
+ return;
+ case BOND_BONDED:
+ close();
+ return;
+ case BOND_NONE:
+ default:
+ closeWithError(
+ new PairingException(
+ "BluetoothClassicPairer, createBond failed, reason:" + reason));
+ }
+ }
+ }
+
+ // Applies UsesPermission annotation will create circular dependency.
+ @SuppressLint("MissingPermission")
+ static void setPairingConfirmation(BluetoothDevice device, boolean confirm) {
+ Log.i(TAG, "BluetoothClassicPairer: setPairingConfirmation " + maskBluetoothAddress(device)
+ + ", confirm: " + confirm);
+ device.setPairingConfirmation(confirm);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothUuids.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothUuids.java
new file mode 100644
index 0000000..c5475a6
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothUuids.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import java.util.UUID;
+
+/**
+ * Utilities for dealing with UUIDs assigned by the Bluetooth SIG. Has a lot in common with
+ * com.android.BluetoothUuid, but that class is hidden.
+ */
+public class BluetoothUuids {
+
+ /**
+ * The Base UUID is used for calculating 128-bit UUIDs from "short UUIDs" (16- and 32-bit).
+ *
+ * @see {https://www.bluetooth.com/specifications/assigned-numbers/service-discovery}
+ */
+ private static final UUID BASE_UUID = UUID.fromString("00000000-0000-1000-8000-00805F9B34FB");
+
+ /**
+ * Fast Pair custom GATT characteristics 128-bit UUIDs base.
+ *
+ * <p>Notes: The 16-bit value locates at the 3rd and 4th bytes.
+ *
+ * @see {go/fastpair-128bit-gatt}
+ */
+ private static final UUID FAST_PAIR_BASE_UUID =
+ UUID.fromString("FE2C0000-8366-4814-8EB0-01DE32100BEA");
+
+ private static final int BIT_INDEX_OF_16_BIT_UUID = 32;
+
+ private BluetoothUuids() {}
+
+ /**
+ * Returns the 16-bit version of the UUID. If this is not a 16-bit UUID, throws
+ * IllegalArgumentException.
+ */
+ public static short get16BitUuid(UUID uuid) {
+ if (!is16BitUuid(uuid)) {
+ throw new IllegalArgumentException("Not a 16-bit Bluetooth UUID: " + uuid);
+ }
+ return (short) (uuid.getMostSignificantBits() >> BIT_INDEX_OF_16_BIT_UUID);
+ }
+
+ /** Checks whether the UUID is 16 bit */
+ public static boolean is16BitUuid(UUID uuid) {
+ // See Service Discovery Protocol in the Bluetooth Core Specification. Bits at index 32-48
+ // are the 16-bit UUID, and the rest must match the Base UUID.
+ return uuid.getLeastSignificantBits() == BASE_UUID.getLeastSignificantBits()
+ && (uuid.getMostSignificantBits() & 0xFFFF0000FFFFFFFFL)
+ == BASE_UUID.getMostSignificantBits();
+ }
+
+ /** Converts short UUID to 128 bit UUID */
+ public static UUID to128BitUuid(short shortUuid) {
+ return new UUID(
+ ((shortUuid & 0xFFFFL) << BIT_INDEX_OF_16_BIT_UUID)
+ | BASE_UUID.getMostSignificantBits(), BASE_UUID.getLeastSignificantBits());
+ }
+
+ /** Transfers the 16-bit Fast Pair custom GATT characteristics to 128-bit. */
+ public static UUID toFastPair128BitUuid(short shortUuid) {
+ return new UUID(
+ ((shortUuid & 0xFFFFL) << BIT_INDEX_OF_16_BIT_UUID)
+ | FAST_PAIR_BASE_UUID.getMostSignificantBits(),
+ FAST_PAIR_BASE_UUID.getLeastSignificantBits());
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BroadcastConstants.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BroadcastConstants.java
new file mode 100644
index 0000000..c26c6ad
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BroadcastConstants.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+/**
+ * Constants to share with the cloud syncing process.
+ */
+public class BroadcastConstants {
+
+ // TODO: Set right value for AOSP.
+ /** Package name of the cloud syncing logic. */
+ public static final String PACKAGE_NAME = "PACKAGE_NAME";
+ /** Service name of the cloud syncing instance. */
+ public static final String SERVICE_NAME = PACKAGE_NAME + ".SERVICE_NAME";
+ private static final String PREFIX = PACKAGE_NAME + ".PREFIX_NAME.";
+
+ /** Action when a fast pair device is added. */
+ public static final String ACTION_FAST_PAIR_DEVICE_ADDED =
+ PREFIX + "ACTION_FAST_PAIR_DEVICE_ADDED";
+ /**
+ * The BLE address of a device. BLE is used here instead of public because the caller of the
+ * library never knows what the device's public address is.
+ */
+ public static final String EXTRA_ADDRESS = PREFIX + "BLE_ADDRESS";
+ /** The public address of a device. */
+ public static final String EXTRA_PUBLIC_ADDRESS = PREFIX + "PUBLIC_ADDRESS";
+ /** Account key. */
+ public static final String EXTRA_ACCOUNT_KEY = PREFIX + "ACCOUNT_KEY";
+ /** Whether a paring is retroactive. */
+ public static final String EXTRA_RETROACTIVE_PAIR = PREFIX + "EXTRA_RETROACTIVE_PAIR";
+
+ private BroadcastConstants() {
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Bytes.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Bytes.java
new file mode 100644
index 0000000..637cd03
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Bytes.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import androidx.annotation.Nullable;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.ShortBuffer;
+import java.util.Arrays;
+
+/** Represents a block of bytes, with hashCode and equals. */
+public abstract class Bytes {
+ private static final char[] sHexDigits = "0123456789abcdef".toCharArray();
+ private final byte[] mBytes;
+
+ /**
+ * A logical value consisting of one or more bytes in the given order (little-endian, i.e.
+ * LSO...MSO, or big-endian, i.e. MSO...LSO). E.g. the Fast Pair Model ID is a 3-byte value,
+ * and a Bluetooth device address is a 6-byte value.
+ */
+ public static class Value extends Bytes {
+ private final ByteOrder mByteOrder;
+
+ /**
+ * Constructor.
+ */
+ public Value(byte[] bytes, ByteOrder byteOrder) {
+ super(bytes);
+ this.mByteOrder = byteOrder;
+ }
+
+ /**
+ * Gets bytes.
+ */
+ public byte[] getBytes(ByteOrder byteOrder) {
+ return this.mByteOrder.equals(byteOrder) ? getBytes() : reverse(getBytes());
+ }
+
+ private static byte[] reverse(byte[] bytes) {
+ byte[] reversedBytes = new byte[bytes.length];
+ for (int i = 0; i < bytes.length; i++) {
+ reversedBytes[i] = bytes[bytes.length - i - 1];
+ }
+ return reversedBytes;
+ }
+ }
+
+ Bytes(byte[] bytes) {
+ mBytes = bytes;
+ }
+
+ private static String toHexString(byte[] bytes) {
+ StringBuilder sb = new StringBuilder(2 * bytes.length);
+ for (byte b : bytes) {
+ sb.append(sHexDigits[(b >> 4) & 0xf]).append(sHexDigits[b & 0xf]);
+ }
+ return sb.toString();
+ }
+
+ /** Returns 2-byte values in the same order, each using the given byte order. */
+ public static byte[] toBytes(ByteOrder byteOrder, short... shorts) {
+ ByteBuffer byteBuffer = ByteBuffer.allocate(shorts.length * 2).order(byteOrder);
+ for (short s : shorts) {
+ byteBuffer.putShort(s);
+ }
+ return byteBuffer.array();
+ }
+
+ /** Returns the shorts in the same order, each converted using the given byte order. */
+ static short[] toShorts(ByteOrder byteOrder, byte[] bytes) {
+ ShortBuffer shortBuffer = ByteBuffer.wrap(bytes).order(byteOrder).asShortBuffer();
+ short[] shorts = new short[shortBuffer.remaining()];
+ shortBuffer.get(shorts);
+ return shorts;
+ }
+
+ /** @return The bytes. */
+ public byte[] getBytes() {
+ return mBytes;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Bytes)) {
+ return false;
+ }
+ Bytes that = (Bytes) o;
+ return Arrays.equals(mBytes, that.mBytes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(mBytes);
+ }
+
+ @Override
+ public String toString() {
+ return toHexString(mBytes);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ConnectException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ConnectException.java
new file mode 100644
index 0000000..9c8d292
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ConnectException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import com.android.server.nearby.intdefs.FastPairEventIntDefs.ConnectErrorCode;
+
+
+/** Thrown when connecting to a bluetooth device fails. */
+public class ConnectException extends PairingException {
+ final @ConnectErrorCode int mErrorCode;
+
+ ConnectException(@ConnectErrorCode int errorCode, String format, Object... objects) {
+ super(format, objects);
+ this.mErrorCode = errorCode;
+ }
+
+ /** Returns error code. */
+ public @ConnectErrorCode int getErrorCode() {
+ return mErrorCode;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Constants.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Constants.java
new file mode 100644
index 0000000..cfecd2f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Constants.java
@@ -0,0 +1,703 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static android.bluetooth.BluetoothProfile.A2DP;
+import static android.bluetooth.BluetoothProfile.HEADSET;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.to128BitUuid;
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.toFastPair128BitUuid;
+
+import static com.google.common.primitives.Bytes.concat;
+
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothHeadset;
+import android.util.Log;
+
+import androidx.annotation.IntDef;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.primitives.Shorts;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.Random;
+import java.util.UUID;
+
+/**
+ * Fast Pair and Transport Discovery Service constants.
+ *
+ * <p>Unless otherwise specified, these numbers come from
+ * {https://www.bluetooth.com/specifications/gatt}.
+ */
+public final class Constants {
+
+ /** A2DP sink service uuid. */
+ public static final short A2DP_SINK_SERVICE_UUID = 0x110B;
+
+ /** Headset service uuid. */
+ public static final short HEADSET_SERVICE_UUID = 0x1108;
+
+ /** Hands free sink service uuid. */
+ public static final short HANDS_FREE_SERVICE_UUID = 0x111E;
+
+ /** Bluetooth address length. */
+ public static final int BLUETOOTH_ADDRESS_LENGTH = 6;
+
+ private static final String TAG = Constants.class.getSimpleName();
+
+ /**
+ * Defined by https://developers.google.com/nearby/fast-pair/spec.
+ */
+ public static final class FastPairService {
+
+ /** Fast Pair service UUID. */
+ public static final UUID ID = to128BitUuid((short) 0xFE2C);
+
+ /**
+ * Characteristic to write verification bytes to during the key handshake.
+ */
+ public static final class KeyBasedPairingCharacteristic {
+
+ private static final short SHORT_UUID = 0x1234;
+
+ /**
+ * Gets the new 128-bit UUID of this characteristic.
+ *
+ * <p>Note: For GATT server only. GATT client should use {@link
+ * KeyBasedPairingCharacteristic#getId(BluetoothGattConnection)}.
+ */
+ public static final UUID CUSTOM_128_BIT_UUID = toFastPair128BitUuid(SHORT_UUID);
+
+ /**
+ * Gets the {@link UUID} of this characteristic.
+ *
+ * <p>This method is designed for being backward compatible with old version of UUID
+ * therefore needs the {@link BluetoothGattConnection} parameter to check the supported
+ * status of the Fast Pair provider.
+ */
+ public static UUID getId(BluetoothGattConnection gattConnection) {
+ return getSupportedUuid(gattConnection, SHORT_UUID);
+ }
+
+ /**
+ * Constants related to the decrypted request written to this characteristic.
+ */
+ public static final class Request {
+
+ /**
+ * The size of this message.
+ */
+ public static final int SIZE = 16;
+
+ /**
+ * The index of this message for indicating the type byte.
+ */
+ public static final int TYPE_INDEX = 0;
+
+ /**
+ * The index of this message for indicating the flags byte.
+ */
+ public static final int FLAGS_INDEX = 1;
+
+ /**
+ * The index of this message for indicating the verification data start from.
+ */
+ public static final int VERIFICATION_DATA_INDEX = 2;
+
+ /**
+ * The length of verification data, it is Provider’s current BLE address or public
+ * address.
+ */
+ public static final int VERIFICATION_DATA_LENGTH = BLUETOOTH_ADDRESS_LENGTH;
+
+ /**
+ * The index of this message for indicating the seeker's public address start from.
+ */
+ public static final int SEEKER_PUBLIC_ADDRESS_INDEX = 8;
+
+ /**
+ * The index of this message for indicating event group.
+ */
+ public static final int EVENT_GROUP_INDEX = 8;
+
+ /**
+ * The index of this message for indicating event code.
+ */
+ public static final int EVENT_CODE_INDEX = 9;
+
+ /**
+ * The index of this message for indicating the length of additional data of the
+ * event.
+ */
+ public static final int EVENT_ADDITIONAL_DATA_LENGTH_INDEX = 10;
+
+ /**
+ * The index of this message for indicating the event additional data start from.
+ */
+ public static final int EVENT_ADDITIONAL_DATA_INDEX = 11;
+
+ /**
+ * The index of this message for indicating the additional data type used in the
+ * following Additional Data characteristic.
+ */
+ public static final int ADDITIONAL_DATA_TYPE_INDEX = 10;
+
+ /**
+ * The type of this message for Key-based Pairing Request.
+ */
+ public static final byte TYPE_KEY_BASED_PAIRING_REQUEST = 0x00;
+
+ /**
+ * The bit indicating that the Fast Pair device should temporarily become
+ * discoverable.
+ */
+ public static final byte REQUEST_DISCOVERABLE = (byte) (1 << 7);
+
+ /**
+ * The bit indicating that the requester (Seeker) has included their public address
+ * in bytes [7,12] of the request, and the Provider should initiate bonding to that
+ * address.
+ */
+ public static final byte PROVIDER_INITIATES_BONDING = (byte) (1 << 6);
+
+ /**
+ * The bit indicating that Seeker requests Provider shall return the existing name.
+ */
+ public static final byte REQUEST_DEVICE_NAME = (byte) (1 << 5);
+
+ /**
+ * The bit to request retroactive pairing.
+ */
+ public static final byte REQUEST_RETROACTIVE_PAIR = (byte) (1 << 4);
+
+ /**
+ * The type of this message for action over BLE.
+ */
+ public static final byte TYPE_ACTION_OVER_BLE = 0x10;
+
+ private Request() {
+ }
+ }
+
+ /**
+ * Enumerates all flags of key-based pairing request.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ KeyBasedPairingRequestFlag.REQUEST_DISCOVERABLE,
+ KeyBasedPairingRequestFlag.PROVIDER_INITIATES_BONDING,
+ KeyBasedPairingRequestFlag.REQUEST_DEVICE_NAME,
+ KeyBasedPairingRequestFlag.REQUEST_RETROACTIVE_PAIR,
+ })
+ public @interface KeyBasedPairingRequestFlag {
+ /**
+ * The bit indicating that the Fast Pair device should temporarily become
+ * discoverable.
+ */
+ int REQUEST_DISCOVERABLE = (byte) (1 << 7);
+ /**
+ * The bit indicating that the requester (Seeker) has included their public address
+ * in bytes [7,12] of the request, and the Provider should initiate bonding to that
+ * address.
+ */
+ int PROVIDER_INITIATES_BONDING = (byte) (1 << 6);
+ /**
+ * The bit indicating that Seeker requests Provider shall return the existing name.
+ */
+ int REQUEST_DEVICE_NAME = (byte) (1 << 5);
+ /**
+ * The bit indicating that the Seeker request retroactive pairing.
+ */
+ int REQUEST_RETROACTIVE_PAIR = (byte) (1 << 4);
+ }
+
+ /**
+ * Enumerates all flags of action over BLE request, see Fast Pair spec for details.
+ */
+ @IntDef(
+ value = {
+ ActionOverBleFlag.DEVICE_ACTION,
+ ActionOverBleFlag.ADDITIONAL_DATA_CHARACTERISTIC,
+ })
+ public @interface ActionOverBleFlag {
+ /**
+ * The bit indicating that the handshaking is for Device Action.
+ */
+ int DEVICE_ACTION = (byte) (1 << 7);
+ /**
+ * The bit indicating that this handshake will be followed by Additional Data
+ * characteristic.
+ */
+ int ADDITIONAL_DATA_CHARACTERISTIC = (byte) (1 << 6);
+ }
+
+
+ /**
+ * Constants related to the decrypted response sent back in a notify.
+ */
+ public static final class Response {
+
+ /**
+ * The type of this message = Key-based Pairing Response.
+ */
+ public static final byte TYPE = 0x01;
+
+ private Response() {
+ }
+ }
+
+ private KeyBasedPairingCharacteristic() {
+ }
+ }
+
+ /**
+ * Characteristic used during Key-based Pairing, to exchange the encrypted passkey.
+ */
+ public static final class PasskeyCharacteristic {
+
+ private static final short SHORT_UUID = 0x1235;
+
+ /**
+ * Gets the new 128-bit UUID of this characteristic.
+ *
+ * <p>Note: For GATT server only. GATT client should use {@link
+ * PasskeyCharacteristic#getId(BluetoothGattConnection)}.
+ */
+ public static final UUID CUSTOM_128_BIT_UUID = toFastPair128BitUuid(SHORT_UUID);
+
+ /**
+ * Gets the {@link UUID} of this characteristic.
+ *
+ * <p>This method is designed for being backward compatible with old version of UUID
+ * therefore
+ * needs the {@link BluetoothGattConnection} parameter to check the supported status of
+ * the Fast Pair provider.
+ */
+ public static UUID getId(BluetoothGattConnection gattConnection) {
+ return getSupportedUuid(gattConnection, SHORT_UUID);
+ }
+
+ /**
+ * The type of the Passkey Block message.
+ */
+ @IntDef(
+ value = {
+ Type.SEEKER,
+ Type.PROVIDER,
+ })
+ public @interface Type {
+ /**
+ * Seeker's Passkey.
+ */
+ int SEEKER = (byte) 0x02;
+ /**
+ * Provider's Passkey.
+ */
+ int PROVIDER = (byte) 0x03;
+ }
+
+ /**
+ * Constructs the encrypted value to write to the characteristic.
+ */
+ public static byte[] encrypt(@Type int type, byte[] secret, int passkey)
+ throws GeneralSecurityException {
+ Preconditions.checkArgument(
+ 0 < passkey && passkey < /*2^24=*/ 16777216,
+ "Passkey %s must be positive and fit in 3 bytes",
+ passkey);
+ byte[] passkeyBytes =
+ new byte[]{(byte) (passkey >>> 16), (byte) (passkey >>> 8), (byte) passkey};
+ byte[] salt =
+ new byte[AesEcbSingleBlockEncryption.AES_BLOCK_LENGTH - 1
+ - passkeyBytes.length];
+ new Random().nextBytes(salt);
+ return AesEcbSingleBlockEncryption.encrypt(
+ secret, concat(new byte[]{(byte) type}, passkeyBytes, salt));
+ }
+
+ /**
+ * Extracts the passkey from the encrypted characteristic value.
+ */
+ public static int decrypt(@Type int type, byte[] secret,
+ byte[] passkeyCharacteristicValue)
+ throws GeneralSecurityException {
+ byte[] decrypted = AesEcbSingleBlockEncryption
+ .decrypt(secret, passkeyCharacteristicValue);
+ if (decrypted[0] != (byte) type) {
+ throw new GeneralSecurityException(
+ "Wrong Passkey Block type (expected " + type + ", got "
+ + decrypted[0] + ")");
+ }
+ return ByteBuffer.allocate(4)
+ .put((byte) 0)
+ .put(decrypted, /*offset=*/ 1, /*length=*/ 3)
+ .getInt(0);
+ }
+
+ private PasskeyCharacteristic() {
+ }
+ }
+
+ /**
+ * Characteristic to write to during the key exchange.
+ */
+ public static final class AccountKeyCharacteristic {
+
+ private static final short SHORT_UUID = 0x1236;
+
+ /**
+ * Gets the new 128-bit UUID of this characteristic.
+ *
+ * <p>Note: For GATT server only. GATT client should use {@link
+ * AccountKeyCharacteristic#getId(BluetoothGattConnection)}.
+ */
+ public static final UUID CUSTOM_128_BIT_UUID = toFastPair128BitUuid(SHORT_UUID);
+
+ /**
+ * Gets the {@link UUID} of this characteristic.
+ *
+ * <p>This method is designed for being backward compatible with old version of UUID
+ * therefore
+ * needs the {@link BluetoothGattConnection} parameter to check the supported status of
+ * the Fast Pair provider.
+ */
+ public static UUID getId(BluetoothGattConnection gattConnection) {
+ return getSupportedUuid(gattConnection, SHORT_UUID);
+ }
+
+ /**
+ * The type for this message, account key request.
+ */
+ public static final byte TYPE = 0x04;
+
+ private AccountKeyCharacteristic() {
+ }
+ }
+
+ /**
+ * Characteristic to write to and notify on for handling personalized name, see {@link
+ * NamingEncoder}.
+ */
+ public static final class NameCharacteristic {
+
+ private static final short SHORT_UUID = 0x1237;
+
+ /**
+ * Gets the new 128-bit UUID of this characteristic.
+ *
+ * <p>Note: For GATT server only. GATT client should use {@link
+ * NameCharacteristic#getId(BluetoothGattConnection)}.
+ */
+ public static final UUID CUSTOM_128_BIT_UUID = toFastPair128BitUuid(SHORT_UUID);
+
+ /**
+ * Gets the {@link UUID} of this characteristic.
+ *
+ * <p>This method is designed for being backward compatible with old version of UUID
+ * therefore
+ * needs the {@link BluetoothGattConnection} parameter to check the supported status of
+ * the Fast Pair provider.
+ */
+ public static UUID getId(BluetoothGattConnection gattConnection) {
+ return getSupportedUuid(gattConnection, SHORT_UUID);
+ }
+
+ private NameCharacteristic() {
+ }
+ }
+
+ /**
+ * Characteristic to write to and notify on for handling additional data, see
+ * https://developers.google.com/nearby/fast-pair/early-access/spec#AdditionalData
+ */
+ public static final class AdditionalDataCharacteristic {
+
+ private static final short SHORT_UUID = 0x1237;
+
+ public static final int DATA_ID_INDEX = 0;
+ public static final int DATA_LENGTH_INDEX = 1;
+ public static final int DATA_START_INDEX = 2;
+
+ /**
+ * Gets the new 128-bit UUID of this characteristic.
+ *
+ * <p>Note: For GATT server only. GATT client should use {@link
+ * AdditionalDataCharacteristic#getId(BluetoothGattConnection)}.
+ */
+ public static final UUID CUSTOM_128_BIT_UUID = toFastPair128BitUuid(SHORT_UUID);
+
+ /**
+ * Gets the {@link UUID} of this characteristic.
+ *
+ * <p>This method is designed for being backward compatible with old version of UUID
+ * therefore
+ * needs the {@link BluetoothGattConnection} parameter to check the supported status of
+ * the Fast Pair provider.
+ */
+ public static UUID getId(BluetoothGattConnection gattConnection) {
+ return getSupportedUuid(gattConnection, SHORT_UUID);
+ }
+
+ /**
+ * Enumerates all types of additional data.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ AdditionalDataType.PERSONALIZED_NAME,
+ AdditionalDataType.UNKNOWN,
+ })
+ public @interface AdditionalDataType {
+ /**
+ * The value indicating that the type is for personalized name.
+ */
+ int PERSONALIZED_NAME = (byte) 0x01;
+ int UNKNOWN = (byte) 0x00; // and all others.
+ }
+ }
+
+ /**
+ * Characteristic to control the beaconing feature (FastPair+Eddystone).
+ */
+ public static final class BeaconActionsCharacteristic {
+
+ private static final short SHORT_UUID = 0x1238;
+
+ /**
+ * Gets the new 128-bit UUID of this characteristic.
+ *
+ * <p>Note: For GATT server only. GATT client should use {@link
+ * BeaconActionsCharacteristic#getId(BluetoothGattConnection)}.
+ */
+ public static final UUID CUSTOM_128_BIT_UUID = toFastPair128BitUuid(SHORT_UUID);
+
+ /**
+ * Gets the {@link UUID} of this characteristic.
+ *
+ * <p>This method is designed for being backward compatible with old version of UUID
+ * therefore
+ * needs the {@link BluetoothGattConnection} parameter to check the supported status of
+ * the Fast Pair provider.
+ */
+ public static UUID getId(BluetoothGattConnection gattConnection) {
+ return getSupportedUuid(gattConnection, SHORT_UUID);
+ }
+
+ /**
+ * Enumerates all types of beacon actions.
+ */
+ /** Fast Pair Bond State. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ BeaconActionType.READ_BEACON_PARAMETERS,
+ BeaconActionType.READ_PROVISIONING_STATE,
+ BeaconActionType.SET_EPHEMERAL_IDENTITY_KEY,
+ BeaconActionType.CLEAR_EPHEMERAL_IDENTITY_KEY,
+ BeaconActionType.READ_EPHEMERAL_IDENTITY_KEY,
+ BeaconActionType.RING,
+ BeaconActionType.READ_RINGING_STATE,
+ BeaconActionType.UNKNOWN,
+ })
+ public @interface BeaconActionType {
+ int READ_BEACON_PARAMETERS = (byte) 0x00;
+ int READ_PROVISIONING_STATE = (byte) 0x01;
+ int SET_EPHEMERAL_IDENTITY_KEY = (byte) 0x02;
+ int CLEAR_EPHEMERAL_IDENTITY_KEY = (byte) 0x03;
+ int READ_EPHEMERAL_IDENTITY_KEY = (byte) 0x04;
+ int RING = (byte) 0x05;
+ int READ_RINGING_STATE = (byte) 0x06;
+ int UNKNOWN = (byte) 0xFF; // and all others
+ }
+
+ /** Converts value to enum. */
+ public static @BeaconActionType int valueOf(byte value) {
+ switch(value) {
+ case BeaconActionType.READ_BEACON_PARAMETERS:
+ case BeaconActionType.READ_PROVISIONING_STATE:
+ case BeaconActionType.SET_EPHEMERAL_IDENTITY_KEY:
+ case BeaconActionType.CLEAR_EPHEMERAL_IDENTITY_KEY:
+ case BeaconActionType.READ_EPHEMERAL_IDENTITY_KEY:
+ case BeaconActionType.RING:
+ case BeaconActionType.READ_RINGING_STATE:
+ case BeaconActionType.UNKNOWN:
+ return value;
+ default:
+ return BeaconActionType.UNKNOWN;
+ }
+ }
+ }
+
+
+ /**
+ * Characteristic to read for checking firmware version. 0X2A26 is assigned number from
+ * bluetooth SIG website.
+ */
+ public static final class FirmwareVersionCharacteristic {
+
+ /** UUID for firmware version. */
+ public static final UUID ID = to128BitUuid((short) 0x2A26);
+
+ private FirmwareVersionCharacteristic() {
+ }
+ }
+
+ private FastPairService() {
+ }
+ }
+
+ /**
+ * Defined by the BR/EDR Handover Profile. Pre-release version here:
+ * {https://jfarfel.users.x20web.corp.google.com/Bluetooth%20Handover%20d09.pdf}
+ */
+ public interface TransportDiscoveryService {
+
+ UUID ID = to128BitUuid((short) 0x1824);
+
+ byte BLUETOOTH_SIG_ORGANIZATION_ID = 0x01;
+ byte SERVICE_UUIDS_16_BIT_LIST_TYPE = 0x01;
+ byte SERVICE_UUIDS_32_BIT_LIST_TYPE = 0x02;
+ byte SERVICE_UUIDS_128_BIT_LIST_TYPE = 0x03;
+
+ /**
+ * Writing to this allows you to activate the BR/EDR transport.
+ */
+ interface ControlPointCharacteristic {
+
+ UUID ID = to128BitUuid((short) 0x2ABC);
+ byte ACTIVATE_TRANSPORT_OP_CODE = 0x01;
+ }
+
+ /**
+ * Info necessary to pair (mostly the Bluetooth Address).
+ */
+ interface BrHandoverDataCharacteristic {
+
+ UUID ID = to128BitUuid((short) 0x2C01);
+
+ /**
+ * All bits are reserved for future use.
+ */
+ byte BR_EDR_FEATURES = 0x00;
+ }
+
+ /**
+ * This characteristic exists only to wrap the descriptor.
+ */
+ interface BluetoothSigDataCharacteristic {
+
+ UUID ID = to128BitUuid((short) 0x2C02);
+
+ /**
+ * The entire Transport Block data (e.g. supported Bluetooth services).
+ */
+ interface BrTransportBlockDataDescriptor {
+
+ UUID ID = to128BitUuid((short) 0x2C03);
+ }
+ }
+ }
+
+ public static final UUID CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID =
+ to128BitUuid((short) 0x2902);
+
+ /**
+ * Wrapper for Bluetooth profile
+ */
+ public static class Profile {
+
+ public final int type;
+ public final String name;
+ public final String connectionStateAction;
+
+ private Profile(int type, String name, String connectionStateAction) {
+ this.type = type;
+ this.name = name;
+ this.connectionStateAction = connectionStateAction;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+
+ /**
+ * {@link BluetoothHeadset} is used for both Headset and HandsFree (HFP).
+ */
+ private static final Profile HEADSET_AND_HANDS_FREE_PROFILE =
+ new Profile(
+ HEADSET, "HEADSET_AND_HANDS_FREE",
+ BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+
+ /** Fast Pair supported profiles. */
+ public static final ImmutableMap<Short, Profile> PROFILES =
+ ImmutableMap.<Short, Profile>builder()
+ .put(
+ Constants.A2DP_SINK_SERVICE_UUID,
+ new Profile(A2DP, "A2DP",
+ BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED))
+ .put(Constants.HEADSET_SERVICE_UUID, HEADSET_AND_HANDS_FREE_PROFILE)
+ .put(Constants.HANDS_FREE_SERVICE_UUID, HEADSET_AND_HANDS_FREE_PROFILE)
+ .build();
+
+ static short[] getSupportedProfiles() {
+ return Shorts.toArray(PROFILES.keySet());
+ }
+
+ /**
+ * Helper method of getting 128-bit UUID for Fast Pair custom GATT characteristics.
+ *
+ * <p>This method is designed for being backward compatible with old version of UUID therefore
+ * needs the {@link BluetoothGattConnection} parameter to check the supported status of the Fast
+ * Pair provider.
+ *
+ * <p>Note: For new custom GATT characteristics, don't need to use this helper and please just
+ * call {@code toFastPair128BitUuid(shortUuid)} to get the UUID. Which also implies that callers
+ * don't need to provide {@link BluetoothGattConnection} to get the UUID anymore.
+ */
+ private static UUID getSupportedUuid(BluetoothGattConnection gattConnection, short shortUuid) {
+ // In worst case (new characteristic not found), this method's performance impact is about
+ // 6ms
+ // by using Pixel2 + JBL LIVE220. And the impact should be less and less along with more and
+ // more devices adopt the new characteristics.
+ try {
+ // Checks the new UUID first.
+ if (gattConnection
+ .getCharacteristic(FastPairService.ID, toFastPair128BitUuid(shortUuid))
+ != null) {
+ Log.d(TAG, "Uses new KeyBasedPairingCharacteristic.ID");
+ return toFastPair128BitUuid(shortUuid);
+ }
+ } catch (BluetoothException e) {
+ Log.d(TAG, "Uses old KeyBasedPairingCharacteristic.ID");
+ }
+ // Returns the old UUID for default.
+ return to128BitUuid(shortUuid);
+ }
+
+ private Constants() {
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/CreateBondException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/CreateBondException.java
new file mode 100644
index 0000000..d6aa3b2
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/CreateBondException.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import com.android.server.nearby.intdefs.FastPairEventIntDefs.CreateBondErrorCode;
+
+/** Thrown when binding (pairing) with a bluetooth device fails. */
+public class CreateBondException extends PairingException {
+ final @CreateBondErrorCode int mErrorCode;
+ int mReason;
+
+ CreateBondException(@CreateBondErrorCode int errorCode, int reason, String format,
+ Object... objects) {
+ super(format, objects);
+ this.mErrorCode = errorCode;
+ this.mReason = reason;
+ }
+
+ /** Returns error code. */
+ public @CreateBondErrorCode int getErrorCode() {
+ return mErrorCode;
+ }
+
+ /** Returns reason. */
+ public int getReason() {
+ return mReason;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiver.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiver.java
new file mode 100644
index 0000000..5bcf10a
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiver.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+/**
+ * Like {@link SimpleBroadcastReceiver}, but for intents about a certain {@link BluetoothDevice}.
+ */
+abstract class DeviceIntentReceiver extends SimpleBroadcastReceiver {
+
+ private static final String TAG = DeviceIntentReceiver.class.getSimpleName();
+
+ private final BluetoothDevice mDevice;
+
+ static DeviceIntentReceiver oneShotReceiver(
+ Context context, Preferences preferences, BluetoothDevice device, String... actions) {
+ return new DeviceIntentReceiver(context, preferences, device, actions) {
+ @Override
+ protected void onReceiveDeviceIntent(Intent intent) throws Exception {
+ close();
+ }
+ };
+ }
+
+ /**
+ * @param context The context to use to register / unregister the receiver.
+ * @param device The interesting device. We ignore intents about other devices.
+ * @param actions The actions to include in our intent filter.
+ */
+ protected DeviceIntentReceiver(
+ Context context, Preferences preferences, BluetoothDevice device, String... actions) {
+ super(context, preferences, actions);
+ this.mDevice = device;
+ }
+
+ /**
+ * Called with intents about the interesting device (see {@link #DeviceIntentReceiver}). Any
+ * exception thrown by this method will be delivered via {@link #await}.
+ */
+ protected abstract void onReceiveDeviceIntent(Intent intent) throws Exception;
+
+ // incompatible types in argument.
+ @Override
+ protected void onReceive(Intent intent) throws Exception {
+ BluetoothDevice intentDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ if (mDevice == null || mDevice.equals(intentDevice)) {
+ onReceiveDeviceIntent(intent);
+ } else {
+ Log.v(TAG,
+ "Ignoring intent for device=" + maskBluetoothAddress(intentDevice)
+ + "(expected "
+ + maskBluetoothAddress(mDevice) + ")");
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EllipticCurveDiffieHellmanExchange.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EllipticCurveDiffieHellmanExchange.java
new file mode 100644
index 0000000..dbcdf07
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EllipticCurveDiffieHellmanExchange.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.primitives.Bytes.concat;
+
+import androidx.annotation.Nullable;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPrivateKeySpec;
+import java.security.spec.ECPublicKeySpec;
+import java.util.Arrays;
+
+import javax.crypto.KeyAgreement;
+
+/**
+ * Helper for generating keys based off of the Elliptic-Curve Diffie-Hellman algorithm (ECDH).
+ */
+public final class EllipticCurveDiffieHellmanExchange {
+
+ public static final int PUBLIC_KEY_LENGTH = 64;
+ static final int PRIVATE_KEY_LENGTH = 32;
+
+ private static final String[] PROVIDERS = {"GmsCore_OpenSSL", "AndroidOpenSSL", "SC", "BC"};
+
+ private static final String EC_ALGORITHM = "EC";
+
+ /**
+ * Also known as prime256v1 or NIST P-256.
+ */
+ private static final ECGenParameterSpec EC_GEN_PARAMS = new ECGenParameterSpec("secp256r1");
+
+ @Nullable
+ private final ECPublicKey mPublicKey;
+ private final ECPrivateKey mPrivateKey;
+
+ /**
+ * Creates a new EllipticCurveDiffieHellmanExchange object.
+ */
+ public static EllipticCurveDiffieHellmanExchange create() throws GeneralSecurityException {
+ KeyPair keyPair = generateKeyPair();
+ return new EllipticCurveDiffieHellmanExchange(
+ (ECPublicKey) keyPair.getPublic(), (ECPrivateKey) keyPair.getPrivate());
+ }
+
+ /**
+ * Creates a new EllipticCurveDiffieHellmanExchange object.
+ */
+ public static EllipticCurveDiffieHellmanExchange create(byte[] privateKey)
+ throws GeneralSecurityException {
+ ECPrivateKey ecPrivateKey = (ECPrivateKey) generatePrivateKey(privateKey);
+ return new EllipticCurveDiffieHellmanExchange(/*publicKey=*/ null, ecPrivateKey);
+ }
+
+ private EllipticCurveDiffieHellmanExchange(
+ @Nullable ECPublicKey publicKey, ECPrivateKey privateKey) {
+ this.mPublicKey = publicKey;
+ this.mPrivateKey = privateKey;
+ }
+
+ /**
+ * @param otherPublicKey Another party's public key. See {@link #getPublicKey()} for format.
+ * @return The shared secret. Given our public key (and its private key), the other party can
+ * generate the same secret. This is a key meant for symmetric encryption.
+ */
+ public byte[] generateSecret(byte[] otherPublicKey) throws GeneralSecurityException {
+ KeyAgreement agreement = keyAgreement();
+ agreement.init(mPrivateKey);
+ agreement.doPhase(generatePublicKey(otherPublicKey), /*lastPhase=*/ true);
+ byte[] secret = agreement.generateSecret();
+ // Headsets only support AES with 128-bit keys. So, hash the secret so that the entropy is
+ // high and then take only the first 128-bits.
+ secret = MessageDigest.getInstance("SHA-256").digest(secret);
+ return Arrays.copyOf(secret, 16);
+ }
+
+ /**
+ * Returns a public point W on the NIST P-256 elliptic curve. First 32 bytes are the X
+ * coordinate, next 32 bytes are the Y coordinate. Each coordinate is an unsigned big-endian
+ * integer.
+ */
+ public @Nullable byte[] getPublicKey() {
+ if (mPublicKey == null) {
+ return null;
+ }
+ ECPoint w = mPublicKey.getW();
+ // See getPrivateKey for why we're resizing.
+ byte[] x = resizeWithLeadingZeros(w.getAffineX().toByteArray(), 32);
+ byte[] y = resizeWithLeadingZeros(w.getAffineY().toByteArray(), 32);
+ return concat(x, y);
+ }
+
+ /**
+ * Returns a private value S, an unsigned big-endian integer.
+ */
+ public byte[] getPrivateKey() {
+ // Note that BigInteger.toByteArray() returns a signed representation, so it will add an
+ // extra zero byte to the front if the first bit is 1.
+ // We must remove that leading zero (we know the number is unsigned). We must also add
+ // leading zeros if the number is too small.
+ return resizeWithLeadingZeros(mPrivateKey.getS().toByteArray(), 32);
+ }
+
+ /**
+ * Removes or adds leading zeros until we have an array of size {@code n}.
+ */
+ private static byte[] resizeWithLeadingZeros(byte[] x, int n) {
+ if (n < x.length) {
+ int start = x.length - n;
+ for (int i = 0; i < start; i++) {
+ if (x[i] != 0) {
+ throw new IllegalArgumentException(
+ "More than " + n + " non-zero bytes in " + Arrays.toString(x));
+ }
+ }
+ return Arrays.copyOfRange(x, start, x.length);
+ }
+ return concat(new byte[n - x.length], x);
+ }
+
+ /**
+ * @param publicKey See {@link #getPublicKey()} for format.
+ */
+ private static PublicKey generatePublicKey(byte[] publicKey) throws GeneralSecurityException {
+ if (publicKey.length != PUBLIC_KEY_LENGTH) {
+ throw new GeneralSecurityException("Public key length incorrect: " + publicKey.length);
+ }
+ byte[] x = Arrays.copyOf(publicKey, publicKey.length / 2);
+ byte[] y = Arrays.copyOfRange(publicKey, publicKey.length / 2, publicKey.length);
+ return keyFactory()
+ .generatePublic(
+ new ECPublicKeySpec(
+ new ECPoint(new BigInteger(/*signum=*/ 1, x),
+ new BigInteger(/*signum=*/ 1, y)),
+ ecParameterSpec()));
+ }
+
+ /**
+ * @param privateKey See {@link #getPrivateKey()} for format.
+ */
+ private static PrivateKey generatePrivateKey(byte[] privateKey)
+ throws GeneralSecurityException {
+ if (privateKey.length != PRIVATE_KEY_LENGTH) {
+ throw new GeneralSecurityException("Private key length incorrect: "
+ + privateKey.length);
+ }
+ return keyFactory()
+ .generatePrivate(
+ new ECPrivateKeySpec(new BigInteger(/*signum=*/ 1, privateKey),
+ ecParameterSpec()));
+ }
+
+ private static ECParameterSpec ecParameterSpec() throws GeneralSecurityException {
+ // This seems to be the simplest way to get the curve's ECParameterSpec. Verified that it's
+ // the same whether you get it from the public or private key, and that it's the same as the
+ // raw params in SecAggEcUtil.getNistP256Params().
+ return ((ECPublicKey) generateKeyPair().getPublic()).getParams();
+ }
+
+ private static KeyPair generateKeyPair() throws GeneralSecurityException {
+ KeyPairGenerator generator = findProvider(p -> KeyPairGenerator.getInstance(EC_ALGORITHM,
+ p));
+ generator.initialize(EC_GEN_PARAMS);
+ return generator.generateKeyPair();
+ }
+
+ private static KeyAgreement keyAgreement() throws NoSuchProviderException {
+ return findProvider(p -> KeyAgreement.getInstance("ECDH", p));
+ }
+
+ private static KeyFactory keyFactory() throws NoSuchProviderException {
+ return findProvider(p -> KeyFactory.getInstance(EC_ALGORITHM, p));
+ }
+
+ private interface ProviderConsumer<T> {
+
+ T tryProvider(String provider) throws NoSuchAlgorithmException, NoSuchProviderException;
+ }
+
+ private static <T> T findProvider(ProviderConsumer<T> providerConsumer)
+ throws NoSuchProviderException {
+ for (String provider : PROVIDERS) {
+ try {
+ return providerConsumer.tryProvider(provider);
+ } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
+ // No-op
+ }
+ }
+ throw new NoSuchProviderException();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Event.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Event.java
new file mode 100644
index 0000000..0b50dfd
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Event.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import android.bluetooth.BluetoothDevice;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode;
+
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * Describes events that are happening during fast pairing. EventCode is required, everything else
+ * is optional.
+ */
+public class Event implements Parcelable {
+
+ private final @EventCode int mEventCode;
+ private final long mTimestamp;
+ private final Short mProfile;
+ private final BluetoothDevice mBluetoothDevice;
+ private final Exception mException;
+
+ private Event(@EventCode int eventCode, long timestamp, @Nullable Short profile,
+ @Nullable BluetoothDevice bluetoothDevice, @Nullable Exception exception) {
+ mEventCode = eventCode;
+ mTimestamp = timestamp;
+ mProfile = profile;
+ mBluetoothDevice = bluetoothDevice;
+ mException = exception;
+ }
+
+ /**
+ * Returns event code.
+ */
+ public @EventCode int getEventCode() {
+ return mEventCode;
+ }
+
+ /**
+ * Returns timestamp.
+ */
+ public long getTimestamp() {
+ return mTimestamp;
+ }
+
+ /**
+ * Returns profile.
+ */
+ @Nullable
+ public Short getProfile() {
+ return mProfile;
+ }
+
+ /**
+ * Returns Bluetooth device.
+ */
+ @Nullable
+ public BluetoothDevice getBluetoothDevice() {
+ return mBluetoothDevice;
+ }
+
+ /**
+ * Returns exception.
+ */
+ @Nullable
+ public Exception getException() {
+ return mException;
+ }
+
+ /**
+ * Returns whether profile is not null.
+ */
+ public boolean hasProfile() {
+ return getProfile() != null;
+ }
+
+ /**
+ * Returns whether Bluetooth device is not null.
+ */
+ public boolean hasBluetoothDevice() {
+ return getBluetoothDevice() != null;
+ }
+
+ /**
+ * Returns a builder.
+ */
+ public static Builder builder() {
+ return new Event.Builder();
+ }
+
+ /**
+ * Returns whether it fails.
+ */
+ public boolean isFailure() {
+ return getException() != null;
+ }
+
+ @Override
+ public String toString() {
+ return "Event{"
+ + "eventCode=" + mEventCode + ", "
+ + "timestamp=" + mTimestamp + ", "
+ + "profile=" + mProfile + ", "
+ + "bluetoothDevice=" + mBluetoothDevice + ", "
+ + "exception=" + mException
+ + "}";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof Event) {
+ Event that = (Event) o;
+ return this.mEventCode == that.getEventCode()
+ && this.mTimestamp == that.getTimestamp()
+ && (this.mProfile == null
+ ? that.getProfile() == null : this.mProfile.equals(that.getProfile()))
+ && (this.mBluetoothDevice == null
+ ? that.getBluetoothDevice() == null :
+ this.mBluetoothDevice.equals(that.getBluetoothDevice()))
+ && (this.mException == null
+ ? that.getException() == null :
+ this.mException.equals(that.getException()));
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mEventCode, mTimestamp, mProfile, mBluetoothDevice, mException);
+ }
+
+
+ /**
+ * Builder
+ */
+ public static class Builder {
+ private @EventCode int mEventCode;
+ private long mTimestamp;
+ private Short mProfile;
+ private BluetoothDevice mBluetoothDevice;
+ private Exception mException;
+
+ /**
+ * Set event code.
+ */
+ public Builder setEventCode(@EventCode int eventCode) {
+ this.mEventCode = eventCode;
+ return this;
+ }
+
+ /**
+ * Set timestamp.
+ */
+ public Builder setTimestamp(long timestamp) {
+ this.mTimestamp = timestamp;
+ return this;
+ }
+
+ /**
+ * Set profile.
+ */
+ public Builder setProfile(@Nullable Short profile) {
+ this.mProfile = profile;
+ return this;
+ }
+
+ /**
+ * Set Bluetooth device.
+ */
+ public Builder setBluetoothDevice(@Nullable BluetoothDevice device) {
+ this.mBluetoothDevice = device;
+ return this;
+ }
+
+ /**
+ * Set exception.
+ */
+ public Builder setException(@Nullable Exception exception) {
+ this.mException = exception;
+ return this;
+ }
+
+ /**
+ * Builds event.
+ */
+ public Event build() {
+ return new Event(mEventCode, mTimestamp, mProfile, mBluetoothDevice, mException);
+ }
+ }
+
+ @Override
+ public final void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(getEventCode());
+ dest.writeLong(getTimestamp());
+ dest.writeValue(getProfile());
+ dest.writeParcelable(getBluetoothDevice(), 0);
+ dest.writeSerializable(getException());
+ }
+
+ @Override
+ public final int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Event Creator instance.
+ */
+ public static final Creator<Event> CREATOR =
+ new Creator<Event>() {
+ @Override
+ /** Creates Event from Parcel. */
+ public Event createFromParcel(Parcel in) {
+ return Event.builder()
+ .setEventCode(in.readInt())
+ .setTimestamp(in.readLong())
+ .setProfile((Short) in.readValue(Short.class.getClassLoader()))
+ .setBluetoothDevice(
+ in.readParcelable(BluetoothDevice.class.getClassLoader()))
+ .setException((Exception) in.readSerializable())
+ .build();
+ }
+
+ @Override
+ /** Returns Event array. */
+ public Event[] newArray(int size) {
+ return new Event[size];
+ }
+ };
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EventLogger.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EventLogger.java
new file mode 100644
index 0000000..4fc1917
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EventLogger.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+/** Logs events triggered during Fast Pairing. */
+public interface EventLogger {
+
+ /** Log successful event. */
+ void logEventSucceeded(Event event);
+
+ /** Log failed event. */
+ void logEventFailed(Event event, Exception e);
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EventLoggerWrapper.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EventLoggerWrapper.java
new file mode 100644
index 0000000..024bfde
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/EventLoggerWrapper.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+
+import com.android.server.nearby.common.bluetooth.fastpair.Preferences.ExtraLoggingInformation;
+import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode;
+
+import javax.annotation.Nullable;
+
+/**
+ * Convenience wrapper around EventLogger.
+ */
+// TODO(b/202559985): cleanup EventLoggerWrapper.
+class EventLoggerWrapper {
+
+ EventLoggerWrapper(@Nullable EventLogger eventLogger) {
+ }
+
+ /**
+ * Binds to the logging service. This operation blocks until binding has completed or timed
+ * out.
+ */
+ void bind(
+ Context context, String address,
+ @Nullable ExtraLoggingInformation extraLoggingInformation) {
+ }
+
+ boolean isBound() {
+ return false;
+ }
+
+ void unbind(Context context) {
+ }
+
+ void setCurrentEvent(@EventCode int code) {
+ }
+
+ void setCurrentProfile(short profile) {
+ }
+
+ void logCurrentEventFailed(Exception e) {
+ }
+
+ void logCurrentEventSucceeded() {
+ }
+
+ void setDevice(@Nullable BluetoothDevice device) {
+ }
+
+ boolean isCurrentEvent() {
+ return false;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConnection.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConnection.java
new file mode 100644
index 0000000..c963aa6
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConnection.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import android.annotation.WorkerThread;
+import android.bluetooth.BluetoothDevice;
+
+import androidx.annotation.Nullable;
+import androidx.core.util.Consumer;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/** Abstract class for pairing or connecting via FastPair. */
+public abstract class FastPairConnection {
+ @Nullable protected OnPairedCallback mPairedCallback;
+ @Nullable protected OnGetBluetoothAddressCallback mOnGetBluetoothAddressCallback;
+ @Nullable protected PasskeyConfirmationHandler mPasskeyConfirmationHandler;
+ @Nullable protected FastPairSignalChecker mFastPairSignalChecker;
+ @Nullable protected Consumer<Integer> mRescueFromError;
+ @Nullable protected Runnable mPrepareCreateBondCallback;
+ protected boolean mPasskeyIsGotten;
+
+ /** Sets a callback to be invoked once the device is paired. */
+ public void setOnPairedCallback(OnPairedCallback callback) {
+ this.mPairedCallback = callback;
+ }
+
+ /** Sets a callback to be invoked while the target bluetooth address is decided. */
+ public void setOnGetBluetoothAddressCallback(OnGetBluetoothAddressCallback callback) {
+ this.mOnGetBluetoothAddressCallback = callback;
+ }
+
+ /** Sets a callback to be invoked while handling the passkey confirmation. */
+ public void setPasskeyConfirmationHandler(
+ PasskeyConfirmationHandler passkeyConfirmationHandler) {
+ this.mPasskeyConfirmationHandler = passkeyConfirmationHandler;
+ }
+
+ public void setFastPairSignalChecker(FastPairSignalChecker fastPairSignalChecker) {
+ this.mFastPairSignalChecker = fastPairSignalChecker;
+ }
+
+ public void setRescueFromError(Consumer<Integer> rescueFromError) {
+ this.mRescueFromError = rescueFromError;
+ }
+
+ public void setPrepareCreateBondCallback(Runnable runnable) {
+ this.mPrepareCreateBondCallback = runnable;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ public Runnable getPrepareCreateBondCallback() {
+ return mPrepareCreateBondCallback;
+ }
+
+ /**
+ * Sets the fast pair history for identifying whether or not the provider has paired with the
+ * primary account on other phones before.
+ */
+ @WorkerThread
+ public abstract void setFastPairHistory(List<FastPairHistoryItem> fastPairHistoryItem);
+
+ /** Sets the device name to the Provider. */
+ public abstract void setProviderDeviceName(String deviceName);
+
+ /** Gets the device name from the Provider. */
+ @Nullable
+ public abstract String getProviderDeviceName();
+
+ /**
+ * Gets the existing account key of the Provider.
+ *
+ * @return the existing account key if the Provider has paired with the account, null otherwise
+ */
+ @WorkerThread
+ @Nullable
+ public abstract byte[] getExistingAccountKey();
+
+ /**
+ * Pairs with Provider. Synchronous: Blocks until paired and connected. Throws on any error.
+ *
+ * @return the secret key for the user's account, if written
+ */
+ @WorkerThread
+ @Nullable
+ public abstract SharedSecret pair()
+ throws BluetoothException, InterruptedException, TimeoutException, ExecutionException,
+ PairingException, ReflectionException;
+
+ /**
+ * Pairs with Provider. Synchronous: Blocks until paired and connected. Throws on any error.
+ *
+ * @param key can be in two different formats. If it is 16 bytes long, then it is an AES account
+ * key. Otherwise, it's a public key generated by {@link EllipticCurveDiffieHellmanExchange}.
+ * See go/fast-pair-2-spec for how each of these keys are used.
+ * @return the secret key for the user's account, if written
+ */
+ @WorkerThread
+ @Nullable
+ public abstract SharedSecret pair(@Nullable byte[] key)
+ throws BluetoothException, InterruptedException, TimeoutException, ExecutionException,
+ PairingException, GeneralSecurityException, ReflectionException;
+
+ /** Unpairs with Provider. Synchronous: Blocks until unpaired. Throws on any error. */
+ @WorkerThread
+ public abstract void unpair(BluetoothDevice device)
+ throws InterruptedException, TimeoutException, ExecutionException, PairingException,
+ ReflectionException;
+
+ /** Gets the public address of the Provider. */
+ @Nullable
+ public abstract String getPublicAddress();
+
+
+ /** Callback for getting notifications when pairing has completed. */
+ public interface OnPairedCallback {
+ /** Called when the device at address has finished pairing. */
+ void onPaired(String address);
+ }
+
+ /** Callback for getting bluetooth address Bisto oobe need this information */
+ public interface OnGetBluetoothAddressCallback {
+ /** Called when the device has received bluetooth address. */
+ void onGetBluetoothAddress(String address);
+ }
+
+ /** Holds the exchanged secret key and the public mac address of the device. */
+ public static class SharedSecret {
+ private final byte[] mKey;
+ private final String mAddress;
+ private SharedSecret(byte[] key, String address) {
+ mKey = key;
+ mAddress = address;
+ }
+
+ /** Creates Shared Secret. */
+ public static SharedSecret create(byte[] key, String address) {
+ return new SharedSecret(key, address);
+ }
+
+ /** Gets Shared Secret Key. */
+ public byte[] getKey() {
+ return mKey;
+ }
+
+ /** Gets Shared Secret Address. */
+ public String getAddress() {
+ return mAddress;
+ }
+
+ @Override
+ public String toString() {
+ return "SharedSecret{"
+ + "key=" + Arrays.toString(mKey) + ", "
+ + "address=" + mAddress
+ + "}";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof SharedSecret) {
+ SharedSecret that = (SharedSecret) o;
+ return Arrays.equals(this.mKey, that.getKey())
+ && this.mAddress.equals(that.getAddress());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(Arrays.hashCode(mKey), mAddress);
+ }
+ }
+
+ /** Invokes if gotten the passkey. */
+ public void setPasskeyIsGotten() {
+ mPasskeyIsGotten = true;
+ }
+
+ /** Returns the value of passkeyIsGotten. */
+ public boolean getPasskeyIsGotten() {
+ return mPasskeyIsGotten;
+ }
+
+ /** Interface to get latest address of ModelId. */
+ public interface FastPairSignalChecker {
+ /** Gets address of ModelId. */
+ String getValidAddressForModelId(String currentDevice);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConstants.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConstants.java
new file mode 100644
index 0000000..0ff1bf2
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConstants.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import android.bluetooth.BluetoothDevice;
+
+/** Constants to share with other team. */
+public class FastPairConstants {
+ private static final String PACKAGE_NAME = "com.android.server.nearby";
+ private static final String PREFIX = PACKAGE_NAME + ".common.bluetooth.fastpair.";
+
+ /** MODEL_ID item name for extended intent field. */
+ public static final String EXTRA_MODEL_ID = PREFIX + "MODEL_ID";
+ /** CONNECTION_ID item name for extended intent field. */
+ public static final String EXTRA_CONNECTION_ID = PREFIX + "CONNECTION_ID";
+ /** BLUETOOTH_MAC_ADDRESS item name for extended intent field. */
+ public static final String EXTRA_BLUETOOTH_MAC_ADDRESS = PREFIX + "BLUETOOTH_MAC_ADDRESS";
+ /** COMPANION_SCAN_ITEM item name for extended intent field. */
+ public static final String EXTRA_SCAN_ITEM = PREFIX + "COMPANION_SCAN_ITEM";
+ /** BOND_RESULT item name for extended intent field. */
+ public static final String EXTRA_BOND_RESULT = PREFIX + "EXTRA_BOND_RESULT";
+
+ /**
+ * The bond result of the {@link BluetoothDevice} when FastPair launches the companion app, it
+ * means device is BONDED but the pairing process is not triggered by FastPair.
+ */
+ public static final int BOND_RESULT_SUCCESS_WITHOUT_FP = 0;
+
+ /**
+ * The bond result of the {@link BluetoothDevice} when FastPair launches the companion app, it
+ * means device is BONDED and the pairing process is triggered by FastPair.
+ */
+ public static final int BOND_RESULT_SUCCESS_WITH_FP = 1;
+
+ /**
+ * The bond result of the {@link BluetoothDevice} when FastPair launches the companion app, it
+ * means the pairing process triggered by FastPair is failed due to the lack of PIN code.
+ */
+ public static final int BOND_RESULT_FAIL_WITH_FP_WITHOUT_PIN = 2;
+
+ /**
+ * The bond result of the {@link BluetoothDevice} when FastPair launches the companion app, it
+ * means the pairing process triggered by FastPair is failed due to the PIN code is not
+ * confirmed by the user.
+ */
+ public static final int BOND_RESULT_FAIL_WITH_FP_WITH_PIN_NOT_CONFIRMED = 3;
+
+ /**
+ * The bond result of the {@link BluetoothDevice} when FastPair launches the companion app, it
+ * means the pairing process triggered by FastPair is failed due to the user thinks the PIN is
+ * wrong.
+ */
+ public static final int BOND_RESULT_FAIL_WITH_FP_WITH_PIN_WRONG = 4;
+
+ /**
+ * The bond result of the {@link BluetoothDevice} when FastPair launches the companion app, it
+ * means the pairing process triggered by FastPair is failed even after the user confirmed the
+ * PIN code is correct.
+ */
+ public static final int BOND_RESULT_FAIL_WITH_FP_WITH_PIN_CORRECT = 5;
+
+ private FastPairConstants() {}
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java
new file mode 100644
index 0000000..789ef59
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java
@@ -0,0 +1,2127 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static android.bluetooth.BluetoothDevice.BOND_BONDED;
+import static android.bluetooth.BluetoothDevice.BOND_BONDING;
+import static android.bluetooth.BluetoothDevice.BOND_NONE;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.get16BitUuid;
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.to128BitUuid;
+import static com.android.server.nearby.common.bluetooth.fastpair.Bytes.toBytes;
+import static com.android.server.nearby.common.bluetooth.fastpair.Bytes.toShorts;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Verify.verifyNotNull;
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.primitives.Bytes.concat;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.ParcelUuid;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.BluetoothGattException;
+import com.android.server.nearby.common.bluetooth.BluetoothTimeoutException;
+import com.android.server.nearby.common.bluetooth.fastpair.BluetoothAudioPairer.KeyBasedPairingInfo;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AccountKeyCharacteristic;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AdditionalDataCharacteristic.AdditionalDataType;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.FirmwareVersionCharacteristic;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.NameCharacteristic;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.TransportDiscoveryService;
+import com.android.server.nearby.common.bluetooth.fastpair.HandshakeHandler.ActionOverBle;
+import com.android.server.nearby.common.bluetooth.fastpair.HandshakeHandler.HandshakeException;
+import com.android.server.nearby.common.bluetooth.fastpair.HandshakeHandler.HandshakeMessage;
+import com.android.server.nearby.common.bluetooth.fastpair.HandshakeHandler.KeyBasedPairingRequest;
+import com.android.server.nearby.common.bluetooth.fastpair.Ltv.ParseException;
+import com.android.server.nearby.common.bluetooth.fastpair.TimingLogger.ScopedTiming;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection.ChangeObserver;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.fastpair.FastPairController;
+import com.android.server.nearby.intdefs.FastPairEventIntDefs.BrEdrHandoverErrorCode;
+import com.android.server.nearby.intdefs.FastPairEventIntDefs.ConnectErrorCode;
+import com.android.server.nearby.intdefs.FastPairEventIntDefs.CreateBondErrorCode;
+import com.android.server.nearby.intdefs.FastPairEventIntDefs.ErrorCode;
+import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode;
+
+import com.google.common.base.Ascii;
+import com.google.common.base.Preconditions;
+import com.google.common.primitives.Shorts;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteOrder;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Supports Fast Pair pairing with certain Bluetooth headphones, Auto, etc.
+ *
+ * <p>Based on https://developers.google.com/nearby/fast-pair/spec, the pairing is constructed by
+ * both BLE and BREDR connections. Example state transitions for Fast Pair 2, ie a pairing key is
+ * included in the request (note: timeouts and retries are governed by flags, may change):
+ *
+ * <pre>
+ * {@code
+ * Connect GATT
+ * A) Success -> Handshake
+ * B) Failure (3s timeout) -> Retry 2x -> end
+ *
+ * Handshake
+ * A) Generate a shared secret with the headset (either using anti-spoofing key or account key)
+ * 1) Account key is used directly as the key
+ * 2) Anti-spoofing key is used by combining out private key with the headset's public and
+ * sending our public to the headset to combine with their private to generate a shared
+ * key. Sending our public key to headset takes ~3s.
+ * B) Write an encrypted packet to the headset containing their BLE address for verification
+ * that both sides have the same key (headset decodes this packet and checks it against their
+ * own address) (~250ms).
+ * C) Receive a response from the headset containing their public address (~250ms).
+ *
+ * Discovery (for devices < Oreo)
+ * A) Success -> Create Bond
+ * B) Failure (10s timeout) -> Sleep 1s, Retry 3x -> end
+ *
+ * Connect to device
+ * A) If already bonded
+ * 1) Attempt directly connecting to supported profiles (A2DP, etc)
+ * a) Success -> Write Account Key
+ * b) Failure (15s timeout, usually fails within a ~2s) -> Remove bond (~1s) -> Create bond
+ * B) If not already bonded
+ * 1) Create bond
+ * a) Success -> Connect profile
+ * b) Failure (15s timeout) -> Retry 2x -> end
+ * 2) Connect profile
+ * a) Success -> Write account key
+ * b) Failure -> Retry -> end
+ *
+ * Write account key
+ * A) Callback that pairing succeeded
+ * B) Disconnect GATT
+ * C) Reconnect GATT for secure connection
+ * D) Write account key (~3s)
+ * }
+ * </pre>
+ *
+ * The performance profiling result by {@link TimingLogger}:
+ *
+ * <pre>
+ * FastPairDualConnection [Exclusive time] / [Total time] ([Timestamp])
+ * Connect GATT #1 3054ms (0)
+ * Handshake 32ms / 740ms (3054)
+ * Generate key via ECDH 10ms (3054)
+ * Add salt 1ms (3067)
+ * Encrypt request 3ms (3068)
+ * Write data to GATT 692ms (3097)
+ * Wait response from GATT 0ms (3789)
+ * Decrypt response 2ms (3789)
+ * Get BR/EDR handover information via SDP 1ms (3795)
+ * Pair device #1 6ms / 4887ms (3805)
+ * Create bond 3965ms / 4881ms (3809)
+ * Exchange passkey 587ms / 915ms (7124)
+ * Encrypt passkey 6ms (7694)
+ * Send passkey to remote 290ms (7700)
+ * Wait for remote passkey 0ms (7993)
+ * Decrypt passkey 18ms (7994)
+ * Confirm the pairing: true 14ms (8025)
+ * Close BondedReceiver 1ms (8688)
+ * Connect: A2DP 19ms / 370ms (8701)
+ * Wait connection 348ms / 349ms (8720)
+ * Close ConnectedReceiver 1ms (9068)
+ * Close profile: A2DP 2ms (9069)
+ * Write account key 2ms / 789ms (9163)
+ * Encrypt key 0ms (9164)
+ * Write key via GATT #1 777ms / 783ms (9164)
+ * Close GATT 6ms (9941)
+ * Start CloudSyncing 2ms (9947)
+ * Broadcast Validator 2ms (9949)
+ * FastPairDualConnection end, 9952ms
+ * </pre>
+ */
+// TODO(b/203441105): break down FastPairDualConnection into smaller classes.
+public class FastPairDualConnection extends FastPairConnection {
+
+ private static final String TAG = FastPairDualConnection.class.getSimpleName();
+
+ @VisibleForTesting
+ static final int GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST = 10000;
+ @VisibleForTesting
+ static final int GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED = 20000;
+ @VisibleForTesting
+ static final int GATT_ERROR_CODE_USER_RETRY = 30000;
+ @VisibleForTesting
+ static final int GATT_ERROR_CODE_PAIR_WITH_SAME_MODEL_ID_COUNT = 40000;
+ @VisibleForTesting
+ static final int GATT_ERROR_CODE_TIMEOUT = 1000;
+
+ @Nullable
+ private static String sInitialConnectionFirmwareVersion;
+ private static final byte[] REQUESTED_SERVICES_LTV =
+ new Ltv(
+ TransportDiscoveryService.SERVICE_UUIDS_16_BIT_LIST_TYPE,
+ toBytes(
+ ByteOrder.LITTLE_ENDIAN,
+ Constants.A2DP_SINK_SERVICE_UUID,
+ Constants.HANDS_FREE_SERVICE_UUID,
+ Constants.HEADSET_SERVICE_UUID))
+ .getBytes();
+ private static final byte[] TDS_CONTROL_POINT_REQUEST =
+ concat(
+ new byte[]{
+ TransportDiscoveryService.ControlPointCharacteristic
+ .ACTIVATE_TRANSPORT_OP_CODE,
+ TransportDiscoveryService.BLUETOOTH_SIG_ORGANIZATION_ID
+ },
+ REQUESTED_SERVICES_LTV);
+
+ private static boolean sTestMode = false;
+
+ static void enableTestMode() {
+ sTestMode = true;
+ }
+
+ /**
+ * Operation Result Code.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ ResultCode.UNKNOWN,
+ ResultCode.SUCCESS,
+ ResultCode.OP_CODE_NOT_SUPPORTED,
+ ResultCode.INVALID_PARAMETER,
+ ResultCode.UNSUPPORTED_ORGANIZATION_ID,
+ ResultCode.OPERATION_FAILED,
+ })
+
+ public @interface ResultCode {
+
+ int UNKNOWN = (byte) 0xFF;
+ int SUCCESS = (byte) 0x00;
+ int OP_CODE_NOT_SUPPORTED = (byte) 0x01;
+ int INVALID_PARAMETER = (byte) 0x02;
+ int UNSUPPORTED_ORGANIZATION_ID = (byte) 0x03;
+ int OPERATION_FAILED = (byte) 0x04;
+ }
+
+
+ private static @ResultCode int fromTdsControlPointIndication(byte[] response) {
+ return response == null || response.length < 2 ? ResultCode.UNKNOWN : from(response[1]);
+ }
+
+ private static @ResultCode int from(byte byteValue) {
+ switch (byteValue) {
+ case ResultCode.UNKNOWN:
+ case ResultCode.SUCCESS:
+ case ResultCode.OP_CODE_NOT_SUPPORTED:
+ case ResultCode.INVALID_PARAMETER:
+ case ResultCode.UNSUPPORTED_ORGANIZATION_ID:
+ case ResultCode.OPERATION_FAILED:
+ return byteValue;
+ default:
+ return ResultCode.UNKNOWN;
+ }
+ }
+
+ private static class BrEdrHandoverInformation {
+
+ private final byte[] mBluetoothAddress;
+ private final short[] mProfiles;
+
+ private BrEdrHandoverInformation(byte[] bluetoothAddress, short[] profiles) {
+ this.mBluetoothAddress = bluetoothAddress;
+
+ // For now, since we only connect to one profile, prefer A2DP Sink over headset/HFP.
+ // TODO(b/37167120): Connect to more than one profile.
+ Set<Short> profileSet = new HashSet<>(Shorts.asList(profiles));
+ if (profileSet.contains(Constants.A2DP_SINK_SERVICE_UUID)) {
+ profileSet.remove(Constants.HEADSET_SERVICE_UUID);
+ profileSet.remove(Constants.HANDS_FREE_SERVICE_UUID);
+ }
+ this.mProfiles = Shorts.toArray(profileSet);
+ }
+
+ @Override
+ public String toString() {
+ return "BrEdrHandoverInformation{"
+ + maskBluetoothAddress(BluetoothAddress.encode(mBluetoothAddress))
+ + ", profiles="
+ + (mProfiles.length > 0 ? Shorts.join(",", mProfiles) : "(none)")
+ + "}";
+ }
+ }
+
+ private final Context mContext;
+ private final Preferences mPreferences;
+ private final EventLoggerWrapper mEventLogger;
+ private final BluetoothAdapter mBluetoothAdapter =
+ checkNotNull(BluetoothAdapter.getDefaultAdapter());
+ private String mBleAddress;
+
+ private final TimingLogger mTimingLogger;
+ private GattConnectionManager mGattConnectionManager;
+ private boolean mProviderInitiatesBonding;
+ private @Nullable
+ byte[] mPairingSecret;
+ private @Nullable
+ byte[] mPairingKey;
+ @Nullable
+ private String mPublicAddress;
+ @VisibleForTesting
+ @Nullable
+ FastPairHistoryFinder mPairedHistoryFinder;
+ @Nullable
+ private String mProviderDeviceName = null;
+ private boolean mNeedUpdateProviderName = false;
+ @Nullable
+ DeviceNameReceiver mDeviceNameReceiver;
+ @Nullable
+ private HandshakeHandler mHandshakeHandlerForTest;
+ @Nullable
+ private Runnable mBeforeDirectlyConnectProfileFromCacheForTest;
+
+ public FastPairDualConnection(
+ Context context,
+ String bleAddress,
+ Preferences preferences,
+ @Nullable EventLogger eventLogger) {
+ this(context, bleAddress, preferences, eventLogger,
+ new TimingLogger("FastPairDualConnection", preferences));
+ }
+
+ @VisibleForTesting
+ FastPairDualConnection(
+ Context context,
+ String bleAddress,
+ Preferences preferences,
+ @Nullable EventLogger eventLogger,
+ TimingLogger timingLogger) {
+ this.mContext = context;
+ this.mPreferences = preferences;
+ this.mEventLogger = new EventLoggerWrapper(eventLogger);
+ this.mBleAddress = bleAddress;
+ this.mTimingLogger = timingLogger;
+ }
+
+ /**
+ * Unpairs with headphones. Synchronous: Blocks until unpaired. Throws on any error.
+ */
+ @WorkerThread
+ public void unpair(BluetoothDevice device)
+ throws ReflectionException, InterruptedException, ExecutionException, TimeoutException,
+ PairingException {
+ if (mPreferences.getExtraLoggingInformation() != null) {
+ mEventLogger
+ .bind(mContext, device.getAddress(), mPreferences.getExtraLoggingInformation());
+ }
+ new BluetoothAudioPairer(
+ mContext,
+ device,
+ mPreferences,
+ mEventLogger,
+ /* keyBasedPairingInfo= */ null,
+ /* passkeyConfirmationHandler= */ null,
+ mTimingLogger)
+ .unpair();
+ if (mEventLogger.isBound()) {
+ mEventLogger.unbind(mContext);
+ }
+ }
+
+ /**
+ * Sets the fast pair history for identifying the provider which has paired (without being
+ * forgotten) with the primary account on the device, i.e. the history is not limited on this
+ * phone, can be on other phones with the same account. If they have already paired, Fast Pair
+ * should not generate new account key and default personalized name for it after initial pair.
+ */
+ @WorkerThread
+ public void setFastPairHistory(List<FastPairHistoryItem> fastPairHistoryItem) {
+ Log.i(TAG, "Paired history has been set.");
+ this.mPairedHistoryFinder = new FastPairHistoryFinder(fastPairHistoryItem);
+ }
+
+ /**
+ * Update the provider device name when we take provider default name and account based name
+ * into consideration.
+ */
+ public void setProviderDeviceName(String deviceName) {
+ Log.i(TAG, "Update provider device name = " + deviceName);
+ mProviderDeviceName = deviceName;
+ mNeedUpdateProviderName = true;
+ }
+
+ /**
+ * Gets the device name from the Provider (via GATT notify).
+ */
+ @Nullable
+ public String getProviderDeviceName() {
+ if (mDeviceNameReceiver == null) {
+ Log.i(TAG, "getProviderDeviceName failed, deviceNameReceiver == null.");
+ return null;
+ }
+ if (mPairingSecret == null) {
+ Log.i(TAG, "getProviderDeviceName failed, pairingSecret == null.");
+ return null;
+ }
+ String deviceName = mDeviceNameReceiver.getParsedResult(mPairingSecret);
+ Log.i(TAG, "getProviderDeviceName = " + deviceName);
+
+ return deviceName;
+ }
+
+ /**
+ * Get the existing account key of the provider, this API can be called after handshake.
+ *
+ * @return the existing account key if the provider has paired with the account before.
+ * Otherwise, return null, i.e. it is a real initial pairing.
+ */
+ @WorkerThread
+ @Nullable
+ public byte[] getExistingAccountKey() {
+ return mPairedHistoryFinder == null ? null : mPairedHistoryFinder.getExistingAccountKey();
+ }
+
+ /**
+ * Pairs with headphones. Synchronous: Blocks until paired and connected. Throws on any error.
+ *
+ * @return the secret key for the user's account, if written.
+ */
+ @WorkerThread
+ @Nullable
+ public SharedSecret pair()
+ throws BluetoothException, InterruptedException, ReflectionException, TimeoutException,
+ ExecutionException, PairingException {
+ try {
+ return pair(/*key=*/ null);
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException("Should never happen, no security key!", e);
+ }
+ }
+
+ /**
+ * Pairs with headphones. Synchronous: Blocks until paired and connected. Throws on any error.
+ *
+ * @param key can be in two different formats. If it is 16 bytes long, then it is an AES account
+ * key. Otherwise, it's a public key generated by {@link EllipticCurveDiffieHellmanExchange}.
+ * See go/fast-pair-2-spec for how each of these keys are used.
+ * @return the secret key for the user's account, if written
+ */
+ @WorkerThread
+ @Nullable
+ public SharedSecret pair(@Nullable byte[] key)
+ throws BluetoothException, InterruptedException, ReflectionException, TimeoutException,
+ ExecutionException, PairingException, GeneralSecurityException {
+ mPairingKey = key;
+ if (key != null) {
+ Log.i(TAG, "Starting to pair " + maskBluetoothAddress(mBleAddress) + ": key["
+ + key.length + "], " + mPreferences);
+ } else {
+ Log.i(TAG, "Pairing " + maskBluetoothAddress(mBleAddress) + ": " + mPreferences);
+ }
+ if (mPreferences.getExtraLoggingInformation() != null) {
+ this.mEventLogger.bind(
+ mContext, mBleAddress, mPreferences.getExtraLoggingInformation());
+ }
+ // Provider never initiates if key is null (Fast Pair 1.0).
+ if (key != null && mPreferences.getProviderInitiatesBondingIfSupported()) {
+ // Provider can't initiate if we can't get our own public address, so check.
+ this.mEventLogger.setCurrentEvent(EventCode.GET_LOCAL_PUBLIC_ADDRESS);
+ if (BluetoothAddress.getPublicAddress(mContext) != null) {
+ this.mEventLogger.logCurrentEventSucceeded();
+ mProviderInitiatesBonding = true;
+ } else {
+ this.mEventLogger
+ .logCurrentEventFailed(new IllegalStateException("null bluetooth_address"));
+ Log.e(TAG,
+ "Want provider to initiate bonding, but cannot access Bluetooth public "
+ + "address. Falling back to initiating bonding ourselves.");
+ }
+ }
+
+ // User might be pairing with a bonded device. In this case, we just connect profile
+ // directly and finish pairing.
+ if (directConnectProfileWithCachedAddress()) {
+ callbackOnPaired();
+ mTimingLogger.dump();
+ if (mEventLogger.isBound()) {
+ mEventLogger.unbind(mContext);
+ }
+ return null;
+ }
+
+ // Lazily initialize a new connection manager for each pairing request.
+ initGattConnectionManager();
+ boolean isSecretHandshakeCompleted = true;
+
+ try {
+ if (key != null && key.length > 0) {
+ // GATT_CONNECTION_AND_SECRET_HANDSHAKE start.
+ mEventLogger.setCurrentEvent(EventCode.GATT_CONNECTION_AND_SECRET_HANDSHAKE);
+ isSecretHandshakeCompleted = false;
+ Exception lastException = null;
+ boolean lastExceptionFromHandshake = false;
+ long startTime = SystemClock.elapsedRealtime();
+ // We communicate over this connection twice for Key-based Pairing: once before
+ // bonding begins, and once during (to transfer the passkey). Empirically, keeping
+ // it alive throughout is far more reliable than disconnecting and reconnecting for
+ // each step. The while loop is for retry of GATT connection and handshake only.
+ do {
+ boolean isHandshaking = false;
+ try (BluetoothGattConnection connection =
+ mGattConnectionManager
+ .getConnectionWithSignalLostCheck(mRescueFromError)) {
+ mEventLogger.setCurrentEvent(EventCode.SECRET_HANDSHAKE);
+ if (lastException != null && !lastExceptionFromHandshake) {
+ logRetrySuccessEvent(EventCode.RECOVER_BY_RETRY_GATT, lastException,
+ mEventLogger);
+ lastException = null;
+ }
+ try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
+ "Handshake")) {
+ isHandshaking = true;
+ handshakeForKeyBasedPairing(key);
+ // After handshake, Fast Pair has the public address of the provider, so
+ // we can check if it has paired with the account.
+ if (mPublicAddress != null && mPairedHistoryFinder != null) {
+ if (mPairedHistoryFinder.isInPairedHistory(mPublicAddress)) {
+ Log.i(TAG, "The provider is found in paired history.");
+ } else {
+ Log.i(TAG, "The provider is not found in paired history.");
+ }
+ }
+ }
+ isHandshaking = false;
+ // SECRET_HANDSHAKE end.
+ mEventLogger.logCurrentEventSucceeded();
+ isSecretHandshakeCompleted = true;
+ if (mPrepareCreateBondCallback != null) {
+ mPrepareCreateBondCallback.run();
+ }
+ if (lastException != null && lastExceptionFromHandshake) {
+ logRetrySuccessEvent(EventCode.RECOVER_BY_RETRY_HANDSHAKE_RECONNECT,
+ lastException, mEventLogger);
+ }
+ logManualRetryCounts(/* success= */ true);
+ // GATT_CONNECTION_AND_SECRET_HANDSHAKE end.
+ mEventLogger.logCurrentEventSucceeded();
+ return pair(mPreferences.getEnableBrEdrHandover());
+ } catch (SignalLostException e) {
+ long spentTime = SystemClock.elapsedRealtime() - startTime;
+ if (spentTime > mPreferences.getAddressRotateRetryMaxSpentTimeMs()) {
+ Log.w(TAG, "Signal lost but already spend too much time " + spentTime
+ + "ms");
+ throw e;
+ }
+
+ logCurrentEventFailedBySignalLost(e);
+ lastException = (Exception) e.getCause();
+ lastExceptionFromHandshake = isHandshaking;
+ if (mRescueFromError != null && isHandshaking) {
+ mRescueFromError.accept(ErrorCode.SUCCESS_SECRET_HANDSHAKE_RECONNECT);
+ }
+ Log.i(TAG, "Signal lost, retry");
+ // In case we meet some GATT error which is not recoverable and fail very
+ // quick.
+ SystemClock.sleep(mPreferences.getPairingRetryDelayMs());
+ } catch (SignalRotatedException e) {
+ long spentTime = SystemClock.elapsedRealtime() - startTime;
+ if (spentTime > mPreferences.getAddressRotateRetryMaxSpentTimeMs()) {
+ Log.w(TAG, "Address rotated but already spend too much time "
+ + spentTime + "ms");
+ throw e;
+ }
+
+ logCurrentEventFailedBySignalRotated(e);
+ setBleAddress(e.getNewAddress());
+ lastException = (Exception) e.getCause();
+ lastExceptionFromHandshake = isHandshaking;
+ if (mRescueFromError != null) {
+ mRescueFromError.accept(ErrorCode.SUCCESS_ADDRESS_ROTATE);
+ }
+ Log.i(TAG, "Address rotated, retry");
+ } catch (HandshakeException e) {
+ long spentTime = SystemClock.elapsedRealtime() - startTime;
+ if (spentTime > mPreferences
+ .getSecretHandshakeRetryGattConnectionMaxSpentTimeMs()) {
+ Log.w(TAG, "Secret handshake failed but already spend too much time "
+ + spentTime + "ms");
+ throw e.getOriginalException();
+ }
+ if (mEventLogger.isCurrentEvent()) {
+ mEventLogger.logCurrentEventFailed(e.getOriginalException());
+ }
+ initGattConnectionManager();
+ lastException = e.getOriginalException();
+ lastExceptionFromHandshake = true;
+ if (mRescueFromError != null) {
+ mRescueFromError.accept(ErrorCode.SUCCESS_SECRET_HANDSHAKE_RECONNECT);
+ }
+ Log.i(TAG, "Handshake failed, retry GATT connection");
+ }
+ } while (mPreferences.getRetryGattConnectionAndSecretHandshake());
+ }
+ if (mPrepareCreateBondCallback != null) {
+ mPrepareCreateBondCallback.run();
+ }
+ return pair(mPreferences.getEnableBrEdrHandover());
+ } catch (SignalLostException e) {
+ logCurrentEventFailedBySignalLost(e);
+ // GATT_CONNECTION_AND_SECRET_HANDSHAKE end.
+ if (!isSecretHandshakeCompleted) {
+ logManualRetryCounts(/* success= */ false);
+ logCurrentEventFailedBySignalLost(e);
+ }
+ throw e;
+ } catch (SignalRotatedException e) {
+ logCurrentEventFailedBySignalRotated(e);
+ // GATT_CONNECTION_AND_SECRET_HANDSHAKE end.
+ if (!isSecretHandshakeCompleted) {
+ logManualRetryCounts(/* success= */ false);
+ logCurrentEventFailedBySignalRotated(e);
+ }
+ throw e;
+ } catch (BluetoothException
+ | InterruptedException
+ | ReflectionException
+ | TimeoutException
+ | ExecutionException
+ | PairingException
+ | GeneralSecurityException e) {
+ if (mEventLogger.isCurrentEvent()) {
+ mEventLogger.logCurrentEventFailed(e);
+ }
+ // GATT_CONNECTION_AND_SECRET_HANDSHAKE end.
+ if (!isSecretHandshakeCompleted) {
+ logManualRetryCounts(/* success= */ false);
+ if (mEventLogger.isCurrentEvent()) {
+ mEventLogger.logCurrentEventFailed(e);
+ }
+ }
+ throw e;
+ } finally {
+ mTimingLogger.dump();
+ if (mEventLogger.isBound()) {
+ mEventLogger.unbind(mContext);
+ }
+ }
+ }
+
+ private boolean directConnectProfileWithCachedAddress() throws ReflectionException {
+ if (TextUtils.isEmpty(mPreferences.getCachedDeviceAddress())
+ || !mPreferences.getDirectConnectProfileIfModelIdInCache()
+ || mPreferences.getSkipConnectingProfiles()) {
+ return false;
+ }
+ Log.i(TAG, "Try to direct connect profile with cached address "
+ + maskBluetoothAddress(mPreferences.getCachedDeviceAddress()));
+ mEventLogger.setCurrentEvent(EventCode.DIRECTLY_CONNECT_PROFILE_WITH_CACHED_ADDRESS);
+ BluetoothDevice device =
+ mBluetoothAdapter.getRemoteDevice(mPreferences.getCachedDeviceAddress()).unwrap();
+ AtomicBoolean interruptConnection = new AtomicBoolean(false);
+ BroadcastReceiver receiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null
+ || !BluetoothDevice.ACTION_PAIRING_REQUEST
+ .equals(intent.getAction())) {
+ return;
+ }
+ BluetoothDevice pairingDevice = intent
+ .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ if (pairingDevice == null || !device.getAddress()
+ .equals(pairingDevice.getAddress())) {
+ return;
+ }
+ abortBroadcast();
+ // Should be the clear link key case, make it fail directly to go back to
+ // initial pairing process.
+ pairingDevice.setPairingConfirmation(/* confirm= */ false);
+ Log.w(TAG, "Get pairing request broadcast for device "
+ + maskBluetoothAddress(device.getAddress())
+ + " while try to direct connect profile with cached address, reject"
+ + " and to go back to initial pairing process");
+ interruptConnection.set(true);
+ }
+ };
+ mContext.registerReceiver(receiver,
+ new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST));
+ try (ScopedTiming scopedTiming =
+ new ScopedTiming(mTimingLogger,
+ "Connect to profile with cached address directly")) {
+ if (mBeforeDirectlyConnectProfileFromCacheForTest != null) {
+ mBeforeDirectlyConnectProfileFromCacheForTest.run();
+ }
+ attemptConnectProfiles(
+ new BluetoothAudioPairer(
+ mContext,
+ device,
+ mPreferences,
+ mEventLogger,
+ /* keyBasedPairingInfo= */ null,
+ /* passkeyConfirmationHandler= */ null,
+ mTimingLogger),
+ maskBluetoothAddress(device),
+ getSupportedProfiles(device),
+ /* numConnectionAttempts= */ 1,
+ /* enablePairingBehavior= */ false,
+ interruptConnection);
+ Log.i(TAG,
+ "Directly connected to " + maskBluetoothAddress(device)
+ + "with cached address.");
+ mEventLogger.logCurrentEventSucceeded();
+ mEventLogger.setDevice(device);
+ logPairWithPossibleCachedAddress(device.getAddress());
+ return true;
+ } catch (PairingException e) {
+ if (interruptConnection.get()) {
+ Log.w(TAG, "Fail to connected to " + maskBluetoothAddress(device)
+ + " with cached address due to link key is cleared.", e);
+ mEventLogger.logCurrentEventFailed(
+ new ConnectException(ConnectErrorCode.LINK_KEY_CLEARED,
+ "Link key is cleared"));
+ } else {
+ Log.w(TAG, "Fail to connected to " + maskBluetoothAddress(device)
+ + " with cached address.", e);
+ mEventLogger.logCurrentEventFailed(e);
+ }
+ return false;
+ } finally {
+ mContext.unregisterReceiver(receiver);
+ }
+ }
+
+ /**
+ * Logs for user retry, check go/fastpairquality21q3 for more details.
+ */
+ private void logManualRetryCounts(boolean success) {
+ if (!mPreferences.getLogUserManualRetry()) {
+ return;
+ }
+
+ // We don't want to be the final event on analytics.
+ if (!mEventLogger.isCurrentEvent()) {
+ return;
+ }
+
+ mEventLogger.setCurrentEvent(EventCode.GATT_HANDSHAKE_MANUAL_RETRY_ATTEMPTS);
+ if (mPreferences.getPairFailureCounts() <= 0 && success) {
+ mEventLogger.logCurrentEventSucceeded();
+ } else {
+ int errorCode = mPreferences.getPairFailureCounts();
+ if (errorCode > 99) {
+ errorCode = 99;
+ }
+ errorCode += success ? 0 : 100;
+ // To not conflict with current error codes.
+ errorCode += GATT_ERROR_CODE_USER_RETRY;
+ mEventLogger.logCurrentEventFailed(
+ new BluetoothGattException("Error for manual retry", errorCode));
+ }
+ }
+
+ static void logRetrySuccessEvent(
+ @EventCode int eventCode,
+ @Nullable Exception recoverFromException,
+ EventLoggerWrapper eventLogger) {
+ if (recoverFromException == null) {
+ return;
+ }
+ eventLogger.setCurrentEvent(eventCode);
+ eventLogger.logCurrentEventFailed(recoverFromException);
+ }
+
+ private void initGattConnectionManager() {
+ mGattConnectionManager =
+ new GattConnectionManager(
+ mContext,
+ mPreferences,
+ mEventLogger,
+ mBluetoothAdapter,
+ this::toggleBluetooth,
+ mBleAddress,
+ mTimingLogger,
+ mFastPairSignalChecker,
+ isPairingWithAntiSpoofingPublicKey());
+ }
+
+ private void logCurrentEventFailedBySignalRotated(SignalRotatedException e) {
+ if (!mEventLogger.isCurrentEvent()) {
+ return;
+ }
+
+ Log.w(TAG, "BLE Address for pairing device might rotated!");
+ mEventLogger.logCurrentEventFailed(
+ new BluetoothGattException(
+ "BLE Address for pairing device might rotated",
+ appendMoreErrorCode(GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED,
+ e.getCause()),
+ e));
+ }
+
+ private void logCurrentEventFailedBySignalLost(SignalLostException e) {
+ if (!mEventLogger.isCurrentEvent()) {
+ return;
+ }
+
+ Log.w(TAG, "BLE signal for pairing device might lost!");
+ mEventLogger.logCurrentEventFailed(
+ new BluetoothGattException(
+ "BLE signal for pairing device might lost",
+ appendMoreErrorCode(GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST, e.getCause()),
+ e));
+ }
+
+ @VisibleForTesting
+ static int appendMoreErrorCode(int masterErrorCode, @Nullable Throwable cause) {
+ if (cause instanceof BluetoothGattException) {
+ return masterErrorCode + ((BluetoothGattException) cause).getGattErrorCode();
+ } else if (cause instanceof TimeoutException
+ || cause instanceof BluetoothTimeoutException
+ || cause instanceof BluetoothOperationTimeoutException) {
+ return masterErrorCode + GATT_ERROR_CODE_TIMEOUT;
+ } else {
+ return masterErrorCode;
+ }
+ }
+
+ private void setBleAddress(String newAddress) {
+ if (TextUtils.isEmpty(newAddress) || Ascii.equalsIgnoreCase(newAddress, mBleAddress)) {
+ return;
+ }
+
+ mBleAddress = newAddress;
+
+ // Recreates a GattConnectionManager with the new address for establishing a new GATT
+ // connection later.
+ initGattConnectionManager();
+
+ mEventLogger.setDevice(mBluetoothAdapter.getRemoteDevice(mBleAddress).unwrap());
+ }
+
+ /**
+ * Gets the public address of the headset used in the connection. Before the handshake, this
+ * could be null.
+ */
+ @Nullable
+ public String getPublicAddress() {
+ return mPublicAddress;
+ }
+
+ /**
+ * Pairs with a Bluetooth device. In general, this process goes through the following steps:
+ *
+ * <ol>
+ * <li>Get BrEdr handover information if requested
+ * <li>Discover the device (on Android N and lower to work around a bug)
+ * <li>Connect to the device
+ * <ul>
+ * <li>Attempt a direct connection to a supported profile if we're already bonded
+ * <li>Create a new bond with the not bonded device and then connect to a supported
+ * profile
+ * </ul>
+ * <li>Write the account secret
+ * </ol>
+ *
+ * <p>Blocks until paired. May take 10+ seconds, so run on a background thread.
+ */
+ @Nullable
+ private SharedSecret pair(boolean enableBrEdrHandover)
+ throws BluetoothException, InterruptedException, ReflectionException, TimeoutException,
+ ExecutionException, PairingException, GeneralSecurityException {
+ BrEdrHandoverInformation brEdrHandoverInformation = null;
+ if (enableBrEdrHandover) {
+ try (ScopedTiming scopedTiming =
+ new ScopedTiming(mTimingLogger, "Get BR/EDR handover information via GATT")) {
+ brEdrHandoverInformation =
+ getBrEdrHandoverInformation(mGattConnectionManager.getConnection());
+ } catch (BluetoothException | TdsException e) {
+ Log.w(TAG,
+ "Couldn't get BR/EDR Handover info via TDS. Trying direct connect.", e);
+ mEventLogger.logCurrentEventFailed(e);
+ }
+ }
+
+ if (brEdrHandoverInformation == null) {
+ // Pair directly to the BLE address. Works if the BLE and Bluetooth Classic addresses
+ // are the same, or if we can do BLE cross-key transport.
+ brEdrHandoverInformation =
+ new BrEdrHandoverInformation(
+ BluetoothAddress
+ .decode(mPublicAddress != null ? mPublicAddress : mBleAddress),
+ attemptGetBluetoothClassicProfiles(
+ mBluetoothAdapter.getRemoteDevice(mBleAddress).unwrap(),
+ mPreferences.getNumSdpAttempts()));
+ }
+
+ BluetoothDevice device =
+ mBluetoothAdapter.getRemoteDevice(brEdrHandoverInformation.mBluetoothAddress)
+ .unwrap();
+ callbackOnGetAddress(device.getAddress());
+ mEventLogger.setDevice(device);
+
+ Log.i(TAG, "Pairing with " + brEdrHandoverInformation);
+ KeyBasedPairingInfo keyBasedPairingInfo =
+ mPairingSecret == null
+ ? null
+ : new KeyBasedPairingInfo(
+ mPairingSecret, mGattConnectionManager, mProviderInitiatesBonding);
+
+ BluetoothAudioPairer pairer =
+ new BluetoothAudioPairer(
+ mContext,
+ device,
+ mPreferences,
+ mEventLogger,
+ keyBasedPairingInfo,
+ mPasskeyConfirmationHandler,
+ mTimingLogger);
+
+ logPairWithPossibleCachedAddress(device.getAddress());
+ logPairWithModelIdInCacheAndDiscoveryFailForCachedAddress(device);
+
+ // In the case where we are already bonded, we should first just try connecting to supported
+ // profiles. If successful, then this will be much faster than recreating the bond like we
+ // normally do and we can finish early. It is also more reliable than tearing down the bond
+ // and recreating it.
+ try {
+ if (!sTestMode) {
+ attemptDirectConnectionIfBonded(device, pairer);
+ }
+ callbackOnPaired();
+ return maybeWriteAccountKey(device);
+ } catch (PairingException e) {
+ Log.i(TAG, "Failed to directly connect to supported profiles: " + e.getMessage());
+ // Catches exception when we fail to connect support profile. And makes the flow to go
+ // through step to write account key when device is bonded.
+ if (mPreferences.getEnablePairFlowShowUiWithoutProfileConnection()
+ && device.getBondState() == BluetoothDevice.BOND_BONDED) {
+ if (mPreferences.getSkipConnectingProfiles()
+ && !mPreferences.getCheckBondStateWhenSkipConnectingProfiles()) {
+ Log.i(TAG, "For notCheckBondStateWhenSkipConnectingProfiles case should do "
+ + "re-bond");
+ } else {
+ Log.i(TAG, "Fail to connect profile when device is bonded, still call back on"
+ + "pair callback to show ui");
+ callbackOnPaired();
+ return maybeWriteAccountKey(device);
+ }
+ }
+ }
+
+ if (mPreferences.getMoreEventLogForQuality()) {
+ switch (device.getBondState()) {
+ case BOND_BONDED:
+ mEventLogger.setCurrentEvent(EventCode.BEFORE_CREATE_BOND_BONDED);
+ break;
+ case BOND_BONDING:
+ mEventLogger.setCurrentEvent(EventCode.BEFORE_CREATE_BOND_BONDING);
+ break;
+ case BOND_NONE:
+ default:
+ mEventLogger.setCurrentEvent(EventCode.BEFORE_CREATE_BOND);
+ }
+ }
+
+ for (int i = 1; i <= mPreferences.getNumCreateBondAttempts(); i++) {
+ try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Pair device #" + i)) {
+ pairer.pair();
+ if (mPreferences.getMoreEventLogForQuality()) {
+ // For EventCode.BEFORE_CREATE_BOND
+ mEventLogger.logCurrentEventSucceeded();
+ }
+ break;
+ } catch (Exception e) {
+ mEventLogger.logCurrentEventFailed(e);
+ if (mPasskeyIsGotten) {
+ Log.w(TAG,
+ "createBond() failed because of " + e.getMessage()
+ + " after getting the passkey. Skip retry.");
+ if (mPreferences.getMoreEventLogForQuality()) {
+ // For EventCode.BEFORE_CREATE_BOND
+ mEventLogger.logCurrentEventFailed(
+ new CreateBondException(
+ CreateBondErrorCode.FAILED_BUT_ALREADY_RECEIVE_PASS_KEY,
+ 0,
+ "Already get the passkey"));
+ }
+ break;
+ }
+ Log.e(TAG,
+ "removeBond() or createBond() failed, attempt " + i + " of " + mPreferences
+ .getNumCreateBondAttempts() + ". Bond state "
+ + device.getBondState(), e);
+ if (i < mPreferences.getNumCreateBondAttempts()) {
+ toggleBluetooth();
+
+ // We've seen 3 createBond() failures within 100ms (!). And then success again
+ // later (even without turning on/off bluetooth). So create some minimum break
+ // time.
+ Log.i(TAG, "Sleeping 1 sec after createBond() failure.");
+ SystemClock.sleep(1000);
+ } else if (mPreferences.getMoreEventLogForQuality()) {
+ // For EventCode.BEFORE_CREATE_BOND
+ mEventLogger.logCurrentEventFailed(e);
+ }
+ }
+ }
+ boolean deviceCreateBondFailWithNullSecret = false;
+ if (!pairer.isPaired()) {
+ if (mPairingSecret != null) {
+ // Bonding could fail for a few different reasons here. It could be an error, an
+ // attacker may have tried to bond, or the device may not be up to spec.
+ throw new PairingException("createBond() failed, exiting connection process.");
+ } else if (mPreferences.getSkipConnectingProfiles()) {
+ throw new PairingException(
+ "createBond() failed and skipping connecting to a profile.");
+ } else {
+ // When bond creation has failed, connecting a profile will still work most of the
+ // time for Fast Pair 1.0 devices (ie, pairing secret is null), so continue on with
+ // the spec anyways and attempt to connect supported profiles.
+ Log.w(TAG, "createBond() failed, will try connecting profiles anyway.");
+ deviceCreateBondFailWithNullSecret = true;
+ }
+ } else if (mPreferences.getEnablePairFlowShowUiWithoutProfileConnection()) {
+ Log.i(TAG, "new flow to call on paired callback for ui when pairing step is finished");
+ callbackOnPaired();
+ }
+
+ if (!mPreferences.getSkipConnectingProfiles()) {
+ if (mPreferences.getWaitForUuidsAfterBonding()
+ && brEdrHandoverInformation.mProfiles.length == 0) {
+ short[] supportedProfiles = getCachedUuids(device);
+ if (supportedProfiles.length == 0
+ && mPreferences.getNumSdpAttemptsAfterBonded() > 0) {
+ Log.i(TAG, "Found no supported profiles in UUID cache, manually trigger SDP.");
+ attemptGetBluetoothClassicProfiles(device,
+ mPreferences.getNumSdpAttemptsAfterBonded());
+ }
+ brEdrHandoverInformation =
+ new BrEdrHandoverInformation(
+ brEdrHandoverInformation.mBluetoothAddress, supportedProfiles);
+ }
+ short[] profiles = brEdrHandoverInformation.mProfiles;
+ if (profiles.length == 0) {
+ profiles = Constants.getSupportedProfiles();
+ Log.w(TAG,
+ "Attempting to connect constants profiles, " + Arrays.toString(profiles));
+ } else {
+ Log.i(TAG, "Attempting to connect device profiles, " + Arrays.toString(profiles));
+ }
+
+ try {
+ attemptConnectProfiles(
+ pairer,
+ maskBluetoothAddress(device),
+ profiles,
+ mPreferences.getNumConnectAttempts(),
+ /* enablePairingBehavior= */ false);
+ } catch (PairingException e) {
+ // For new pair flow to show ui, we already show success ui when finishing the
+ // createBond step. So we should catch the exception from connecting profile to
+ // avoid showing fail ui for user.
+ if (mPreferences.getEnablePairFlowShowUiWithoutProfileConnection()
+ && !deviceCreateBondFailWithNullSecret) {
+ Log.i(TAG, "Fail to connect profile when device is bonded");
+ } else {
+ throw e;
+ }
+ }
+ }
+ if (!mPreferences.getEnablePairFlowShowUiWithoutProfileConnection()) {
+ Log.i(TAG, "original flow to call on paired callback for ui");
+ callbackOnPaired();
+ } else if (deviceCreateBondFailWithNullSecret) {
+ // This paired callback is called for device which create bond fail with null secret
+ // such as FastPair 1.0 device when directly connecting to any supported profile.
+ Log.i(TAG, "call on paired callback for ui for device with null secret without bonded "
+ + "state");
+ callbackOnPaired();
+ }
+ if (mPreferences.getEnableFirmwareVersionCharacteristic()
+ && validateBluetoothGattCharacteristic(
+ mGattConnectionManager.getConnection(), FirmwareVersionCharacteristic.ID)) {
+ try {
+ sInitialConnectionFirmwareVersion = readFirmwareVersion();
+ } catch (BluetoothException e) {
+ Log.i(TAG, "Fast Pair: head phone does not support firmware read", e);
+ }
+ }
+
+ // Catch exception when writing account key or name fail to avoid showing pairing failure
+ // notice for user. Because device is already paired successfully based on paring step.
+ SharedSecret secret = null;
+ try {
+ secret = maybeWriteAccountKey(device);
+ } catch (InterruptedException
+ | ExecutionException
+ | TimeoutException
+ | NoSuchAlgorithmException
+ | BluetoothException e) {
+ Log.w(TAG, "Fast Pair: Got exception when writing account key or name to provider", e);
+ }
+
+ return secret;
+ }
+
+ private void logPairWithPossibleCachedAddress(String brEdrAddressForBonding) {
+ if (TextUtils.isEmpty(mPreferences.getPossibleCachedDeviceAddress())
+ || !mPreferences.getLogPairWithCachedModelId()) {
+ return;
+ }
+ mEventLogger.setCurrentEvent(EventCode.PAIR_WITH_CACHED_MODEL_ID);
+ if (Ascii.equalsIgnoreCase(
+ mPreferences.getPossibleCachedDeviceAddress(), brEdrAddressForBonding)) {
+ mEventLogger.logCurrentEventSucceeded();
+ Log.i(TAG, "Repair with possible cached device "
+ + maskBluetoothAddress(mPreferences.getPossibleCachedDeviceAddress()));
+ } else {
+ mEventLogger.logCurrentEventFailed(
+ new PairingException("Pairing with 2nd device with same model ID"));
+ Log.i(TAG, "Pair with a new device " + maskBluetoothAddress(brEdrAddressForBonding)
+ + " with model ID in cache "
+ + maskBluetoothAddress(mPreferences.getPossibleCachedDeviceAddress()));
+ }
+ }
+
+ /**
+ * Logs two type of events. First, why cachedAddress mechanism doesn't work if it's repair with
+ * bonded device case. Second, if it's not the case, log how many devices with the same model Id
+ * is already paired.
+ */
+ private void logPairWithModelIdInCacheAndDiscoveryFailForCachedAddress(BluetoothDevice device) {
+ if (!mPreferences.getLogPairWithCachedModelId()) {
+ return;
+ }
+
+ if (device.getBondState() == BOND_BONDED) {
+ if (mPreferences.getSameModelIdPairedDeviceCount() <= 0) {
+ Log.i(TAG, "Device is bonded but we don't have this model Id in cache.");
+ } else if (TextUtils.isEmpty(mPreferences.getCachedDeviceAddress())
+ && mPreferences.getDirectConnectProfileIfModelIdInCache()
+ && !mPreferences.getSkipConnectingProfiles()) {
+ // Pair with bonded device case. Log why the cached address is not found.
+ mEventLogger.setCurrentEvent(
+ EventCode.DIRECTLY_CONNECT_PROFILE_WITH_CACHED_ADDRESS);
+ mEventLogger.logCurrentEventFailed(
+ mPreferences.getIsDeviceFinishCheckAddressFromCache()
+ ? new ConnectException(ConnectErrorCode.FAIL_TO_DISCOVERY,
+ "Failed to discovery")
+ : new ConnectException(
+ ConnectErrorCode.DISCOVERY_NOT_FINISHED,
+ "Discovery not finished"));
+ Log.i(TAG, "Failed to get cached address due to "
+ + (mPreferences.getIsDeviceFinishCheckAddressFromCache()
+ ? "Failed to discovery"
+ : "Discovery not finished"));
+ }
+ } else if (device.getBondState() == BOND_NONE) {
+ // Pair with new device case, log how many devices with the same model id is in FastPair
+ // cache already.
+ mEventLogger.setCurrentEvent(EventCode.PAIR_WITH_NEW_MODEL);
+ if (mPreferences.getSameModelIdPairedDeviceCount() <= 0) {
+ mEventLogger.logCurrentEventSucceeded();
+ } else {
+ mEventLogger.logCurrentEventFailed(
+ new BluetoothGattException(
+ "Already have this model ID in cache",
+ GATT_ERROR_CODE_PAIR_WITH_SAME_MODEL_ID_COUNT
+ + mPreferences.getSameModelIdPairedDeviceCount()));
+ }
+ Log.i(TAG, "This device already has " + mPreferences.getSameModelIdPairedDeviceCount()
+ + " peripheral with the same model Id");
+ }
+ }
+
+ /**
+ * Attempts to directly connect to any supported profile if we're already bonded, this will save
+ * time over tearing down the bond and recreating it.
+ */
+ private void attemptDirectConnectionIfBonded(BluetoothDevice device,
+ BluetoothAudioPairer pairer)
+ throws PairingException {
+ if (mPreferences.getSkipConnectingProfiles()) {
+ if (mPreferences.getCheckBondStateWhenSkipConnectingProfiles()
+ && device.getBondState() == BluetoothDevice.BOND_BONDED) {
+ Log.i(TAG, "Skipping connecting to profiles by preferences.");
+ return;
+ }
+ throw new PairingException(
+ "Skipping connecting to profiles, no direct connection possible.");
+ } else if (!mPreferences.getAttemptDirectConnectionWhenPreviouslyBonded()
+ || device.getBondState() != BluetoothDevice.BOND_BONDED) {
+ throw new PairingException(
+ "Not previously bonded skipping direct connection, %s", device.getBondState());
+ }
+ short[] supportedProfiles = getSupportedProfiles(device);
+ mEventLogger.setCurrentEvent(EventCode.DIRECTLY_CONNECTED_TO_PROFILE);
+ try (ScopedTiming scopedTiming =
+ new ScopedTiming(mTimingLogger, "Connect to profile directly")) {
+ attemptConnectProfiles(
+ pairer,
+ maskBluetoothAddress(device),
+ supportedProfiles,
+ mPreferences.getEnablePairFlowShowUiWithoutProfileConnection()
+ ? mPreferences.getNumConnectAttempts()
+ : 1,
+ mPreferences.getEnablePairingWhileDirectlyConnecting());
+ Log.i(TAG, "Directly connected to " + maskBluetoothAddress(device));
+ mEventLogger.logCurrentEventSucceeded();
+ } catch (PairingException e) {
+ mEventLogger.logCurrentEventFailed(e);
+ // Rethrow e so that the exception bubbles up and we continue the normal pairing
+ // process.
+ throw e;
+ }
+ }
+
+ @VisibleForTesting
+ void attemptConnectProfiles(
+ BluetoothAudioPairer pairer,
+ String deviceMaskedBluetoothAddress,
+ short[] profiles,
+ int numConnectionAttempts,
+ boolean enablePairingBehavior)
+ throws PairingException {
+ attemptConnectProfiles(
+ pairer,
+ deviceMaskedBluetoothAddress,
+ profiles,
+ numConnectionAttempts,
+ enablePairingBehavior,
+ new AtomicBoolean(false));
+ }
+
+ private void attemptConnectProfiles(
+ BluetoothAudioPairer pairer,
+ String deviceMaskedBluetoothAddress,
+ short[] profiles,
+ int numConnectionAttempts,
+ boolean enablePairingBehavior,
+ AtomicBoolean interruptConnection)
+ throws PairingException {
+ if (mPreferences.getMoreEventLogForQuality()) {
+ mEventLogger.setCurrentEvent(EventCode.BEFORE_CONNECT_PROFILE);
+ }
+ Exception lastException = null;
+ for (short profile : profiles) {
+ if (interruptConnection.get()) {
+ Log.w(TAG, "attemptConnectProfiles interrupted");
+ break;
+ }
+ if (!mPreferences.isSupportedProfile(profile)) {
+ Log.w(TAG, "Ignoring unsupported profile=" + profile);
+ continue;
+ }
+ for (int i = 1; i <= numConnectionAttempts; i++) {
+ if (interruptConnection.get()) {
+ Log.w(TAG, "attemptConnectProfiles interrupted");
+ break;
+ }
+ mEventLogger.setCurrentEvent(EventCode.CONNECT_PROFILE);
+ mEventLogger.setCurrentProfile(profile);
+ try {
+ pairer.connect(profile, enablePairingBehavior);
+ mEventLogger.logCurrentEventSucceeded();
+ if (mPreferences.getMoreEventLogForQuality()) {
+ // For EventCode.BEFORE_CONNECT_PROFILE
+ mEventLogger.logCurrentEventSucceeded();
+ }
+ // If successful, we're done.
+ // TODO(b/37167120): Connect to more than one profile.
+ return;
+ } catch (InterruptedException
+ | ReflectionException
+ | TimeoutException
+ | ExecutionException
+ | ConnectException e) {
+ Log.w(TAG,
+ "Error connecting to profile=" + profile
+ + " for device=" + deviceMaskedBluetoothAddress
+ + " (attempt " + i + " of " + mPreferences
+ .getNumConnectAttempts(), e);
+ mEventLogger.logCurrentEventFailed(e);
+ lastException = e;
+ }
+ }
+ }
+ if (mPreferences.getMoreEventLogForQuality()) {
+ // For EventCode.BEFORE_CONNECT_PROFILE
+ if (lastException != null) {
+ mEventLogger.logCurrentEventFailed(lastException);
+ } else {
+ mEventLogger.logCurrentEventSucceeded();
+ }
+ }
+ throw new PairingException(
+ "Unable to connect to any profiles in: %s", Arrays.toString(profiles));
+ }
+
+ /**
+ * Checks whether or not an account key should be written to the device and writes it if so.
+ * This is called after handle notifying the pairedCallback that we've finished pairing, because
+ * at this point the headset is ready to use.
+ */
+ @Nullable
+ private SharedSecret maybeWriteAccountKey(BluetoothDevice device)
+ throws InterruptedException, ExecutionException, TimeoutException,
+ NoSuchAlgorithmException,
+ BluetoothException {
+ if (!sTestMode) {
+ Locator.get(mContext, FastPairController.class).setShouldUpload(false);
+ }
+ if (!shouldWriteAccountKey()) {
+ // For FastPair 2.0, here should be a subsequent pairing case.
+ return null;
+ }
+
+ // Check if it should be a subsequent pairing but go through initial pairing. If there is an
+ // existed paired history found, use the same account key instead of creating a new one.
+ byte[] accountKey =
+ mPairedHistoryFinder == null ? null : mPairedHistoryFinder.getExistingAccountKey();
+ if (accountKey == null) {
+ // It is a real initial pairing, generate a new account key for the headset.
+ try (ScopedTiming scopedTiming1 =
+ new ScopedTiming(mTimingLogger, "Write account key")) {
+ accountKey = doWriteAccountKey(createAccountKey(), device.getAddress());
+ if (accountKey == null) {
+ // Without writing account key back to provider, close the connection.
+ mGattConnectionManager.closeConnection();
+ return null;
+ }
+ if (!mPreferences.getIsRetroactivePairing()) {
+ try (ScopedTiming scopedTiming2 = new ScopedTiming(mTimingLogger,
+ "Start CloudSyncing")) {
+ // Start to sync to the footprint
+ Locator.get(mContext, FastPairController.class).setShouldUpload(true);
+ //mContext.startService(createCloudSyncingIntent(accountKey));
+ } catch (SecurityException e) {
+ Log.w(TAG, "Error adding device.", e);
+ }
+ }
+ }
+ } else if (shouldWriteAccountKeyForExistingCase(accountKey)) {
+ // There is an existing account key, but go through initial pairing, and still write the
+ // existing account key.
+ doWriteAccountKey(accountKey, device.getAddress());
+ }
+
+ // When finish writing account key in initial pairing, write new device name back to
+ // provider.
+ UUID characteristicUuid = NameCharacteristic.getId(mGattConnectionManager.getConnection());
+ if (mPreferences.getEnableNamingCharacteristic()
+ && mNeedUpdateProviderName
+ && validateBluetoothGattCharacteristic(
+ mGattConnectionManager.getConnection(), characteristicUuid)) {
+ try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
+ "WriteNameToProvider")) {
+ writeNameToProvider(this.mProviderDeviceName, device.getAddress());
+ }
+ }
+
+ // When finish writing account key and name back to provider, close the connection.
+ mGattConnectionManager.closeConnection();
+ return SharedSecret.create(accountKey, device.getAddress());
+ }
+
+ private boolean shouldWriteAccountKey() {
+ return isWritingAccountKeyEnabled() && isPairingWithAntiSpoofingPublicKey();
+ }
+
+ private boolean isWritingAccountKeyEnabled() {
+ return mPreferences.getNumWriteAccountKeyAttempts() > 0;
+ }
+
+ private boolean isPairingWithAntiSpoofingPublicKey() {
+ return isPairingWithAntiSpoofingPublicKey(mPairingKey);
+ }
+
+ private boolean isPairingWithAntiSpoofingPublicKey(@Nullable byte[] key) {
+ return key != null && key.length == EllipticCurveDiffieHellmanExchange.PUBLIC_KEY_LENGTH;
+ }
+
+ /**
+ * Creates and writes an account key to the provided mac address.
+ */
+ @Nullable
+ private byte[] doWriteAccountKey(byte[] accountKey, String macAddress)
+ throws InterruptedException, ExecutionException, TimeoutException, BluetoothException {
+ byte[] localPairingSecret = mPairingSecret;
+ if (localPairingSecret == null) {
+ Log.w(TAG, "Pairing secret was null, account key couldn't be encrypted or written.");
+ return null;
+ }
+ if (!mPreferences.getSkipDisconnectingGattBeforeWritingAccountKey()) {
+ try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
+ "Close GATT and sleep")) {
+ // Make a new connection instead of reusing gattConnection, because this is
+ // post-pairing and we need an encrypted connection.
+ mGattConnectionManager.closeConnection();
+ // Sleep before re-connecting to gatt, for writing account key, could increase
+ // stability.
+ Thread.sleep(mPreferences.getWriteAccountKeySleepMillis());
+ }
+ }
+
+ byte[] encryptedKey;
+ try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Encrypt key")) {
+ encryptedKey = AesEcbSingleBlockEncryption.encrypt(localPairingSecret, accountKey);
+ } catch (GeneralSecurityException e) {
+ Log.w("Failed to encrypt key.", e);
+ return null;
+ }
+
+ for (int i = 1; i <= mPreferences.getNumWriteAccountKeyAttempts(); i++) {
+ mEventLogger.setCurrentEvent(EventCode.WRITE_ACCOUNT_KEY);
+ try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
+ "Write key via GATT #" + i)) {
+ writeAccountKey(encryptedKey, macAddress);
+ mEventLogger.logCurrentEventSucceeded();
+ return accountKey;
+ } catch (BluetoothException e) {
+ Log.w("Error writing account key attempt " + i + " of " + mPreferences
+ .getNumWriteAccountKeyAttempts(), e);
+ mEventLogger.logCurrentEventFailed(e);
+ // Retry with a while for stability.
+ Thread.sleep(mPreferences.getWriteAccountKeySleepMillis());
+ }
+ }
+ return null;
+ }
+
+ private byte[] createAccountKey() throws NoSuchAlgorithmException {
+ return AccountKeyGenerator.createAccountKey();
+ }
+
+ @VisibleForTesting
+ boolean shouldWriteAccountKeyForExistingCase(byte[] existingAccountKey) {
+ if (!mPreferences.getKeepSameAccountKeyWrite()) {
+ Log.i(TAG,
+ "The provider has already paired with the account, skip writing account key.");
+ return false;
+ }
+ if (existingAccountKey[0] != AccountKeyCharacteristic.TYPE) {
+ Log.i(TAG,
+ "The provider has already paired with the account, but accountKey[0] != 0x04."
+ + " Forget the device from the account and re-try");
+
+ return false;
+ }
+ Log.i(TAG, "The provider has already paired with the account, still write the same account "
+ + "key.");
+ return true;
+ }
+
+ /**
+ * Performs a key-based pairing request handshake to authenticate and get the remote device's
+ * public address.
+ *
+ * @param key is described in {@link #pair(byte[])}
+ */
+ @VisibleForTesting
+ SharedSecret handshakeForKeyBasedPairing(byte[] key)
+ throws InterruptedException, ExecutionException, TimeoutException, BluetoothException,
+ GeneralSecurityException, PairingException {
+ // We may also initialize gattConnectionManager of prepareForHandshake() that will be used
+ // in registerNotificationForNamePacket(), so we need to call it here.
+ HandshakeHandler handshakeHandler = prepareForHandshake();
+ KeyBasedPairingRequest.Builder keyBasedPairingRequestBuilder =
+ new KeyBasedPairingRequest.Builder()
+ .setVerificationData(BluetoothAddress.decode(mBleAddress));
+ if (mProviderInitiatesBonding) {
+ keyBasedPairingRequestBuilder
+ .addFlag(KeyBasedPairingRequestFlag.PROVIDER_INITIATES_BONDING);
+ }
+ // Seeker only request provider device name in initial pairing.
+ if (mPreferences.getEnableNamingCharacteristic() && isPairingWithAntiSpoofingPublicKey(
+ key)) {
+ keyBasedPairingRequestBuilder.addFlag(KeyBasedPairingRequestFlag.REQUEST_DEVICE_NAME);
+ // Register listener to receive name characteristic response from provider.
+ registerNotificationForNamePacket();
+ }
+ if (mPreferences.getIsRetroactivePairing()) {
+ keyBasedPairingRequestBuilder
+ .addFlag(KeyBasedPairingRequestFlag.REQUEST_RETROACTIVE_PAIR);
+ keyBasedPairingRequestBuilder.setSeekerPublicAddress(
+ Preconditions.checkNotNull(BluetoothAddress.getPublicAddress(mContext)));
+ }
+
+ return performHandshakeWithRetryAndSignalLostCheck(
+ handshakeHandler, key, keyBasedPairingRequestBuilder.build(), /* withRetry= */
+ true);
+ }
+
+ /**
+ * Performs an action-over-BLE request handshake for authentication, i.e. to identify the shared
+ * secret. The given key should be the account key.
+ */
+ private SharedSecret handshakeForActionOverBle(byte[] key,
+ @AdditionalDataType int additionalDataType)
+ throws InterruptedException, ExecutionException, TimeoutException, BluetoothException,
+ GeneralSecurityException, PairingException {
+ HandshakeHandler handshakeHandler = prepareForHandshake();
+ return performHandshakeWithRetryAndSignalLostCheck(
+ handshakeHandler,
+ key,
+ new ActionOverBle.Builder()
+ .setVerificationData(BluetoothAddress.decode(mBleAddress))
+ .setAdditionalDataType(additionalDataType)
+ .build(),
+ /* withRetry= */ false);
+ }
+
+ private HandshakeHandler prepareForHandshake() {
+ if (mGattConnectionManager == null) {
+ mGattConnectionManager =
+ new GattConnectionManager(
+ mContext,
+ mPreferences,
+ mEventLogger,
+ mBluetoothAdapter,
+ this::toggleBluetooth,
+ mBleAddress,
+ mTimingLogger,
+ mFastPairSignalChecker,
+ isPairingWithAntiSpoofingPublicKey());
+ }
+ if (mHandshakeHandlerForTest != null) {
+ Log.w(TAG, "Use handshakeHandlerForTest!");
+ return verifyNotNull(mHandshakeHandlerForTest);
+ }
+ return new HandshakeHandler(
+ mGattConnectionManager, mBleAddress, mPreferences, mEventLogger,
+ mFastPairSignalChecker);
+ }
+
+ @VisibleForTesting
+ void setHandshakeHandlerForTest(@Nullable HandshakeHandler handshakeHandlerForTest) {
+ this.mHandshakeHandlerForTest = handshakeHandlerForTest;
+ }
+
+ private SharedSecret performHandshakeWithRetryAndSignalLostCheck(
+ HandshakeHandler handshakeHandler,
+ byte[] key,
+ HandshakeMessage handshakeMessage,
+ boolean withRetry)
+ throws GeneralSecurityException, ExecutionException, BluetoothException,
+ InterruptedException, TimeoutException, PairingException {
+ SharedSecret handshakeResult =
+ withRetry
+ ? handshakeHandler.doHandshakeWithRetryAndSignalLostCheck(
+ key, handshakeMessage, mRescueFromError)
+ : handshakeHandler.doHandshake(key, handshakeMessage);
+ // TODO: Try to remove these two global variables, publicAddress and pairingSecret.
+ mPublicAddress = handshakeResult.getAddress();
+ mPairingSecret = handshakeResult.getKey();
+ return handshakeResult;
+ }
+
+ private void toggleBluetooth()
+ throws InterruptedException, ExecutionException, TimeoutException {
+ if (!mPreferences.getToggleBluetoothOnFailure()) {
+ return;
+ }
+
+ Log.i(TAG, "Turning Bluetooth off.");
+ mEventLogger.setCurrentEvent(EventCode.DISABLE_BLUETOOTH);
+ mBluetoothAdapter.unwrap().disable();
+ disableBle(mBluetoothAdapter.unwrap());
+ try {
+ waitForBluetoothState(android.bluetooth.BluetoothAdapter.STATE_OFF);
+ mEventLogger.logCurrentEventSucceeded();
+ } catch (TimeoutException e) {
+ mEventLogger.logCurrentEventFailed(e);
+ // Soldier on despite failing to turn off Bluetooth. We can't control whether other
+ // clients (even inside GCore) kept it enabled in BLE-only mode.
+ Log.w(TAG, "Bluetooth still on. BluetoothAdapter state="
+ + getBleState(mBluetoothAdapter.unwrap()), e);
+ }
+
+ // Note: Intentionally don't re-enable BLE-only mode, because we don't know which app
+ // enabled it. The client app should listen to Bluetooth events and enable as necessary
+ // (because the user can toggle at any time; e.g. via Airplane mode).
+ Log.i(TAG, "Turning Bluetooth on.");
+ mEventLogger.setCurrentEvent(EventCode.ENABLE_BLUETOOTH);
+ mBluetoothAdapter.unwrap().enable();
+ waitForBluetoothState(android.bluetooth.BluetoothAdapter.STATE_ON);
+ mEventLogger.logCurrentEventSucceeded();
+ }
+
+ private void waitForBluetoothState(int state)
+ throws TimeoutException, ExecutionException, InterruptedException {
+ waitForBluetoothStateUsingPolling(state);
+ }
+
+ private void waitForBluetoothStateUsingPolling(int state) throws TimeoutException {
+ // There's a bug where we (pretty often!) never get the broadcast for STATE_ON or STATE_OFF.
+ // So poll instead.
+ long start = SystemClock.elapsedRealtime();
+ long timeoutMillis = mPreferences.getBluetoothToggleTimeoutSeconds() * 1000L;
+ while (SystemClock.elapsedRealtime() - start < timeoutMillis) {
+ if (state == getBleState(mBluetoothAdapter.unwrap())) {
+ break;
+ }
+ SystemClock.sleep(mPreferences.getBluetoothStatePollingMillis());
+ }
+
+ if (state != getBleState(mBluetoothAdapter.unwrap())) {
+ throw new TimeoutException(
+ String.format(
+ Locale.getDefault(),
+ "Timed out waiting for state %d, current state is %d",
+ state,
+ getBleState(mBluetoothAdapter.unwrap())));
+ }
+ }
+
+ private BrEdrHandoverInformation getBrEdrHandoverInformation(BluetoothGattConnection connection)
+ throws BluetoothException, TdsException, InterruptedException, ExecutionException,
+ TimeoutException {
+ Log.i(TAG, "Connecting GATT server to BLE address=" + maskBluetoothAddress(mBleAddress));
+ Log.i(TAG, "Telling device to become discoverable");
+ mEventLogger.setCurrentEvent(EventCode.BR_EDR_HANDOVER_WRITE_CONTROL_POINT_REQUEST);
+ ChangeObserver changeObserver =
+ connection.enableNotification(
+ TransportDiscoveryService.ID,
+ TransportDiscoveryService.ControlPointCharacteristic.ID);
+ connection.writeCharacteristic(
+ TransportDiscoveryService.ID,
+ TransportDiscoveryService.ControlPointCharacteristic.ID,
+ TDS_CONTROL_POINT_REQUEST);
+
+ byte[] response =
+ changeObserver.waitForUpdate(
+ TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
+ @ResultCode int resultCode = fromTdsControlPointIndication(response);
+ if (resultCode != ResultCode.SUCCESS) {
+ throw new TdsException(
+ BrEdrHandoverErrorCode.CONTROL_POINT_RESULT_CODE_NOT_SUCCESS,
+ "TDS Control Point result code (%s) was not success in response %s",
+ resultCode,
+ base16().lowerCase().encode(response));
+ }
+ mEventLogger.logCurrentEventSucceeded();
+ return new BrEdrHandoverInformation(
+ getAddressFromBrEdrConnection(connection),
+ getProfilesFromBrEdrConnection(connection));
+ }
+
+ private byte[] getAddressFromBrEdrConnection(BluetoothGattConnection connection)
+ throws BluetoothException, TdsException {
+ Log.i(TAG, "Getting Bluetooth MAC");
+ mEventLogger.setCurrentEvent(EventCode.BR_EDR_HANDOVER_READ_BLUETOOTH_MAC);
+ byte[] brHandoverData =
+ connection.readCharacteristic(
+ TransportDiscoveryService.ID,
+ to128BitUuid(mPreferences.getBrHandoverDataCharacteristicId()));
+ if (brHandoverData == null || brHandoverData.length < 7) {
+ throw new TdsException(
+ BrEdrHandoverErrorCode.BLUETOOTH_MAC_INVALID,
+ "Bluetooth MAC not contained in BR handover data: %s",
+ brHandoverData != null ? base16().lowerCase().encode(brHandoverData)
+ : "(none)");
+ }
+ byte[] bluetoothAddress =
+ new Bytes.Value(Arrays.copyOfRange(brHandoverData, 1, 7), ByteOrder.LITTLE_ENDIAN)
+ .getBytes(ByteOrder.BIG_ENDIAN);
+ mEventLogger.logCurrentEventSucceeded();
+ return bluetoothAddress;
+ }
+
+ private short[] getProfilesFromBrEdrConnection(BluetoothGattConnection connection) {
+ mEventLogger.setCurrentEvent(EventCode.BR_EDR_HANDOVER_READ_TRANSPORT_BLOCK);
+ try {
+ byte[] transportBlock =
+ connection.readDescriptor(
+ TransportDiscoveryService.ID,
+ to128BitUuid(mPreferences.getBluetoothSigDataCharacteristicId()),
+ to128BitUuid(mPreferences.getBrTransportBlockDataDescriptorId()));
+ Log.i(TAG, "Got transport block: " + base16().lowerCase().encode(transportBlock));
+ short[] profiles = getSupportedProfiles(transportBlock);
+ mEventLogger.logCurrentEventSucceeded();
+ return profiles;
+ } catch (BluetoothException | TdsException | ParseException e) {
+ Log.w(TAG, "Failed to get supported profiles from transport block.", e);
+ mEventLogger.logCurrentEventFailed(e);
+ }
+ return new short[0];
+ }
+
+ @VisibleForTesting
+ boolean writeNameToProvider(@Nullable String deviceName, @Nullable String address)
+ throws InterruptedException, TimeoutException, ExecutionException {
+ if (deviceName == null || address == null) {
+ Log.i(TAG, "writeNameToProvider fail because provider name or address is null.");
+ return false;
+ }
+ if (mPairingSecret == null) {
+ Log.i(TAG, "writeNameToProvider fail because no pairingSecret.");
+ return false;
+ }
+ byte[] encryptedDeviceNamePacket;
+ try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Encode device name")) {
+ encryptedDeviceNamePacket =
+ NamingEncoder.encodeNamingPacket(mPairingSecret, deviceName);
+ } catch (GeneralSecurityException e) {
+ Log.w(TAG, "Failed to encrypt device name.", e);
+ return false;
+ }
+
+ for (int i = 1; i <= mPreferences.getNumWriteAccountKeyAttempts(); i++) {
+ mEventLogger.setCurrentEvent(EventCode.WRITE_DEVICE_NAME);
+ try {
+ writeDeviceName(encryptedDeviceNamePacket, address);
+ mEventLogger.logCurrentEventSucceeded();
+ return true;
+ } catch (BluetoothException e) {
+ Log.w(TAG, "Error writing name attempt " + i + " of "
+ + mPreferences.getNumWriteAccountKeyAttempts());
+ mEventLogger.logCurrentEventFailed(e);
+ // Reuses the existing preference because the same usage.
+ Thread.sleep(mPreferences.getWriteAccountKeySleepMillis());
+ }
+ }
+ return false;
+ }
+
+ private void writeAccountKey(byte[] encryptedAccountKey, String address)
+ throws BluetoothException, InterruptedException, ExecutionException, TimeoutException {
+ Log.i(TAG, "Writing account key to address=" + maskBluetoothAddress(address));
+ BluetoothGattConnection connection = mGattConnectionManager.getConnection();
+ connection.setOperationTimeout(
+ TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
+ UUID characteristicUuid = AccountKeyCharacteristic.getId(connection);
+ connection.writeCharacteristic(FastPairService.ID, characteristicUuid, encryptedAccountKey);
+ Log.i(TAG,
+ "Finished writing encrypted account key=" + base16().encode(encryptedAccountKey));
+ }
+
+ private void writeDeviceName(byte[] naming, String address)
+ throws BluetoothException, InterruptedException, ExecutionException, TimeoutException {
+ Log.i(TAG, "Writing new device name to address=" + maskBluetoothAddress(address));
+ BluetoothGattConnection connection = mGattConnectionManager.getConnection();
+ connection.setOperationTimeout(
+ TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
+ UUID characteristicUuid = NameCharacteristic.getId(connection);
+ connection.writeCharacteristic(FastPairService.ID, characteristicUuid, naming);
+ Log.i(TAG, "Finished writing new device name=" + base16().encode(naming));
+ }
+
+ /**
+ * Reads firmware version after write account key to provider since simulator is more stable to
+ * read firmware version in initial gatt connection. This function will also read firmware when
+ * detect bloomfilter. Need to verify this after real device come out. TODO(b/130592473)
+ */
+ @Nullable
+ public String readFirmwareVersion()
+ throws BluetoothException, InterruptedException, ExecutionException, TimeoutException {
+ if (!TextUtils.isEmpty(sInitialConnectionFirmwareVersion)) {
+ String result = sInitialConnectionFirmwareVersion;
+ sInitialConnectionFirmwareVersion = null;
+ return result;
+ }
+ if (mGattConnectionManager == null) {
+ mGattConnectionManager =
+ new GattConnectionManager(
+ mContext,
+ mPreferences,
+ mEventLogger,
+ mBluetoothAdapter,
+ this::toggleBluetooth,
+ mBleAddress,
+ mTimingLogger,
+ mFastPairSignalChecker,
+ /* setMtu= */ true);
+ mGattConnectionManager.closeConnection();
+ }
+ if (sTestMode) {
+ return null;
+ }
+ BluetoothGattConnection connection = mGattConnectionManager.getConnection();
+ connection.setOperationTimeout(
+ TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
+
+ try {
+ String firmwareVersion =
+ new String(
+ connection.readCharacteristic(
+ FastPairService.ID,
+ to128BitUuid(
+ mPreferences.getFirmwareVersionCharacteristicId())));
+ Log.i(TAG, "FastPair: Got the firmware info version number = " + firmwareVersion);
+ mGattConnectionManager.closeConnection();
+ return firmwareVersion;
+ } catch (BluetoothException e) {
+ Log.i(TAG, "FastPair: can't read firmware characteristic.", e);
+ mGattConnectionManager.closeConnection();
+ return null;
+ }
+ }
+
+ @VisibleForTesting
+ @Nullable
+ String getInitialConnectionFirmware() {
+ return sInitialConnectionFirmwareVersion;
+ }
+
+ private void registerNotificationForNamePacket()
+ throws BluetoothException, InterruptedException, ExecutionException, TimeoutException {
+ Log.i(TAG,
+ "register for the device name response from address=" + maskBluetoothAddress(
+ mBleAddress));
+
+ BluetoothGattConnection gattConnection = mGattConnectionManager.getConnection();
+ gattConnection.setOperationTimeout(
+ TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
+ try {
+ mDeviceNameReceiver = new DeviceNameReceiver(gattConnection);
+ } catch (BluetoothException e) {
+ Log.i(TAG, "Can't register for device name response, no naming characteristic.");
+ return;
+ }
+ }
+
+ private short[] getSupportedProfiles(BluetoothDevice device) {
+ short[] supportedProfiles = getCachedUuids(device);
+ if (supportedProfiles.length == 0 && mPreferences.getNumSdpAttemptsAfterBonded() > 0) {
+ supportedProfiles =
+ attemptGetBluetoothClassicProfiles(device,
+ mPreferences.getNumSdpAttemptsAfterBonded());
+ }
+ if (supportedProfiles.length == 0) {
+ supportedProfiles = Constants.getSupportedProfiles();
+ Log.w(TAG, "Attempting to connect constants profiles, "
+ + Arrays.toString(supportedProfiles));
+ } else {
+ Log.i(TAG,
+ "Attempting to connect device profiles, " + Arrays.toString(supportedProfiles));
+ }
+ return supportedProfiles;
+ }
+
+ private static short[] getSupportedProfiles(byte[] transportBlock)
+ throws TdsException, ParseException {
+ if (transportBlock == null || transportBlock.length < 4) {
+ throw new TdsException(
+ BrEdrHandoverErrorCode.TRANSPORT_BLOCK_INVALID,
+ "Transport Block null or too short: %s",
+ base16().lowerCase().encode(transportBlock));
+ }
+ int transportDataLength = transportBlock[2];
+ if (transportBlock.length < 3 + transportDataLength) {
+ throw new TdsException(
+ BrEdrHandoverErrorCode.TRANSPORT_BLOCK_INVALID,
+ "Transport Block has wrong length byte: %s",
+ base16().lowerCase().encode(transportBlock));
+ }
+ byte[] transportData = Arrays.copyOfRange(transportBlock, 3, 3 + transportDataLength);
+ for (Ltv ltv : Ltv.parse(transportData)) {
+ int uuidLength = uuidLength(ltv.mType);
+ // We currently only support a single list of 2-byte UUIDs.
+ // TODO(b/37539535): Support multiple lists, and longer (32-bit, 128-bit) IDs?
+ if (uuidLength == 2) {
+ return toShorts(ByteOrder.LITTLE_ENDIAN, ltv.mValue);
+ }
+ }
+ return new short[0];
+ }
+
+ /**
+ * Returns 0 if the type is not one of the UUID list types; otherwise returns length in bytes.
+ */
+ private static int uuidLength(byte dataType) {
+ switch (dataType) {
+ case TransportDiscoveryService.SERVICE_UUIDS_16_BIT_LIST_TYPE:
+ return 2;
+ case TransportDiscoveryService.SERVICE_UUIDS_32_BIT_LIST_TYPE:
+ return 4;
+ case TransportDiscoveryService.SERVICE_UUIDS_128_BIT_LIST_TYPE:
+ return 16;
+ default:
+ return 0;
+ }
+ }
+
+ private short[] attemptGetBluetoothClassicProfiles(BluetoothDevice device, int numSdpAttempts) {
+ // The docs say that if fetchUuidsWithSdp() has an error or "takes a long time", we get an
+ // intent containing only the stuff in the cache (i.e. nothing). Retry a few times.
+ short[] supportedProfiles = null;
+ for (int i = 1; i <= numSdpAttempts; i++) {
+ mEventLogger.setCurrentEvent(EventCode.GET_PROFILES_VIA_SDP);
+ try (ScopedTiming scopedTiming =
+ new ScopedTiming(mTimingLogger,
+ "Get BR/EDR handover information via SDP #" + i)) {
+ supportedProfiles = getSupportedProfilesViaBluetoothClassic(device);
+ } catch (ExecutionException | InterruptedException | TimeoutException e) {
+ // Ignores and retries if needed.
+ }
+ if (supportedProfiles != null && supportedProfiles.length != 0) {
+ mEventLogger.logCurrentEventSucceeded();
+ break;
+ } else {
+ mEventLogger.logCurrentEventFailed(new TimeoutException());
+ Log.w(TAG, "SDP returned no UUIDs from " + maskBluetoothAddress(device.getAddress())
+ + ", assuming timeout (attempt " + i + " of " + numSdpAttempts + ").");
+ }
+ }
+ return (supportedProfiles == null) ? new short[0] : supportedProfiles;
+ }
+
+ private short[] getSupportedProfilesViaBluetoothClassic(BluetoothDevice device)
+ throws ExecutionException, InterruptedException, TimeoutException {
+ Log.i(TAG, "Getting supported profiles via SDP (Bluetooth Classic) for "
+ + maskBluetoothAddress(device.getAddress()));
+ try (DeviceIntentReceiver supportedProfilesReceiver =
+ DeviceIntentReceiver.oneShotReceiver(
+ mContext, mPreferences, device, BluetoothDevice.ACTION_UUID)) {
+ device.fetchUuidsWithSdp();
+ supportedProfilesReceiver.await(mPreferences.getSdpTimeoutSeconds(), TimeUnit.SECONDS);
+ }
+ return getCachedUuids(device);
+ }
+
+ private static short[] getCachedUuids(BluetoothDevice device) {
+ ParcelUuid[] parcelUuids = device.getUuids();
+ Log.i(TAG, "Got supported UUIDs: " + Arrays.toString(parcelUuids));
+ if (parcelUuids == null) {
+ // The OS can return null.
+ parcelUuids = new ParcelUuid[0];
+ }
+
+ List<Short> shortUuids = new ArrayList<>(parcelUuids.length);
+ for (ParcelUuid parcelUuid : parcelUuids) {
+ UUID uuid = parcelUuid.getUuid();
+ if (BluetoothUuids.is16BitUuid(uuid)) {
+ shortUuids.add(get16BitUuid(uuid));
+ }
+ }
+ return Shorts.toArray(shortUuids);
+ }
+
+ private void callbackOnPaired() {
+ if (mPairedCallback != null) {
+ mPairedCallback.onPaired(mPublicAddress != null ? mPublicAddress : mBleAddress);
+ }
+ }
+
+ private void callbackOnGetAddress(String address) {
+ if (mOnGetBluetoothAddressCallback != null) {
+ mOnGetBluetoothAddressCallback.onGetBluetoothAddress(address);
+ }
+ }
+
+ private boolean validateBluetoothGattCharacteristic(
+ BluetoothGattConnection connection, UUID characteristicUUID) {
+ try (ScopedTiming scopedTiming =
+ new ScopedTiming(mTimingLogger, "Get service characteristic list")) {
+ List<BluetoothGattCharacteristic> serviceCharacteristicList =
+ connection.getService(FastPairService.ID).getCharacteristics();
+ for (BluetoothGattCharacteristic characteristic : serviceCharacteristicList) {
+ if (characteristicUUID.equals(characteristic.getUuid())) {
+ Log.i(TAG, "characteristic is exists, uuid = " + characteristicUUID);
+ return true;
+ }
+ }
+ } catch (BluetoothException e) {
+ Log.w(TAG, "Can't get service characteristic list.", e);
+ }
+ Log.i(TAG, "can't find characteristic, uuid = " + characteristicUUID);
+ return false;
+ }
+
+ // This method is only for testing to make test method block until get name response or time
+ // out.
+ /**
+ * Set name response countdown latch.
+ */
+ public void setNameResponseCountDownLatch(CountDownLatch countDownLatch) {
+ if (mDeviceNameReceiver != null) {
+ mDeviceNameReceiver.setCountDown(countDownLatch);
+ Log.v(TAG, "set up nameResponseCountDown");
+ }
+ }
+
+ private static int getBleState(android.bluetooth.BluetoothAdapter bluetoothAdapter) {
+ // Can't use the public isLeEnabled() API, because it returns false for
+ // STATE_BLE_TURNING_(ON|OFF). So if we assume false == STATE_OFF, that can be
+ // very wrong.
+ return getLeState(bluetoothAdapter);
+ }
+
+ private static int getLeState(android.bluetooth.BluetoothAdapter adapter) {
+ try {
+ return (Integer) Reflect.on(adapter).withMethod("getLeState").get();
+ } catch (ReflectionException e) {
+ Log.i(TAG, "Can't call getLeState", e);
+ }
+ return adapter.getState();
+ }
+
+ private static void disableBle(android.bluetooth.BluetoothAdapter adapter) {
+ adapter.disableBLE();
+ }
+
+ /**
+ * Handle the searching of Fast Pair history. Since there is only one public address using
+ * during Fast Pair connection, {@link #isInPairedHistory(String)} only needs to be called once,
+ * then the result is kept, and call {@link #getExistingAccountKey()} to get the result.
+ */
+ @VisibleForTesting
+ static final class FastPairHistoryFinder {
+
+ private @Nullable
+ byte[] mExistingAccountKey;
+ @Nullable
+ private final List<FastPairHistoryItem> mHistoryItems;
+
+ FastPairHistoryFinder(List<FastPairHistoryItem> historyItems) {
+ this.mHistoryItems = historyItems;
+ }
+
+ @WorkerThread
+ @VisibleForTesting
+ boolean isInPairedHistory(String publicAddress) {
+ if (mHistoryItems == null || mHistoryItems.isEmpty()) {
+ return false;
+ }
+ for (FastPairHistoryItem item : mHistoryItems) {
+ if (item.isMatched(BluetoothAddress.decode(publicAddress))) {
+ mExistingAccountKey = item.accountKey().toByteArray();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // This function should be called after isInPairedHistory(). Or it will just return null.
+ @WorkerThread
+ @VisibleForTesting
+ @Nullable
+ byte[] getExistingAccountKey() {
+ return mExistingAccountKey;
+ }
+ }
+
+ private static final class DeviceNameReceiver {
+
+ @GuardedBy("this")
+ private @Nullable
+ byte[] mEncryptedResponse;
+
+ @GuardedBy("this")
+ @Nullable
+ private String mDecryptedDeviceName;
+
+ @Nullable
+ private CountDownLatch mResponseCountDown;
+
+ DeviceNameReceiver(BluetoothGattConnection gattConnection) throws BluetoothException {
+ UUID characteristicUuid = NameCharacteristic.getId(gattConnection);
+ ChangeObserver observer =
+ gattConnection.enableNotification(FastPairService.ID, characteristicUuid);
+ observer.setListener(
+ (byte[] value) -> {
+ synchronized (DeviceNameReceiver.this) {
+ Log.i(TAG, "DeviceNameReceiver: device name response size = "
+ + value.length);
+ // We don't decrypt it here because we may not finish handshaking and
+ // the pairing
+ // secret is not available.
+ mEncryptedResponse = value;
+ }
+ // For testing to know we get the device name from provider.
+ if (mResponseCountDown != null) {
+ mResponseCountDown.countDown();
+ Log.v(TAG, "Finish nameResponseCountDown.");
+ }
+ });
+ }
+
+ void setCountDown(CountDownLatch countDownLatch) {
+ this.mResponseCountDown = countDownLatch;
+ }
+
+ synchronized @Nullable String getParsedResult(byte[] secret) {
+ if (mDecryptedDeviceName != null) {
+ return mDecryptedDeviceName;
+ }
+ if (mEncryptedResponse == null) {
+ Log.i(TAG, "DeviceNameReceiver: no device name sent from the Provider.");
+ return null;
+ }
+ try {
+ mDecryptedDeviceName = NamingEncoder.decodeNamingPacket(secret, mEncryptedResponse);
+ Log.i(TAG, "DeviceNameReceiver: decrypted provider's name from naming response, "
+ + "name = " + mDecryptedDeviceName);
+ } catch (GeneralSecurityException e) {
+ Log.w(TAG, "DeviceNameReceiver: fail to parse the NameCharacteristic from provider"
+ + ".", e);
+ return null;
+ }
+ return mDecryptedDeviceName;
+ }
+ }
+
+ static void checkFastPairSignal(
+ FastPairSignalChecker fastPairSignalChecker,
+ String currentAddress,
+ Exception originalException)
+ throws SignalLostException, SignalRotatedException {
+ String newAddress = fastPairSignalChecker.getValidAddressForModelId(currentAddress);
+ if (TextUtils.isEmpty(newAddress)) {
+ throw new SignalLostException("Signal lost", originalException);
+ } else if (!Ascii.equalsIgnoreCase(currentAddress, newAddress)) {
+ throw new SignalRotatedException("Address rotated", newAddress, originalException);
+ }
+ }
+
+ @VisibleForTesting
+ public Preferences getPreferences() {
+ return mPreferences;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairHistoryItem.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairHistoryItem.java
new file mode 100644
index 0000000..e774886
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairHistoryItem.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.primitives.Bytes.concat;
+
+import com.google.common.hash.Hashing;
+import com.google.protobuf.ByteString;
+
+import java.util.Arrays;
+
+/**
+ * It contains the sha256 of "account key + headset's public address" to identify the headset which
+ * has paired with the account. Previously, account key is the only information for Fast Pair to
+ * identify the headset, but Fast Pair can't identify the headset in initial pairing, there is no
+ * account key data advertising from headset.
+ */
+public class FastPairHistoryItem {
+
+ private final ByteString mAccountKey;
+ private final ByteString mSha256AccountKeyPublicAddress;
+
+ FastPairHistoryItem(ByteString accountkey, ByteString sha256AccountKeyPublicAddress) {
+ mAccountKey = accountkey;
+ mSha256AccountKeyPublicAddress = sha256AccountKeyPublicAddress;
+ }
+
+ /**
+ * Creates an instance of {@link FastPairHistoryItem}.
+ *
+ * @param accountKey key of an account that has paired with the headset.
+ * @param sha256AccountKeyPublicAddress hash value of account key and headset's public address.
+ */
+ public static FastPairHistoryItem create(
+ ByteString accountKey, ByteString sha256AccountKeyPublicAddress) {
+ return new FastPairHistoryItem(accountKey, sha256AccountKeyPublicAddress);
+ }
+
+ ByteString accountKey() {
+ return mAccountKey;
+ }
+
+ ByteString sha256AccountKeyPublicAddress() {
+ return mSha256AccountKeyPublicAddress;
+ }
+
+ // Return true if the input public address is considered the same as this history item. Because
+ // of privacy concern, Fast Pair does not really store the public address, it is identified by
+ // the SHA256 of the account key and the public key.
+ final boolean isMatched(byte[] publicAddress) {
+ return Arrays.equals(
+ sha256AccountKeyPublicAddress().toByteArray(),
+ Hashing.sha256().hashBytes(concat(accountKey().toByteArray(), publicAddress))
+ .asBytes());
+ }
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManager.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManager.java
new file mode 100644
index 0000000..e7ce4bf
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManager.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.AES_BLOCK_LENGTH;
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
+import static com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection.logRetrySuccessEvent;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.util.Consumer;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.BluetoothGattException;
+import com.android.server.nearby.common.bluetooth.BluetoothTimeoutException;
+import com.android.server.nearby.common.bluetooth.fastpair.TimingLogger.ScopedTiming;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.ConnectionOptions;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
+import com.android.server.nearby.intdefs.FastPairEventIntDefs.ErrorCode;
+import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Manager for working with Gatt connections.
+ *
+ * <p>This helper class allows for opening and closing GATT connections to a provided address.
+ * Optionally, it can also support automatically reopening a connection in the case that it has been
+ * closed when it's next needed through {@link Preferences#getAutomaticallyReconnectGattWhenNeeded}.
+ */
+// TODO(b/202524672): Add class unit test.
+final class GattConnectionManager {
+
+ private static final String TAG = GattConnectionManager.class.getSimpleName();
+
+ private final Context mContext;
+ private final Preferences mPreferences;
+ private final EventLoggerWrapper mEventLogger;
+ private final BluetoothAdapter mBluetoothAdapter;
+ private final ToggleBluetoothTask mToggleBluetooth;
+ private final String mAddress;
+ private final TimingLogger mTimingLogger;
+ private final boolean mSetMtu;
+ @Nullable
+ private final FastPairConnection.FastPairSignalChecker mFastPairSignalChecker;
+ @Nullable
+ private BluetoothGattConnection mGattConnection;
+ private static boolean sTestMode = false;
+
+ static void enableTestMode() {
+ sTestMode = true;
+ }
+
+ GattConnectionManager(
+ Context context,
+ Preferences preferences,
+ EventLoggerWrapper eventLogger,
+ BluetoothAdapter bluetoothAdapter,
+ ToggleBluetoothTask toggleBluetooth,
+ String address,
+ TimingLogger timingLogger,
+ @Nullable FastPairConnection.FastPairSignalChecker fastPairSignalChecker,
+ boolean setMtu) {
+ this.mContext = context;
+ this.mPreferences = preferences;
+ this.mEventLogger = eventLogger;
+ this.mBluetoothAdapter = bluetoothAdapter;
+ this.mToggleBluetooth = toggleBluetooth;
+ this.mAddress = address;
+ this.mTimingLogger = timingLogger;
+ this.mFastPairSignalChecker = fastPairSignalChecker;
+ this.mSetMtu = setMtu;
+ }
+
+ /**
+ * Gets a gatt connection to address. If this connection does not exist, it creates one.
+ */
+ BluetoothGattConnection getConnection()
+ throws InterruptedException, ExecutionException, TimeoutException, BluetoothException {
+ if (mGattConnection == null) {
+ try {
+ mGattConnection =
+ connect(mAddress, /* checkSignalWhenFail= */ false,
+ /* rescueFromError= */ null);
+ } catch (SignalLostException | SignalRotatedException e) {
+ // Impossible to happen here because we didn't do signal check.
+ throw new ExecutionException("getConnection throws SignalLostException", e);
+ }
+ }
+ return mGattConnection;
+ }
+
+ BluetoothGattConnection getConnectionWithSignalLostCheck(
+ @Nullable Consumer<Integer> rescueFromError)
+ throws InterruptedException, ExecutionException, TimeoutException, BluetoothException,
+ SignalLostException, SignalRotatedException {
+ if (mGattConnection == null) {
+ mGattConnection = connect(mAddress, /* checkSignalWhenFail= */ true,
+ rescueFromError);
+ }
+ return mGattConnection;
+ }
+
+ /**
+ * Closes the gatt connection when it is open.
+ */
+ void closeConnection() throws BluetoothException {
+ if (mGattConnection != null) {
+ try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Close GATT")) {
+ mGattConnection.close();
+ mGattConnection = null;
+ }
+ }
+ }
+
+ private BluetoothGattConnection connect(
+ String address, boolean checkSignalWhenFail,
+ @Nullable Consumer<Integer> rescueFromError)
+ throws InterruptedException, ExecutionException, TimeoutException, BluetoothException,
+ SignalLostException, SignalRotatedException {
+ int i = 1;
+ boolean isRecoverable = true;
+ long startElapsedRealtime = SystemClock.elapsedRealtime();
+ BluetoothException lastException = null;
+ mEventLogger.setCurrentEvent(EventCode.GATT_CONNECT);
+ while (isRecoverable) {
+ try (ScopedTiming scopedTiming =
+ new ScopedTiming(mTimingLogger, "Connect GATT #" + i)) {
+ Log.i(TAG, "Connecting to GATT server at " + maskBluetoothAddress(address));
+ if (sTestMode) {
+ return null;
+ }
+ BluetoothGattConnection connection =
+ new BluetoothGattHelper(mContext, mBluetoothAdapter)
+ .connect(
+ mBluetoothAdapter.getRemoteDevice(address),
+ getConnectionOptions(startElapsedRealtime));
+ connection.setOperationTimeout(
+ TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
+ if (mPreferences.getAutomaticallyReconnectGattWhenNeeded()) {
+ connection.addCloseListener(
+ () -> {
+ Log.i(TAG, "Gatt connection with " + maskBluetoothAddress(address)
+ + " closed.");
+ mGattConnection = null;
+ });
+ }
+ mEventLogger.logCurrentEventSucceeded();
+ if (lastException != null) {
+ logRetrySuccessEvent(EventCode.RECOVER_BY_RETRY_GATT, lastException,
+ mEventLogger);
+ }
+ return connection;
+ } catch (BluetoothException e) {
+ lastException = e;
+
+ boolean ableToRetry;
+ if (mPreferences.getGattConnectRetryTimeoutMillis() > 0) {
+ ableToRetry =
+ (SystemClock.elapsedRealtime() - startElapsedRealtime)
+ < mPreferences.getGattConnectRetryTimeoutMillis();
+ Log.i(TAG, "Retry connecting GATT by timeout: " + ableToRetry);
+ } else {
+ ableToRetry = i < mPreferences.getNumAttempts();
+ }
+
+ if (mPreferences.getRetryGattConnectionAndSecretHandshake()) {
+ if (isNoRetryError(mPreferences, e)) {
+ ableToRetry = false;
+ }
+
+ if (ableToRetry) {
+ if (rescueFromError != null) {
+ rescueFromError.accept(
+ e instanceof BluetoothOperationTimeoutException
+ ? ErrorCode.SUCCESS_RETRY_GATT_TIMEOUT
+ : ErrorCode.SUCCESS_RETRY_GATT_ERROR);
+ }
+ if (mFastPairSignalChecker != null && checkSignalWhenFail) {
+ FastPairDualConnection
+ .checkFastPairSignal(mFastPairSignalChecker, address, e);
+ }
+ }
+ isRecoverable = ableToRetry;
+ if (ableToRetry && mPreferences.getPairingRetryDelayMs() > 0) {
+ SystemClock.sleep(mPreferences.getPairingRetryDelayMs());
+ }
+ } else {
+ isRecoverable =
+ ableToRetry
+ && (e instanceof BluetoothOperationTimeoutException
+ || e instanceof BluetoothTimeoutException
+ || (e instanceof BluetoothGattException
+ && ((BluetoothGattException) e).getGattErrorCode() == 133));
+ }
+ Log.w(TAG, "GATT connect attempt " + i + "of " + mPreferences.getNumAttempts()
+ + " failed, " + (isRecoverable ? "recovering" : "permanently"), e);
+ if (isRecoverable) {
+ // If we're going to retry, log failure here. If we throw, an upper level will
+ // log it.
+ mToggleBluetooth.toggleBluetooth();
+ i++;
+ mEventLogger.logCurrentEventFailed(e);
+ mEventLogger.setCurrentEvent(EventCode.GATT_CONNECT);
+ }
+ }
+ }
+ throw checkNotNull(lastException);
+ }
+
+ static boolean isNoRetryError(Preferences preferences, BluetoothException e) {
+ return e instanceof BluetoothGattException
+ && preferences
+ .getGattConnectionAndSecretHandshakeNoRetryGattError()
+ .contains(((BluetoothGattException) e).getGattErrorCode());
+ }
+
+ @VisibleForTesting
+ long getTimeoutMs(long spentTime) {
+ long timeoutInMs;
+ if (mPreferences.getRetryGattConnectionAndSecretHandshake()) {
+ timeoutInMs =
+ spentTime < mPreferences.getGattConnectShortTimeoutRetryMaxSpentTimeMs()
+ ? mPreferences.getGattConnectShortTimeoutMs()
+ : mPreferences.getGattConnectLongTimeoutMs();
+ } else {
+ timeoutInMs = TimeUnit.SECONDS.toMillis(mPreferences.getGattConnectionTimeoutSeconds());
+ }
+ return timeoutInMs;
+ }
+
+ private ConnectionOptions getConnectionOptions(long startElapsedRealtime) {
+ return createConnectionOptions(
+ mSetMtu,
+ getTimeoutMs(SystemClock.elapsedRealtime() - startElapsedRealtime));
+ }
+
+ public static ConnectionOptions createConnectionOptions(boolean setMtu, long timeoutInMs) {
+ ConnectionOptions.Builder builder = ConnectionOptions.builder();
+ if (setMtu) {
+ // There are 3 overhead bytes added to BLE packets.
+ builder.setMtu(
+ AES_BLOCK_LENGTH + EllipticCurveDiffieHellmanExchange.PUBLIC_KEY_LENGTH + 3);
+ }
+ builder.setConnectionTimeoutMillis(timeoutInMs);
+ return builder.build();
+ }
+
+ @VisibleForTesting
+ void setGattConnection(BluetoothGattConnection gattConnection) {
+ this.mGattConnection = gattConnection;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandler.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandler.java
new file mode 100644
index 0000000..984133b
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandler.java
@@ -0,0 +1,560 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.AES_BLOCK_LENGTH;
+import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.decrypt;
+import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.encrypt;
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.BLUETOOTH_ADDRESS_LENGTH;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.ADDITIONAL_DATA_CHARACTERISTIC;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.DEVICE_ACTION;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.ADDITIONAL_DATA_TYPE_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.EVENT_ADDITIONAL_DATA_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.EVENT_ADDITIONAL_DATA_LENGTH_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.EVENT_CODE_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.EVENT_GROUP_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.FLAGS_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.SEEKER_PUBLIC_ADDRESS_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.TYPE_ACTION_OVER_BLE;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.TYPE_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.TYPE_KEY_BASED_PAIRING_REQUEST;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.VERIFICATION_DATA_INDEX;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.VERIFICATION_DATA_LENGTH;
+import static com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection.logRetrySuccessEvent;
+import static com.android.server.nearby.common.bluetooth.fastpair.GattConnectionManager.isNoRetryError;
+
+import static com.google.common.base.Verify.verifyNotNull;
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.primitives.Bytes.concat;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.util.Consumer;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.BluetoothTimeoutException;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AdditionalDataCharacteristic.AdditionalDataType;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request;
+import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection.SharedSecret;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection.ChangeObserver;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
+import com.android.server.nearby.intdefs.FastPairEventIntDefs.ErrorCode;
+import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode;
+
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Handles the handshake step of Fast Pair, the Provider's public address and the shared secret will
+ * be disclosed during this step. It is the first step of all key-based operations, e.g. key-based
+ * pairing and action over BLE.
+ *
+ * @see <a href="https://developers.google.com/nearby/fast-pair/spec#procedure">
+ * Fastpair Spec Procedure</a>
+ */
+public class HandshakeHandler {
+
+ private static final String TAG = HandshakeHandler.class.getSimpleName();
+ private final GattConnectionManager mGattConnectionManager;
+ private final String mProviderBleAddress;
+ private final Preferences mPreferences;
+ private final EventLoggerWrapper mEventLogger;
+ @Nullable
+ private final FastPairConnection.FastPairSignalChecker mFastPairSignalChecker;
+
+ /**
+ * Keeps the keys used during handshaking, generated by {@link #createKey(byte[])}.
+ */
+ private static final class Keys {
+
+ private final byte[] mSharedSecret;
+ private final byte[] mPublicKey;
+
+ private Keys(byte[] sharedSecret, byte[] publicKey) {
+ this.mSharedSecret = sharedSecret;
+ this.mPublicKey = publicKey;
+ }
+ }
+
+ public HandshakeHandler(
+ GattConnectionManager gattConnectionManager,
+ String bleAddress,
+ Preferences preferences,
+ EventLoggerWrapper eventLogger,
+ @Nullable FastPairConnection.FastPairSignalChecker fastPairSignalChecker) {
+ this.mGattConnectionManager = gattConnectionManager;
+ this.mProviderBleAddress = bleAddress;
+ this.mPreferences = preferences;
+ this.mEventLogger = eventLogger;
+ this.mFastPairSignalChecker = fastPairSignalChecker;
+ }
+
+ /**
+ * Performs a handshake to authenticate and get the remote device's public address. Returns the
+ * AES-128 key as the shared secret for this pairing session.
+ */
+ public SharedSecret doHandshake(byte[] key, HandshakeMessage message)
+ throws GeneralSecurityException, InterruptedException, ExecutionException,
+ TimeoutException, BluetoothException, PairingException {
+ Keys keys = createKey(key);
+ Log.i(TAG,
+ "Handshake " + maskBluetoothAddress(mProviderBleAddress) + ", flags "
+ + message.mFlags);
+ byte[] handshakeResponse =
+ processGattCommunication(
+ createPacket(keys, message),
+ SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
+ String providerPublicAddress = decodeResponse(keys.mSharedSecret, handshakeResponse);
+
+ return SharedSecret.create(keys.mSharedSecret, providerPublicAddress);
+ }
+
+ /**
+ * Performs a handshake to authenticate and get the remote device's public address. Returns the
+ * AES-128 key as the shared secret for this pairing session. Will retry and also performs
+ * FastPair signal check if fails.
+ */
+ public SharedSecret doHandshakeWithRetryAndSignalLostCheck(
+ byte[] key, HandshakeMessage message, @Nullable Consumer<Integer> rescueFromError)
+ throws GeneralSecurityException, InterruptedException, ExecutionException,
+ TimeoutException, BluetoothException, PairingException {
+ Keys keys = createKey(key);
+ Log.i(TAG,
+ "Handshake " + maskBluetoothAddress(mProviderBleAddress) + ", flags "
+ + message.mFlags);
+ int retryCount = 0;
+ byte[] handshakeResponse = null;
+ long startTime = SystemClock.elapsedRealtime();
+ BluetoothException lastException = null;
+ do {
+ try {
+ mEventLogger.setCurrentEvent(EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+ handshakeResponse =
+ processGattCommunication(
+ createPacket(keys, message),
+ getTimeoutMs(SystemClock.elapsedRealtime() - startTime));
+ mEventLogger.logCurrentEventSucceeded();
+ if (lastException != null) {
+ logRetrySuccessEvent(EventCode.RECOVER_BY_RETRY_HANDSHAKE, lastException,
+ mEventLogger);
+ }
+ } catch (BluetoothException e) {
+ lastException = e;
+ long spentTime = SystemClock.elapsedRealtime() - startTime;
+ Log.w(TAG, "Secret handshake failed, address="
+ + maskBluetoothAddress(mProviderBleAddress)
+ + ", spent time=" + spentTime + "ms, retryCount=" + retryCount);
+ mEventLogger.logCurrentEventFailed(e);
+
+ if (!mPreferences.getRetryGattConnectionAndSecretHandshake()) {
+ throw e;
+ }
+
+ if (spentTime > mPreferences.getSecretHandshakeLongTimeoutRetryMaxSpentTimeMs()) {
+ Log.w(TAG, "Spent too long time for handshake, timeInMs=" + spentTime);
+ throw e;
+ }
+ if (isNoRetryError(mPreferences, e)) {
+ throw e;
+ }
+
+ if (mFastPairSignalChecker != null) {
+ FastPairDualConnection
+ .checkFastPairSignal(mFastPairSignalChecker, mProviderBleAddress, e);
+ }
+ retryCount++;
+ if (retryCount > mPreferences.getSecretHandshakeRetryAttempts()
+ || ((e instanceof BluetoothOperationTimeoutException)
+ && !mPreferences.getRetrySecretHandshakeTimeout())) {
+ throw new HandshakeException("Fail on handshake!", e);
+ }
+ if (rescueFromError != null) {
+ rescueFromError.accept(
+ (e instanceof BluetoothTimeoutException
+ || e instanceof BluetoothOperationTimeoutException)
+ ? ErrorCode.SUCCESS_RETRY_SECRET_HANDSHAKE_TIMEOUT
+ : ErrorCode.SUCCESS_RETRY_SECRET_HANDSHAKE_ERROR);
+ }
+ }
+ } while (mPreferences.getRetryGattConnectionAndSecretHandshake()
+ && handshakeResponse == null);
+ if (retryCount > 0) {
+ Log.i(TAG, "Secret handshake failed but restored by retry, retry count=" + retryCount);
+ }
+ String providerPublicAddress =
+ decodeResponse(keys.mSharedSecret, verifyNotNull(handshakeResponse));
+
+ return SharedSecret.create(keys.mSharedSecret, providerPublicAddress);
+ }
+
+ @VisibleForTesting
+ long getTimeoutMs(long spentTime) {
+ if (!mPreferences.getRetryGattConnectionAndSecretHandshake()) {
+ return SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds());
+ } else {
+ return spentTime < mPreferences.getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs()
+ ? mPreferences.getSecretHandshakeShortTimeoutMs()
+ : mPreferences.getSecretHandshakeLongTimeoutMs();
+ }
+ }
+
+ /**
+ * If the given key is an ecc-256 public key (currently, we are using secp256r1), the shared
+ * secret is generated by ECDH; if the input key is AES-128 key (should be the account key),
+ * then it is the shared secret.
+ */
+ private Keys createKey(byte[] key) throws GeneralSecurityException {
+ if (key.length == EllipticCurveDiffieHellmanExchange.PUBLIC_KEY_LENGTH) {
+ EllipticCurveDiffieHellmanExchange exchange = EllipticCurveDiffieHellmanExchange
+ .create();
+ byte[] publicKey = exchange.getPublicKey();
+ if (publicKey != null) {
+ Log.i(TAG, "Handshake " + maskBluetoothAddress(mProviderBleAddress)
+ + ", generates key by ECDH.");
+ } else {
+ throw new GeneralSecurityException("Failed to do ECDH.");
+ }
+ return new Keys(exchange.generateSecret(key), publicKey);
+ } else if (key.length == AesEcbSingleBlockEncryption.KEY_LENGTH) {
+ Log.i(TAG, "Handshake " + maskBluetoothAddress(mProviderBleAddress)
+ + ", using the given secret.");
+ return new Keys(key, new byte[0]);
+ } else {
+ throw new GeneralSecurityException("Key length is not correct: " + key.length);
+ }
+ }
+
+ private static byte[] createPacket(Keys keys, HandshakeMessage message)
+ throws GeneralSecurityException {
+ byte[] encryptedMessage = encrypt(keys.mSharedSecret, message.getBytes());
+ return concat(encryptedMessage, keys.mPublicKey);
+ }
+
+ private byte[] processGattCommunication(byte[] packet, long gattOperationTimeoutMS)
+ throws BluetoothException, InterruptedException, ExecutionException, TimeoutException {
+ BluetoothGattConnection gattConnection = mGattConnectionManager.getConnection();
+ gattConnection.setOperationTimeout(gattOperationTimeoutMS);
+ UUID characteristicUuid = KeyBasedPairingCharacteristic.getId(gattConnection);
+ ChangeObserver changeObserver =
+ gattConnection.enableNotification(FastPairService.ID, characteristicUuid);
+
+ Log.i(TAG,
+ "Writing handshake packet to address=" + maskBluetoothAddress(mProviderBleAddress));
+ gattConnection.writeCharacteristic(FastPairService.ID, characteristicUuid, packet);
+ Log.i(TAG, "Waiting handshake packet from address=" + maskBluetoothAddress(
+ mProviderBleAddress));
+ return changeObserver.waitForUpdate(gattOperationTimeoutMS);
+ }
+
+ private String decodeResponse(byte[] sharedSecret, byte[] response)
+ throws PairingException, GeneralSecurityException {
+ if (response.length != AES_BLOCK_LENGTH) {
+ throw new PairingException(
+ "Handshake failed because of incorrect response: " + base16().encode(response));
+ }
+ // 1 byte type, 6 bytes public address, remainder random salt.
+ byte[] decryptedResponse = decrypt(sharedSecret, response);
+ if (decryptedResponse[0] != KeyBasedPairingCharacteristic.Response.TYPE) {
+ throw new PairingException(
+ "Handshake response type incorrect: " + decryptedResponse[0]);
+ }
+ String address = BluetoothAddress.encode(Arrays.copyOfRange(decryptedResponse, 1, 7));
+ Log.i(TAG, "Handshake success with public " + maskBluetoothAddress(address) + ", ble "
+ + maskBluetoothAddress(mProviderBleAddress));
+ return address;
+ }
+
+ /**
+ * The base class for handshake message that contains the common data: message type, flags and
+ * verification data.
+ */
+ abstract static class HandshakeMessage {
+
+ final byte mType;
+ final byte mFlags;
+ private final byte[] mVerificationData;
+
+ HandshakeMessage(Builder<?> builder) {
+ this.mType = builder.mType;
+ this.mVerificationData = builder.mVerificationData;
+ this.mFlags = builder.mFlags;
+ }
+
+ abstract static class Builder<T extends Builder<T>> {
+
+ byte mType;
+ byte mFlags;
+ private byte[] mVerificationData;
+
+ abstract T getThis();
+
+ T setVerificationData(byte[] verificationData) {
+ if (verificationData.length != BLUETOOTH_ADDRESS_LENGTH) {
+ throw new IllegalArgumentException(
+ "Incorrect verification data length: " + verificationData.length + ".");
+ }
+ this.mVerificationData = verificationData;
+ return getThis();
+ }
+ }
+
+ /**
+ * Constructs the base handshake message according to the format of Fast Pair spec.
+ */
+ byte[] constructBaseBytes() {
+ byte[] rawMessage = new byte[Request.SIZE];
+ new SecureRandom().nextBytes(rawMessage);
+ rawMessage[TYPE_INDEX] = mType;
+ rawMessage[FLAGS_INDEX] = mFlags;
+
+ System.arraycopy(
+ mVerificationData,
+ /* srcPos= */ 0,
+ rawMessage,
+ VERIFICATION_DATA_INDEX,
+ VERIFICATION_DATA_LENGTH);
+ return rawMessage;
+ }
+
+ /**
+ * Returns the raw handshake message.
+ */
+ abstract byte[] getBytes();
+ }
+
+ /**
+ * Extends {@link HandshakeMessage} and contains the required data for key-based pairing
+ * request.
+ */
+ public static class KeyBasedPairingRequest extends HandshakeMessage {
+
+ @Nullable
+ private final byte[] mSeekerPublicAddress;
+
+ private KeyBasedPairingRequest(Builder builder) {
+ super(builder);
+ this.mSeekerPublicAddress = builder.mSeekerPublicAddress;
+ }
+
+ @Override
+ byte[] getBytes() {
+ byte[] rawMessage = constructBaseBytes();
+ if (mSeekerPublicAddress != null) {
+ System.arraycopy(
+ mSeekerPublicAddress,
+ /* srcPos= */ 0,
+ rawMessage,
+ SEEKER_PUBLIC_ADDRESS_INDEX,
+ BLUETOOTH_ADDRESS_LENGTH);
+ }
+ Log.i(TAG,
+ "Handshake Message: type (" + rawMessage[TYPE_INDEX] + "), flag ("
+ + rawMessage[FLAGS_INDEX] + ").");
+ return rawMessage;
+ }
+
+ /**
+ * Builder class for key-based pairing request.
+ */
+ public static class Builder extends HandshakeMessage.Builder<Builder> {
+
+ @Nullable
+ private byte[] mSeekerPublicAddress;
+
+ /**
+ * Adds flags without changing other flags.
+ */
+ public Builder addFlag(@KeyBasedPairingRequestFlag int flag) {
+ this.mFlags |= (byte) flag;
+ return this;
+ }
+
+ /**
+ * Set seeker's public address.
+ */
+ public Builder setSeekerPublicAddress(byte[] seekerPublicAddress) {
+ this.mSeekerPublicAddress = seekerPublicAddress;
+ return this;
+ }
+
+ /**
+ * Buulds KeyBasedPairigRequest.
+ */
+ public KeyBasedPairingRequest build() {
+ mType = TYPE_KEY_BASED_PAIRING_REQUEST;
+ return new KeyBasedPairingRequest(this);
+ }
+
+ @Override
+ Builder getThis() {
+ return this;
+ }
+ }
+ }
+
+ /**
+ * Extends {@link HandshakeMessage} and contains the required data for action over BLE request.
+ */
+ public static class ActionOverBle extends HandshakeMessage {
+
+ private final byte mEventGroup;
+ private final byte mEventCode;
+ @Nullable
+ private final byte[] mEventData;
+ private final byte mAdditionalDataType;
+
+ private ActionOverBle(Builder builder) {
+ super(builder);
+ this.mEventGroup = builder.mEventGroup;
+ this.mEventCode = builder.mEventCode;
+ this.mEventData = builder.mEventData;
+ this.mAdditionalDataType = builder.mAdditionalDataType;
+ }
+
+ @Override
+ byte[] getBytes() {
+ byte[] rawMessage = constructBaseBytes();
+ StringBuilder stringBuilder =
+ new StringBuilder(
+ String.format(
+ "type (%02X), flag (%02X)", rawMessage[TYPE_INDEX],
+ rawMessage[FLAGS_INDEX]));
+ if ((mFlags & (byte) DEVICE_ACTION) != 0) {
+ rawMessage[EVENT_GROUP_INDEX] = mEventGroup;
+ rawMessage[EVENT_CODE_INDEX] = mEventCode;
+
+ if (mEventData != null) {
+ rawMessage[EVENT_ADDITIONAL_DATA_LENGTH_INDEX] = (byte) mEventData.length;
+ System.arraycopy(
+ mEventData,
+ /* srcPos= */ 0,
+ rawMessage,
+ EVENT_ADDITIONAL_DATA_INDEX,
+ mEventData.length);
+ } else {
+ rawMessage[EVENT_ADDITIONAL_DATA_LENGTH_INDEX] = (byte) 0;
+ }
+ stringBuilder.append(
+ String.format(
+ ", group(%02X), code(%02X), length(%02X)",
+ rawMessage[EVENT_GROUP_INDEX],
+ rawMessage[EVENT_CODE_INDEX],
+ rawMessage[EVENT_ADDITIONAL_DATA_LENGTH_INDEX]));
+ }
+ if ((mFlags & (byte) ADDITIONAL_DATA_CHARACTERISTIC) != 0) {
+ rawMessage[ADDITIONAL_DATA_TYPE_INDEX] = mAdditionalDataType;
+ stringBuilder.append(
+ String.format(", data id(%02X)", rawMessage[ADDITIONAL_DATA_TYPE_INDEX]));
+ }
+ Log.i(TAG, "Handshake Message: " + stringBuilder);
+ return rawMessage;
+ }
+
+ /**
+ * Builder class for action over BLE request.
+ */
+ public static class Builder extends HandshakeMessage.Builder<Builder> {
+
+ private byte mEventGroup;
+ private byte mEventCode;
+ @Nullable
+ private byte[] mEventData;
+ private byte mAdditionalDataType;
+
+ // Adds a flag to this handshake message. This can be called repeatedly for adding
+ // different preference.
+
+ /**
+ * Adds flag without changing other flags.
+ */
+ public Builder addFlag(@ActionOverBleFlag int flag) {
+ this.mFlags |= (byte) flag;
+ return this;
+ }
+
+ /**
+ * Set event group and event code.
+ */
+ public Builder setEvent(int eventGroup, int eventCode) {
+ this.mFlags |= (byte) DEVICE_ACTION;
+ this.mEventGroup = (byte) (eventGroup & 0xFF);
+ this.mEventCode = (byte) (eventCode & 0xFF);
+ return this;
+ }
+
+ /**
+ * Set event additional data.
+ */
+ public Builder setEventAdditionalData(byte[] data) {
+ this.mEventData = data;
+ return this;
+ }
+
+ /**
+ * Set event additional data type.
+ */
+ public Builder setAdditionalDataType(@AdditionalDataType int additionalDataType) {
+ this.mFlags |= (byte) ADDITIONAL_DATA_CHARACTERISTIC;
+ this.mAdditionalDataType = (byte) additionalDataType;
+ return this;
+ }
+
+ @Override
+ Builder getThis() {
+ return this;
+ }
+
+ ActionOverBle build() {
+ mType = TYPE_ACTION_OVER_BLE;
+ return new ActionOverBle(this);
+ }
+ }
+ }
+
+ /**
+ * Exception for handshake failure.
+ */
+ public static class HandshakeException extends PairingException {
+
+ private final BluetoothException mOriginalException;
+
+ @VisibleForTesting
+ HandshakeException(String format, BluetoothException e) {
+ super(format);
+ mOriginalException = e;
+ }
+
+ public BluetoothException getOriginalException() {
+ return mOriginalException;
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HeadsetPiece.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HeadsetPiece.java
new file mode 100644
index 0000000..26ff79f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HeadsetPiece.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.Nullable;
+import androidx.core.content.FileProvider;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * This class is subclass of real headset. It contains image url, battery value and charging
+ * status.
+ */
+public class HeadsetPiece implements Parcelable {
+ private int mLowLevelThreshold;
+ private int mBatteryLevel;
+ private String mImageUrl;
+ private boolean mCharging;
+ private Uri mImageContentUri;
+
+ private HeadsetPiece(
+ int lowLevelThreshold,
+ int batteryLevel,
+ String imageUrl,
+ boolean charging,
+ @Nullable Uri imageContentUri) {
+ this.mLowLevelThreshold = lowLevelThreshold;
+ this.mBatteryLevel = batteryLevel;
+ this.mImageUrl = imageUrl;
+ this.mCharging = charging;
+ this.mImageContentUri = imageContentUri;
+ }
+
+ /**
+ * Returns a builder of HeadsetPiece.
+ */
+ public static HeadsetPiece.Builder builder() {
+ return new HeadsetPiece.Builder();
+ }
+
+ /**
+ * The low level threshold.
+ */
+ public int lowLevelThreshold() {
+ return mLowLevelThreshold;
+ }
+
+ /**
+ * The battery level.
+ */
+ public int batteryLevel() {
+ return mBatteryLevel;
+ }
+
+ /**
+ * The web URL of the image.
+ */
+ public String imageUrl() {
+ return mImageUrl;
+ }
+
+ /**
+ * Whether the headset is charging.
+ */
+ public boolean charging() {
+ return mCharging;
+ }
+
+ /**
+ * The content Uri of the image if it could be downloaded from the web URL and generated through
+ * {@link FileProvider#getUriForFile} successfully, otherwise null.
+ */
+ @Nullable
+ public Uri imageContentUri() {
+ return mImageContentUri;
+ }
+
+ /**
+ * @return whether battery is low or not.
+ */
+ public boolean isBatteryLow() {
+ return batteryLevel() <= lowLevelThreshold() && batteryLevel() >= 0 && !charging();
+ }
+
+ @Override
+ public String toString() {
+ return "HeadsetPiece{"
+ + "lowLevelThreshold=" + mLowLevelThreshold + ", "
+ + "batteryLevel=" + mBatteryLevel + ", "
+ + "imageUrl=" + mImageUrl + ", "
+ + "charging=" + mCharging + ", "
+ + "imageContentUri=" + mImageContentUri
+ + "}";
+ }
+
+ /**
+ * Builder function for headset piece.
+ */
+ public static class Builder {
+ private int mLowLevelThreshold;
+ private int mBatteryLevel;
+ private String mImageUrl;
+ private boolean mCharging;
+ private Uri mImageContentUri;
+
+ /**
+ * Set low level threshold.
+ */
+ public HeadsetPiece.Builder setLowLevelThreshold(int lowLevelThreshold) {
+ this.mLowLevelThreshold = lowLevelThreshold;
+ return this;
+ }
+
+ /**
+ * Set battery level.
+ */
+ public HeadsetPiece.Builder setBatteryLevel(int level) {
+ this.mBatteryLevel = level;
+ return this;
+ }
+
+ /**
+ * Set image url.
+ */
+ public HeadsetPiece.Builder setImageUrl(String url) {
+ this.mImageUrl = url;
+ return this;
+ }
+
+ /**
+ * Set charging.
+ */
+ public HeadsetPiece.Builder setCharging(boolean charging) {
+ this.mCharging = charging;
+ return this;
+ }
+
+ /**
+ * Set image content Uri.
+ */
+ public HeadsetPiece.Builder setImageContentUri(Uri uri) {
+ this.mImageContentUri = uri;
+ return this;
+ }
+
+ /**
+ * Builds HeadSetPiece.
+ */
+ public HeadsetPiece build() {
+ return new HeadsetPiece(mLowLevelThreshold, mBatteryLevel, mImageUrl, mCharging,
+ mImageContentUri);
+ }
+ }
+
+ @Override
+ public final void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(imageUrl());
+ dest.writeInt(lowLevelThreshold());
+ dest.writeInt(batteryLevel());
+ // Writes 1 if charging, otherwise 0.
+ dest.writeByte((byte) (charging() ? 1 : 0));
+ dest.writeParcelable(imageContentUri(), flags);
+ }
+
+ @Override
+ public final int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<HeadsetPiece> CREATOR =
+ new Creator<HeadsetPiece>() {
+ @Override
+ public HeadsetPiece createFromParcel(Parcel in) {
+ String imageUrl = in.readString();
+ return HeadsetPiece.builder()
+ .setImageUrl(imageUrl != null ? imageUrl : "")
+ .setLowLevelThreshold(in.readInt())
+ .setBatteryLevel(in.readInt())
+ .setCharging(in.readByte() != 0)
+ .setImageContentUri(in.readParcelable(Uri.class.getClassLoader()))
+ .build();
+ }
+
+ @Override
+ public HeadsetPiece[] newArray(int size) {
+ return new HeadsetPiece[size];
+ }
+ };
+
+ @Override
+ public final int hashCode() {
+ return Arrays.hashCode(
+ new Object[]{
+ lowLevelThreshold(), batteryLevel(), imageUrl(), charging(),
+ imageContentUri()
+ });
+ }
+
+ @Override
+ public final boolean equals(@Nullable Object other) {
+ if (other == null) {
+ return false;
+ }
+
+ if (this == other) {
+ return true;
+ }
+
+ if (!(other instanceof HeadsetPiece)) {
+ return false;
+ }
+
+ HeadsetPiece that = (HeadsetPiece) other;
+ return lowLevelThreshold() == that.lowLevelThreshold()
+ && batteryLevel() == that.batteryLevel()
+ && Objects.equals(imageUrl(), that.imageUrl())
+ && charging() == that.charging()
+ && Objects.equals(imageContentUri(), that.imageContentUri());
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HmacSha256.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HmacSha256.java
new file mode 100644
index 0000000..cc7a300
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/HmacSha256.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.KEY_LENGTH;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.security.GeneralSecurityException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * HMAC-SHA256 utility used to generate key-SHA256 based message authentication code. This is
+ * specific for Fast Pair GATT connection exchanging data to verify both the data integrity and the
+ * authentication of a message. It is defined as:
+ *
+ * <ol>
+ * <li>SHA256(concat((key ^ opad),SHA256(concat((key ^ ipad), data)))), where
+ * <li>key is the given secret extended to 64 bytes by concat(secret, ZEROS).
+ * <li>opad is 64 bytes outer padding, consisting of repeated bytes valued 0x5c.
+ * <li>ipad is 64 bytes inner padding, consisting of repeated bytes valued 0x36.
+ * </ol>
+ *
+ */
+final class HmacSha256 {
+ @VisibleForTesting static final int HMAC_SHA256_BLOCK_SIZE = 64;
+
+ private HmacSha256() {}
+
+ /**
+ * Generates the HMAC for given parameters, this is specific for Fast Pair GATT connection
+ * exchanging data which is encrypted using AES-CTR.
+ *
+ * @param secret 16 bytes shared secret.
+ * @param data the data encrypted using AES-CTR and the given nonce.
+ * @return HMAC-SHA256 result.
+ */
+ static byte[] build(byte[] secret, byte[] data) throws GeneralSecurityException {
+ // Currently we only accept AES-128 key here, the second check is to secure we won't
+ // modify KEY_LENGTH to > HMAC_SHA256_BLOCK_SIZE by mistake.
+ if (secret.length != KEY_LENGTH) {
+ throw new GeneralSecurityException("Incorrect key length, should be the AES-128 key.");
+ }
+ if (KEY_LENGTH > HMAC_SHA256_BLOCK_SIZE) {
+ throw new GeneralSecurityException("KEY_LENGTH > HMAC_SHA256_BLOCK_SIZE!");
+ }
+
+ return buildWith64BytesKey(secret, data);
+ }
+
+ /**
+ * Generates the HMAC for given parameters, this is specific for Fast Pair GATT connection
+ * exchanging data which is encrypted using AES-CTR.
+ *
+ * @param secret 16 bytes shared secret.
+ * @param data the data encrypted using AES-CTR and the given nonce.
+ * @return HMAC-SHA256 result.
+ */
+ static byte[] buildWith64BytesKey(byte[] secret, byte[] data) throws GeneralSecurityException {
+ if (secret.length > HMAC_SHA256_BLOCK_SIZE) {
+ throw new GeneralSecurityException("KEY_LENGTH > HMAC_SHA256_BLOCK_SIZE!");
+ }
+
+ Mac mac = Mac.getInstance("HmacSHA256");
+ SecretKeySpec keySpec = new SecretKeySpec(secret, "HmacSHA256");
+ mac.init(keySpec);
+
+ return mac.doFinal(data);
+ }
+
+ /**
+ * Constant-time HMAC comparison to prevent a possible timing attack, e.g. time the same MAC
+ * with all different first byte for a given ciphertext, the right one will take longer as it
+ * will fail on the second byte's verification.
+ *
+ * @param hmac1 HMAC want to be compared with.
+ * @param hmac2 HMAC want to be compared with.
+ * @return true if and ony if the give 2 HMACs are identical and non-null.
+ */
+ static boolean compareTwoHMACs(byte[] hmac1, byte[] hmac2) {
+ if (hmac1 == null || hmac2 == null) {
+ return false;
+ }
+
+ if (hmac1.length != hmac2.length) {
+ return false;
+ }
+ // This is for constant-time comparison, don't optimize it.
+ int res = 0;
+ for (int i = 0; i < hmac1.length; i++) {
+ res |= hmac1[i] ^ hmac2[i];
+ }
+ return res == 0;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Ltv.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Ltv.java
new file mode 100644
index 0000000..88c9484
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Ltv.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import com.google.common.primitives.Bytes;
+import com.google.errorprone.annotations.FormatMethod;
+import com.google.errorprone.annotations.FormatString;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A length, type, value (LTV) data block.
+ */
+public class Ltv {
+
+ private static final int SIZE_OF_LEN_TYPE = 2;
+
+ final byte mType;
+ final byte[] mValue;
+
+ /**
+ * Thrown if there's an error during {@link #parse}.
+ */
+ public static class ParseException extends Exception {
+
+ @FormatMethod
+ private ParseException(@FormatString String format, Object... objects) {
+ super(String.format(format, objects));
+ }
+ }
+
+ /**
+ * Constructor.
+ */
+ public Ltv(byte type, byte... value) {
+ this.mType = type;
+ this.mValue = value;
+ }
+
+ /**
+ * Parses a list of LTV blocks out of the input byte block.
+ */
+ static List<Ltv> parse(byte[] bytes) throws ParseException {
+ List<Ltv> ltvs = new ArrayList<>();
+ // The "+ 2" is for the length and type bytes.
+ for (int valueLength, i = 0; i < bytes.length; i += SIZE_OF_LEN_TYPE + valueLength) {
+ // - 1 since the length in the packet includes the type byte.
+ valueLength = bytes[i] - 1;
+ if (valueLength < 0 || bytes.length < i + SIZE_OF_LEN_TYPE + valueLength) {
+ throw new ParseException(
+ "Wrong length=%d at index=%d in LTVs=%s", bytes[i], i,
+ base16().encode(bytes));
+ }
+ ltvs.add(new Ltv(bytes[i + 1], Arrays.copyOfRange(bytes, i + SIZE_OF_LEN_TYPE,
+ i + SIZE_OF_LEN_TYPE + valueLength)));
+ }
+ return ltvs;
+ }
+
+ /**
+ * Returns an LTV block, where length is mValue.length + 1 (for the type byte).
+ */
+ public byte[] getBytes() {
+ return Bytes.concat(new byte[]{(byte) (mValue.length + 1), mType}, mValue);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoder.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoder.java
new file mode 100644
index 0000000..b04cf73
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoder.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.generateNonce;
+
+import static com.google.common.primitives.Bytes.concat;
+
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+/**
+ * Message stream utilities for encoding raw packet with HMAC.
+ *
+ * <p>Encoded packet is:
+ *
+ * <ol>
+ * <li>Packet[0 - (data length - 1)]: the raw data.
+ * <li>Packet[data length - (data length + 7)]: the 8-byte message nonce.
+ * <li>Packet[(data length + 8) - (data length + 15)]: the 8-byte of HMAC.
+ * </ol>
+ */
+public class MessageStreamHmacEncoder {
+ public static final int EXTRACT_HMAC_SIZE = 8;
+ public static final int SECTION_NONCE_LENGTH = 8;
+
+ private MessageStreamHmacEncoder() {}
+
+ /** Encodes Message Packet. */
+ public static byte[] encodeMessagePacket(byte[] accountKey, byte[] sectionNonce, byte[] data)
+ throws GeneralSecurityException {
+ checkAccountKeyAndSectionNonce(accountKey, sectionNonce);
+
+ if (data == null || data.length == 0) {
+ throw new GeneralSecurityException("No input data for encodeMessagePacket");
+ }
+
+ byte[] messageNonce = generateNonce();
+ byte[] extractedHmac =
+ Arrays.copyOf(
+ HmacSha256.buildWith64BytesKey(
+ accountKey, concat(sectionNonce, messageNonce, data)),
+ EXTRACT_HMAC_SIZE);
+
+ return concat(data, messageNonce, extractedHmac);
+ }
+
+ /** Verifies Hmac. */
+ public static boolean verifyHmac(byte[] accountKey, byte[] sectionNonce, byte[] data)
+ throws GeneralSecurityException {
+ checkAccountKeyAndSectionNonce(accountKey, sectionNonce);
+ if (data == null) {
+ throw new GeneralSecurityException("data is null");
+ }
+ if (data.length <= EXTRACT_HMAC_SIZE + SECTION_NONCE_LENGTH) {
+ throw new GeneralSecurityException("data.length too short");
+ }
+
+ byte[] hmac = Arrays.copyOfRange(data, data.length - EXTRACT_HMAC_SIZE, data.length);
+ byte[] messageNonce =
+ Arrays.copyOfRange(
+ data,
+ data.length - EXTRACT_HMAC_SIZE - SECTION_NONCE_LENGTH,
+ data.length - EXTRACT_HMAC_SIZE);
+ byte[] rawData = Arrays.copyOf(
+ data, data.length - EXTRACT_HMAC_SIZE - SECTION_NONCE_LENGTH);
+ return Arrays.equals(
+ Arrays.copyOf(
+ HmacSha256.buildWith64BytesKey(
+ accountKey, concat(sectionNonce, messageNonce, rawData)),
+ EXTRACT_HMAC_SIZE),
+ hmac);
+ }
+
+ private static void checkAccountKeyAndSectionNonce(byte[] accountKey, byte[] sectionNonce)
+ throws GeneralSecurityException {
+ if (accountKey == null || accountKey.length == 0) {
+ throw new GeneralSecurityException(
+ "Incorrect accountKey for encoding message packet, accountKey.length = "
+ + (accountKey == null ? "NULL" : accountKey.length));
+ }
+
+ if (sectionNonce == null || sectionNonce.length != SECTION_NONCE_LENGTH) {
+ throw new GeneralSecurityException(
+ "Incorrect sectionNonce for encoding message packet, sectionNonce.length = "
+ + (sectionNonce == null ? "NULL" : sectionNonce.length));
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/NamingEncoder.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/NamingEncoder.java
new file mode 100644
index 0000000..1521be6
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/NamingEncoder.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.NONCE_SIZE;
+
+import static com.google.common.primitives.Bytes.concat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.annotation.TargetApi;
+import android.os.Build.VERSION_CODES;
+
+import com.google.common.base.Utf8;
+
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+/**
+ * Naming utilities for encoding naming packet, decoding naming packet and verifying both the data
+ * integrity and the authentication of a message by checking HMAC.
+ *
+ * <p>Naming packet is:
+ *
+ * <ol>
+ * <li>Naming_Packet[0 - 7]: the first 8-byte of HMAC.
+ * <li>Naming_Packet[8 - var]: the encrypted name (with 8-byte nonce appended to the front).
+ * </ol>
+ */
+@TargetApi(VERSION_CODES.M)
+public final class NamingEncoder {
+
+ static final int EXTRACT_HMAC_SIZE = 8;
+ static final int MAX_LENGTH_OF_NAME = 48;
+
+ private NamingEncoder() {
+ }
+
+ /**
+ * Encodes the name to naming packet by the given secret.
+ *
+ * @param secret AES-128 key for encryption.
+ * @param name the given name to be encoded.
+ * @return the encrypted data with the 8-byte extracted HMAC appended to the front.
+ * @throws GeneralSecurityException if the given key or name is invalid for encoding.
+ */
+ public static byte[] encodeNamingPacket(byte[] secret, String name)
+ throws GeneralSecurityException {
+ if (secret == null || secret.length != AesCtrMultipleBlockEncryption.KEY_LENGTH) {
+ throw new GeneralSecurityException(
+ "Incorrect secret for encoding name packet, secret.length = "
+ + (secret == null ? "NULL" : secret.length));
+ }
+
+ if ((name == null) || (name.length() == 0) || (Utf8.encodedLength(name)
+ > MAX_LENGTH_OF_NAME)) {
+ throw new GeneralSecurityException(
+ "Invalid name for encoding name packet, Utf8.encodedLength(name) = "
+ + (name == null ? "NULL" : Utf8.encodedLength(name)));
+ }
+
+ byte[] encryptedData = AesCtrMultipleBlockEncryption.encrypt(secret, name.getBytes(UTF_8));
+ byte[] extractedHmac =
+ Arrays.copyOf(HmacSha256.build(secret, encryptedData), EXTRACT_HMAC_SIZE);
+
+ return concat(extractedHmac, encryptedData);
+ }
+
+ /**
+ * Decodes the name from naming packet by the given secret.
+ *
+ * @param secret AES-128 key used in the encryption to decrypt data.
+ * @param namingPacket naming packet which is encoded by the given secret..
+ * @return the name decoded from the given packet.
+ * @throws GeneralSecurityException if the given key or naming packet is invalid for decoding.
+ */
+ public static String decodeNamingPacket(byte[] secret, byte[] namingPacket)
+ throws GeneralSecurityException {
+ if (secret == null || secret.length != AesCtrMultipleBlockEncryption.KEY_LENGTH) {
+ throw new GeneralSecurityException(
+ "Incorrect secret for decoding name packet, secret.length = "
+ + (secret == null ? "NULL" : secret.length));
+ }
+ if (namingPacket == null
+ || namingPacket.length <= EXTRACT_HMAC_SIZE
+ || namingPacket.length > (MAX_LENGTH_OF_NAME + EXTRACT_HMAC_SIZE + NONCE_SIZE)) {
+ throw new GeneralSecurityException(
+ "Naming packet size is incorrect, namingPacket.length is "
+ + (namingPacket == null ? "NULL" : namingPacket.length));
+ }
+
+ if (!verifyHmac(secret, namingPacket)) {
+ throw new GeneralSecurityException(
+ "Verify HMAC failed, could be incorrect key or naming packet.");
+ }
+ byte[] encryptedData = Arrays
+ .copyOfRange(namingPacket, EXTRACT_HMAC_SIZE, namingPacket.length);
+ return new String(AesCtrMultipleBlockEncryption.decrypt(secret, encryptedData), UTF_8);
+ }
+
+ // Computes the HMAC of the given key and name, and compares the first 8-byte of the HMAC result
+ // with the one from name packet. Must call constant-time comparison to prevent a possible
+ // timing attack, e.g. time the same MAC with all different first byte for a given ciphertext,
+ // the right one will take longer as it will fail on the second byte's verification.
+ private static boolean verifyHmac(byte[] key, byte[] namingPacket)
+ throws GeneralSecurityException {
+ byte[] packetHmac = Arrays.copyOfRange(namingPacket, /* from= */ 0, EXTRACT_HMAC_SIZE);
+ byte[] encryptedData = Arrays
+ .copyOfRange(namingPacket, EXTRACT_HMAC_SIZE, namingPacket.length);
+ byte[] computedHmac = Arrays
+ .copyOf(HmacSha256.build(key, encryptedData), EXTRACT_HMAC_SIZE);
+
+ return HmacSha256.compareTwoHMACs(packetHmac, computedHmac);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PairingException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PairingException.java
new file mode 100644
index 0000000..722dc85
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PairingException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+/** Base class for pairing exceptions. */
+// TODO(b/200594968): convert exceptions into error codes to save memory.
+public class PairingException extends Exception {
+ PairingException(String format, Object... objects) {
+ super(String.format(format, objects));
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PairingProgressListener.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PairingProgressListener.java
new file mode 100644
index 0000000..270cb42
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PairingProgressListener.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Callback interface for pairing progress. */
+public interface PairingProgressListener {
+
+ /** Fast Pair Bond State. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ PairingEvent.START,
+ PairingEvent.SUCCESS,
+ PairingEvent.FAILED,
+ PairingEvent.UNKNOWN,
+ })
+ public @interface PairingEvent {
+ int START = 0;
+ int SUCCESS = 1;
+ int FAILED = 2;
+ int UNKNOWN = 3;
+ }
+
+ /** Returns enum based on the ordinal index. */
+ static @PairingEvent int fromOrdinal(int ordinal) {
+ switch (ordinal) {
+ case 0:
+ return PairingEvent.START;
+ case 1:
+ return PairingEvent.SUCCESS;
+ case 2:
+ return PairingEvent.FAILED;
+ case 3:
+ return PairingEvent.UNKNOWN;
+ default:
+ return PairingEvent.UNKNOWN;
+ }
+ }
+
+ /** Callback function upon pairing progress update. */
+ void onPairingProgressUpdating(@PairingEvent int event, String message);
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PasskeyConfirmationHandler.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PasskeyConfirmationHandler.java
new file mode 100644
index 0000000..f5807a3
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/PasskeyConfirmationHandler.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import android.bluetooth.BluetoothDevice;
+
+/** Interface for getting the passkey confirmation request. */
+public interface PasskeyConfirmationHandler {
+ /** Called when getting the passkey confirmation request while pairing. */
+ void onPasskeyConfirmation(BluetoothDevice device, int passkey);
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Preferences.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Preferences.java
new file mode 100644
index 0000000..bb7b71b
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Preferences.java
@@ -0,0 +1,2309 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.get16BitUuid;
+
+import androidx.annotation.Nullable;
+
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.FirmwareVersionCharacteristic;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.primitives.Shorts;
+
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Preferences that tweak the Fast Pairing process: timeouts, number of retries... All preferences
+ * have default values which should be reasonable for all clients.
+ */
+public class Preferences {
+
+ private final int mGattOperationTimeoutSeconds;
+ private final int mGattConnectionTimeoutSeconds;
+ private final int mBluetoothToggleTimeoutSeconds;
+ private final int mBluetoothToggleSleepSeconds;
+ private final int mClassicDiscoveryTimeoutSeconds;
+ private final int mNumDiscoverAttempts;
+ private final int mDiscoveryRetrySleepSeconds;
+ private final boolean mIgnoreDiscoveryError;
+ private final int mSdpTimeoutSeconds;
+ private final int mNumSdpAttempts;
+ private final int mNumCreateBondAttempts;
+ private final int mNumConnectAttempts;
+ private final int mNumWriteAccountKeyAttempts;
+ private final boolean mToggleBluetoothOnFailure;
+ private final boolean mBluetoothStateUsesPolling;
+ private final int mBluetoothStatePollingMillis;
+ private final int mNumAttempts;
+ private final boolean mEnableBrEdrHandover;
+ private final short mBrHandoverDataCharacteristicId;
+ private final short mBluetoothSigDataCharacteristicId;
+ private final short mFirmwareVersionCharacteristicId;
+ private final short mBrTransportBlockDataDescriptorId;
+ private final boolean mWaitForUuidsAfterBonding;
+ private final boolean mReceiveUuidsAndBondedEventBeforeClose;
+ private final int mRemoveBondTimeoutSeconds;
+ private final int mRemoveBondSleepMillis;
+ private final int mCreateBondTimeoutSeconds;
+ private final int mHidCreateBondTimeoutSeconds;
+ private final int mProxyTimeoutSeconds;
+ private final boolean mRejectPhonebookAccess;
+ private final boolean mRejectMessageAccess;
+ private final boolean mRejectSimAccess;
+ private final int mWriteAccountKeySleepMillis;
+ private final boolean mSkipDisconnectingGattBeforeWritingAccountKey;
+ private final boolean mMoreEventLogForQuality;
+ private final boolean mRetryGattConnectionAndSecretHandshake;
+ private final long mGattConnectShortTimeoutMs;
+ private final long mGattConnectLongTimeoutMs;
+ private final long mGattConnectShortTimeoutRetryMaxSpentTimeMs;
+ private final long mAddressRotateRetryMaxSpentTimeMs;
+ private final long mPairingRetryDelayMs;
+ private final long mSecretHandshakeShortTimeoutMs;
+ private final long mSecretHandshakeLongTimeoutMs;
+ private final long mSecretHandshakeShortTimeoutRetryMaxSpentTimeMs;
+ private final long mSecretHandshakeLongTimeoutRetryMaxSpentTimeMs;
+ private final long mSecretHandshakeRetryAttempts;
+ private final long mSecretHandshakeRetryGattConnectionMaxSpentTimeMs;
+ private final long mSignalLostRetryMaxSpentTimeMs;
+ private final ImmutableSet<Integer> mGattConnectionAndSecretHandshakeNoRetryGattError;
+ private final boolean mRetrySecretHandshakeTimeout;
+ private final boolean mLogUserManualRetry;
+ private final int mPairFailureCounts;
+ private final String mCachedDeviceAddress;
+ private final String mPossibleCachedDeviceAddress;
+ private final int mSameModelIdPairedDeviceCount;
+ private final boolean mIsDeviceFinishCheckAddressFromCache;
+ private final boolean mLogPairWithCachedModelId;
+ private final boolean mDirectConnectProfileIfModelIdInCache;
+ private final boolean mAcceptPasskey;
+ private final byte[] mSupportedProfileUuids;
+ private final boolean mProviderInitiatesBondingIfSupported;
+ private final boolean mAttemptDirectConnectionWhenPreviouslyBonded;
+ private final boolean mAutomaticallyReconnectGattWhenNeeded;
+ private final boolean mSkipConnectingProfiles;
+ private final boolean mIgnoreUuidTimeoutAfterBonded;
+ private final boolean mSpecifyCreateBondTransportType;
+ private final int mCreateBondTransportType;
+ private final boolean mIncreaseIntentFilterPriority;
+ private final boolean mEvaluatePerformance;
+ private final Preferences.ExtraLoggingInformation mExtraLoggingInformation;
+ private final boolean mEnableNamingCharacteristic;
+ private final boolean mEnableFirmwareVersionCharacteristic;
+ private final boolean mKeepSameAccountKeyWrite;
+ private final boolean mIsRetroactivePairing;
+ private final int mNumSdpAttemptsAfterBonded;
+ private final boolean mSupportHidDevice;
+ private final boolean mEnablePairingWhileDirectlyConnecting;
+ private final boolean mAcceptConsentForFastPairOne;
+ private final int mGattConnectRetryTimeoutMillis;
+ private final boolean mEnable128BitCustomGattCharacteristicsId;
+ private final boolean mEnableSendExceptionStepToValidator;
+ private final boolean mEnableAdditionalDataTypeWhenActionOverBle;
+ private final boolean mCheckBondStateWhenSkipConnectingProfiles;
+ private final boolean mHandlePasskeyConfirmationByUi;
+ private final boolean mEnablePairFlowShowUiWithoutProfileConnection;
+
+ private Preferences(
+ int gattOperationTimeoutSeconds,
+ int gattConnectionTimeoutSeconds,
+ int bluetoothToggleTimeoutSeconds,
+ int bluetoothToggleSleepSeconds,
+ int classicDiscoveryTimeoutSeconds,
+ int numDiscoverAttempts,
+ int discoveryRetrySleepSeconds,
+ boolean ignoreDiscoveryError,
+ int sdpTimeoutSeconds,
+ int numSdpAttempts,
+ int numCreateBondAttempts,
+ int numConnectAttempts,
+ int numWriteAccountKeyAttempts,
+ boolean toggleBluetoothOnFailure,
+ boolean bluetoothStateUsesPolling,
+ int bluetoothStatePollingMillis,
+ int numAttempts,
+ boolean enableBrEdrHandover,
+ short brHandoverDataCharacteristicId,
+ short bluetoothSigDataCharacteristicId,
+ short firmwareVersionCharacteristicId,
+ short brTransportBlockDataDescriptorId,
+ boolean waitForUuidsAfterBonding,
+ boolean receiveUuidsAndBondedEventBeforeClose,
+ int removeBondTimeoutSeconds,
+ int removeBondSleepMillis,
+ int createBondTimeoutSeconds,
+ int hidCreateBondTimeoutSeconds,
+ int proxyTimeoutSeconds,
+ boolean rejectPhonebookAccess,
+ boolean rejectMessageAccess,
+ boolean rejectSimAccess,
+ int writeAccountKeySleepMillis,
+ boolean skipDisconnectingGattBeforeWritingAccountKey,
+ boolean moreEventLogForQuality,
+ boolean retryGattConnectionAndSecretHandshake,
+ long gattConnectShortTimeoutMs,
+ long gattConnectLongTimeoutMs,
+ long gattConnectShortTimeoutRetryMaxSpentTimeMs,
+ long addressRotateRetryMaxSpentTimeMs,
+ long pairingRetryDelayMs,
+ long secretHandshakeShortTimeoutMs,
+ long secretHandshakeLongTimeoutMs,
+ long secretHandshakeShortTimeoutRetryMaxSpentTimeMs,
+ long secretHandshakeLongTimeoutRetryMaxSpentTimeMs,
+ long secretHandshakeRetryAttempts,
+ long secretHandshakeRetryGattConnectionMaxSpentTimeMs,
+ long signalLostRetryMaxSpentTimeMs,
+ ImmutableSet<Integer> gattConnectionAndSecretHandshakeNoRetryGattError,
+ boolean retrySecretHandshakeTimeout,
+ boolean logUserManualRetry,
+ int pairFailureCounts,
+ String cachedDeviceAddress,
+ String possibleCachedDeviceAddress,
+ int sameModelIdPairedDeviceCount,
+ boolean isDeviceFinishCheckAddressFromCache,
+ boolean logPairWithCachedModelId,
+ boolean directConnectProfileIfModelIdInCache,
+ boolean acceptPasskey,
+ byte[] supportedProfileUuids,
+ boolean providerInitiatesBondingIfSupported,
+ boolean attemptDirectConnectionWhenPreviouslyBonded,
+ boolean automaticallyReconnectGattWhenNeeded,
+ boolean skipConnectingProfiles,
+ boolean ignoreUuidTimeoutAfterBonded,
+ boolean specifyCreateBondTransportType,
+ int createBondTransportType,
+ boolean increaseIntentFilterPriority,
+ boolean evaluatePerformance,
+ @Nullable Preferences.ExtraLoggingInformation extraLoggingInformation,
+ boolean enableNamingCharacteristic,
+ boolean enableFirmwareVersionCharacteristic,
+ boolean keepSameAccountKeyWrite,
+ boolean isRetroactivePairing,
+ int numSdpAttemptsAfterBonded,
+ boolean supportHidDevice,
+ boolean enablePairingWhileDirectlyConnecting,
+ boolean acceptConsentForFastPairOne,
+ int gattConnectRetryTimeoutMillis,
+ boolean enable128BitCustomGattCharacteristicsId,
+ boolean enableSendExceptionStepToValidator,
+ boolean enableAdditionalDataTypeWhenActionOverBle,
+ boolean checkBondStateWhenSkipConnectingProfiles,
+ boolean handlePasskeyConfirmationByUi,
+ boolean enablePairFlowShowUiWithoutProfileConnection) {
+ this.mGattOperationTimeoutSeconds = gattOperationTimeoutSeconds;
+ this.mGattConnectionTimeoutSeconds = gattConnectionTimeoutSeconds;
+ this.mBluetoothToggleTimeoutSeconds = bluetoothToggleTimeoutSeconds;
+ this.mBluetoothToggleSleepSeconds = bluetoothToggleSleepSeconds;
+ this.mClassicDiscoveryTimeoutSeconds = classicDiscoveryTimeoutSeconds;
+ this.mNumDiscoverAttempts = numDiscoverAttempts;
+ this.mDiscoveryRetrySleepSeconds = discoveryRetrySleepSeconds;
+ this.mIgnoreDiscoveryError = ignoreDiscoveryError;
+ this.mSdpTimeoutSeconds = sdpTimeoutSeconds;
+ this.mNumSdpAttempts = numSdpAttempts;
+ this.mNumCreateBondAttempts = numCreateBondAttempts;
+ this.mNumConnectAttempts = numConnectAttempts;
+ this.mNumWriteAccountKeyAttempts = numWriteAccountKeyAttempts;
+ this.mToggleBluetoothOnFailure = toggleBluetoothOnFailure;
+ this.mBluetoothStateUsesPolling = bluetoothStateUsesPolling;
+ this.mBluetoothStatePollingMillis = bluetoothStatePollingMillis;
+ this.mNumAttempts = numAttempts;
+ this.mEnableBrEdrHandover = enableBrEdrHandover;
+ this.mBrHandoverDataCharacteristicId = brHandoverDataCharacteristicId;
+ this.mBluetoothSigDataCharacteristicId = bluetoothSigDataCharacteristicId;
+ this.mFirmwareVersionCharacteristicId = firmwareVersionCharacteristicId;
+ this.mBrTransportBlockDataDescriptorId = brTransportBlockDataDescriptorId;
+ this.mWaitForUuidsAfterBonding = waitForUuidsAfterBonding;
+ this.mReceiveUuidsAndBondedEventBeforeClose = receiveUuidsAndBondedEventBeforeClose;
+ this.mRemoveBondTimeoutSeconds = removeBondTimeoutSeconds;
+ this.mRemoveBondSleepMillis = removeBondSleepMillis;
+ this.mCreateBondTimeoutSeconds = createBondTimeoutSeconds;
+ this.mHidCreateBondTimeoutSeconds = hidCreateBondTimeoutSeconds;
+ this.mProxyTimeoutSeconds = proxyTimeoutSeconds;
+ this.mRejectPhonebookAccess = rejectPhonebookAccess;
+ this.mRejectMessageAccess = rejectMessageAccess;
+ this.mRejectSimAccess = rejectSimAccess;
+ this.mWriteAccountKeySleepMillis = writeAccountKeySleepMillis;
+ this.mSkipDisconnectingGattBeforeWritingAccountKey =
+ skipDisconnectingGattBeforeWritingAccountKey;
+ this.mMoreEventLogForQuality = moreEventLogForQuality;
+ this.mRetryGattConnectionAndSecretHandshake = retryGattConnectionAndSecretHandshake;
+ this.mGattConnectShortTimeoutMs = gattConnectShortTimeoutMs;
+ this.mGattConnectLongTimeoutMs = gattConnectLongTimeoutMs;
+ this.mGattConnectShortTimeoutRetryMaxSpentTimeMs =
+ gattConnectShortTimeoutRetryMaxSpentTimeMs;
+ this.mAddressRotateRetryMaxSpentTimeMs = addressRotateRetryMaxSpentTimeMs;
+ this.mPairingRetryDelayMs = pairingRetryDelayMs;
+ this.mSecretHandshakeShortTimeoutMs = secretHandshakeShortTimeoutMs;
+ this.mSecretHandshakeLongTimeoutMs = secretHandshakeLongTimeoutMs;
+ this.mSecretHandshakeShortTimeoutRetryMaxSpentTimeMs =
+ secretHandshakeShortTimeoutRetryMaxSpentTimeMs;
+ this.mSecretHandshakeLongTimeoutRetryMaxSpentTimeMs =
+ secretHandshakeLongTimeoutRetryMaxSpentTimeMs;
+ this.mSecretHandshakeRetryAttempts = secretHandshakeRetryAttempts;
+ this.mSecretHandshakeRetryGattConnectionMaxSpentTimeMs =
+ secretHandshakeRetryGattConnectionMaxSpentTimeMs;
+ this.mSignalLostRetryMaxSpentTimeMs = signalLostRetryMaxSpentTimeMs;
+ this.mGattConnectionAndSecretHandshakeNoRetryGattError =
+ gattConnectionAndSecretHandshakeNoRetryGattError;
+ this.mRetrySecretHandshakeTimeout = retrySecretHandshakeTimeout;
+ this.mLogUserManualRetry = logUserManualRetry;
+ this.mPairFailureCounts = pairFailureCounts;
+ this.mCachedDeviceAddress = cachedDeviceAddress;
+ this.mPossibleCachedDeviceAddress = possibleCachedDeviceAddress;
+ this.mSameModelIdPairedDeviceCount = sameModelIdPairedDeviceCount;
+ this.mIsDeviceFinishCheckAddressFromCache = isDeviceFinishCheckAddressFromCache;
+ this.mLogPairWithCachedModelId = logPairWithCachedModelId;
+ this.mDirectConnectProfileIfModelIdInCache = directConnectProfileIfModelIdInCache;
+ this.mAcceptPasskey = acceptPasskey;
+ this.mSupportedProfileUuids = supportedProfileUuids;
+ this.mProviderInitiatesBondingIfSupported = providerInitiatesBondingIfSupported;
+ this.mAttemptDirectConnectionWhenPreviouslyBonded =
+ attemptDirectConnectionWhenPreviouslyBonded;
+ this.mAutomaticallyReconnectGattWhenNeeded = automaticallyReconnectGattWhenNeeded;
+ this.mSkipConnectingProfiles = skipConnectingProfiles;
+ this.mIgnoreUuidTimeoutAfterBonded = ignoreUuidTimeoutAfterBonded;
+ this.mSpecifyCreateBondTransportType = specifyCreateBondTransportType;
+ this.mCreateBondTransportType = createBondTransportType;
+ this.mIncreaseIntentFilterPriority = increaseIntentFilterPriority;
+ this.mEvaluatePerformance = evaluatePerformance;
+ this.mExtraLoggingInformation = extraLoggingInformation;
+ this.mEnableNamingCharacteristic = enableNamingCharacteristic;
+ this.mEnableFirmwareVersionCharacteristic = enableFirmwareVersionCharacteristic;
+ this.mKeepSameAccountKeyWrite = keepSameAccountKeyWrite;
+ this.mIsRetroactivePairing = isRetroactivePairing;
+ this.mNumSdpAttemptsAfterBonded = numSdpAttemptsAfterBonded;
+ this.mSupportHidDevice = supportHidDevice;
+ this.mEnablePairingWhileDirectlyConnecting = enablePairingWhileDirectlyConnecting;
+ this.mAcceptConsentForFastPairOne = acceptConsentForFastPairOne;
+ this.mGattConnectRetryTimeoutMillis = gattConnectRetryTimeoutMillis;
+ this.mEnable128BitCustomGattCharacteristicsId = enable128BitCustomGattCharacteristicsId;
+ this.mEnableSendExceptionStepToValidator = enableSendExceptionStepToValidator;
+ this.mEnableAdditionalDataTypeWhenActionOverBle = enableAdditionalDataTypeWhenActionOverBle;
+ this.mCheckBondStateWhenSkipConnectingProfiles = checkBondStateWhenSkipConnectingProfiles;
+ this.mHandlePasskeyConfirmationByUi = handlePasskeyConfirmationByUi;
+ this.mEnablePairFlowShowUiWithoutProfileConnection =
+ enablePairFlowShowUiWithoutProfileConnection;
+ }
+
+ /**
+ * Timeout for each GATT operation (not for the whole pairing process).
+ */
+ public int getGattOperationTimeoutSeconds() {
+ return mGattOperationTimeoutSeconds;
+ }
+
+ /**
+ * Timeout for Gatt connection operation.
+ */
+ public int getGattConnectionTimeoutSeconds() {
+ return mGattConnectionTimeoutSeconds;
+ }
+
+ /**
+ * Timeout for Bluetooth toggle.
+ */
+ public int getBluetoothToggleTimeoutSeconds() {
+ return mBluetoothToggleTimeoutSeconds;
+ }
+
+ /**
+ * Sleep time for Bluetooth toggle.
+ */
+ public int getBluetoothToggleSleepSeconds() {
+ return mBluetoothToggleSleepSeconds;
+ }
+
+ /**
+ * Timeout for classic discovery.
+ */
+ public int getClassicDiscoveryTimeoutSeconds() {
+ return mClassicDiscoveryTimeoutSeconds;
+ }
+
+ /**
+ * Number of discovery attempts allowed.
+ */
+ public int getNumDiscoverAttempts() {
+ return mNumDiscoverAttempts;
+ }
+
+ /**
+ * Sleep time between discovery retry.
+ */
+ public int getDiscoveryRetrySleepSeconds() {
+ return mDiscoveryRetrySleepSeconds;
+ }
+
+ /**
+ * Whether to ignore error incurred during discovery.
+ */
+ public boolean getIgnoreDiscoveryError() {
+ return mIgnoreDiscoveryError;
+ }
+
+ /**
+ * Timeout for Sdp.
+ */
+ public int getSdpTimeoutSeconds() {
+ return mSdpTimeoutSeconds;
+ }
+
+ /**
+ * Number of Sdp attempts allowed.
+ */
+ public int getNumSdpAttempts() {
+ return mNumSdpAttempts;
+ }
+
+ /**
+ * Number of create bond attempts allowed.
+ */
+ public int getNumCreateBondAttempts() {
+ return mNumCreateBondAttempts;
+ }
+
+ /**
+ * Number of connect attempts allowed.
+ */
+ public int getNumConnectAttempts() {
+ return mNumConnectAttempts;
+ }
+
+ /**
+ * Number of write account key attempts allowed.
+ */
+ public int getNumWriteAccountKeyAttempts() {
+ return mNumWriteAccountKeyAttempts;
+ }
+
+ /**
+ * Returns whether it is OK toggle bluetooth to retry upon failure.
+ */
+ public boolean getToggleBluetoothOnFailure() {
+ return mToggleBluetoothOnFailure;
+ }
+
+ /**
+ * Whether to get Bluetooth state using polling.
+ */
+ public boolean getBluetoothStateUsesPolling() {
+ return mBluetoothStateUsesPolling;
+ }
+
+ /**
+ * Polling time when retrieving Bluetooth state.
+ */
+ public int getBluetoothStatePollingMillis() {
+ return mBluetoothStatePollingMillis;
+ }
+
+ /**
+ * The number of times to attempt a generic operation, before giving up.
+ */
+ public int getNumAttempts() {
+ return mNumAttempts;
+ }
+
+ /**
+ * Returns whether BrEdr handover is enabled.
+ */
+ public boolean getEnableBrEdrHandover() {
+ return mEnableBrEdrHandover;
+ }
+
+ /**
+ * Returns characteristic Id for Br Handover data.
+ */
+ public short getBrHandoverDataCharacteristicId() {
+ return mBrHandoverDataCharacteristicId;
+ }
+
+ /**
+ * Returns characteristic Id for Bluethoth Sig data.
+ */
+ public short getBluetoothSigDataCharacteristicId() {
+ return mBluetoothSigDataCharacteristicId;
+ }
+
+ /**
+ * Returns characteristic Id for Firmware version.
+ */
+ public short getFirmwareVersionCharacteristicId() {
+ return mFirmwareVersionCharacteristicId;
+ }
+
+ /**
+ * Returns descripter Id for Br transport block data.
+ */
+ public short getBrTransportBlockDataDescriptorId() {
+ return mBrTransportBlockDataDescriptorId;
+ }
+
+ /**
+ * Whether to wait for Uuids after bonding.
+ */
+ public boolean getWaitForUuidsAfterBonding() {
+ return mWaitForUuidsAfterBonding;
+ }
+
+ /**
+ * Whether to get received Uuids and bonded events before close.
+ */
+ public boolean getReceiveUuidsAndBondedEventBeforeClose() {
+ return mReceiveUuidsAndBondedEventBeforeClose;
+ }
+
+ /**
+ * Timeout for remove bond operation.
+ */
+ public int getRemoveBondTimeoutSeconds() {
+ return mRemoveBondTimeoutSeconds;
+ }
+
+ /**
+ * Sleep time for remove bond operation.
+ */
+ public int getRemoveBondSleepMillis() {
+ return mRemoveBondSleepMillis;
+ }
+
+ /**
+ * This almost always succeeds (or fails) in 2-10 seconds (Taimen running O -> Nexus 6P sim).
+ */
+ public int getCreateBondTimeoutSeconds() {
+ return mCreateBondTimeoutSeconds;
+ }
+
+ /**
+ * Timeout for creating bond with Hid devices.
+ */
+ public int getHidCreateBondTimeoutSeconds() {
+ return mHidCreateBondTimeoutSeconds;
+ }
+
+ /**
+ * Timeout for get proxy operation.
+ */
+ public int getProxyTimeoutSeconds() {
+ return mProxyTimeoutSeconds;
+ }
+
+ /**
+ * Whether to reject phone book access.
+ */
+ public boolean getRejectPhonebookAccess() {
+ return mRejectPhonebookAccess;
+ }
+
+ /**
+ * Whether to reject message access.
+ */
+ public boolean getRejectMessageAccess() {
+ return mRejectMessageAccess;
+ }
+
+ /**
+ * Whether to reject sim access.
+ */
+ public boolean getRejectSimAccess() {
+ return mRejectSimAccess;
+ }
+
+ /**
+ * Sleep time for write account key operation.
+ */
+ public int getWriteAccountKeySleepMillis() {
+ return mWriteAccountKeySleepMillis;
+ }
+
+ /**
+ * Whether to skip disconneting gatt before writing account key.
+ */
+ public boolean getSkipDisconnectingGattBeforeWritingAccountKey() {
+ return mSkipDisconnectingGattBeforeWritingAccountKey;
+ }
+
+ /**
+ * Whether to get more event log for quality improvement.
+ */
+ public boolean getMoreEventLogForQuality() {
+ return mMoreEventLogForQuality;
+ }
+
+ /**
+ * Whether to retry gatt connection and secrete handshake.
+ */
+ public boolean getRetryGattConnectionAndSecretHandshake() {
+ return mRetryGattConnectionAndSecretHandshake;
+ }
+
+ /**
+ * Short Gatt connection timeoout.
+ */
+ public long getGattConnectShortTimeoutMs() {
+ return mGattConnectShortTimeoutMs;
+ }
+
+ /**
+ * Long Gatt connection timeout.
+ */
+ public long getGattConnectLongTimeoutMs() {
+ return mGattConnectLongTimeoutMs;
+ }
+
+ /**
+ * Short Timeout for Gatt connection, including retry.
+ */
+ public long getGattConnectShortTimeoutRetryMaxSpentTimeMs() {
+ return mGattConnectShortTimeoutRetryMaxSpentTimeMs;
+ }
+
+ /**
+ * Timeout for address rotation, including retry.
+ */
+ public long getAddressRotateRetryMaxSpentTimeMs() {
+ return mAddressRotateRetryMaxSpentTimeMs;
+ }
+
+ /**
+ * Returns pairing retry delay time.
+ */
+ public long getPairingRetryDelayMs() {
+ return mPairingRetryDelayMs;
+ }
+
+ /**
+ * Short timeout for secrete handshake.
+ */
+ public long getSecretHandshakeShortTimeoutMs() {
+ return mSecretHandshakeShortTimeoutMs;
+ }
+
+ /**
+ * Long timeout for secret handshake.
+ */
+ public long getSecretHandshakeLongTimeoutMs() {
+ return mSecretHandshakeLongTimeoutMs;
+ }
+
+ /**
+ * Short timeout for secret handshake, including retry.
+ */
+ public long getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs() {
+ return mSecretHandshakeShortTimeoutRetryMaxSpentTimeMs;
+ }
+
+ /**
+ * Long timeout for secret handshake, including retry.
+ */
+ public long getSecretHandshakeLongTimeoutRetryMaxSpentTimeMs() {
+ return mSecretHandshakeLongTimeoutRetryMaxSpentTimeMs;
+ }
+
+ /**
+ * Number of secrete handshake retry allowed.
+ */
+ public long getSecretHandshakeRetryAttempts() {
+ return mSecretHandshakeRetryAttempts;
+ }
+
+ /**
+ * Timeout for secrete handshake and gatt connection, including retry.
+ */
+ public long getSecretHandshakeRetryGattConnectionMaxSpentTimeMs() {
+ return mSecretHandshakeRetryGattConnectionMaxSpentTimeMs;
+ }
+
+ /**
+ * Timeout for signal lost handling, including retry.
+ */
+ public long getSignalLostRetryMaxSpentTimeMs() {
+ return mSignalLostRetryMaxSpentTimeMs;
+ }
+
+ /**
+ * Returns error for gatt connection and secrete handshake, without retry.
+ */
+ public ImmutableSet<Integer> getGattConnectionAndSecretHandshakeNoRetryGattError() {
+ return mGattConnectionAndSecretHandshakeNoRetryGattError;
+ }
+
+ /**
+ * Whether to retry upon secrete handshake timeout.
+ */
+ public boolean getRetrySecretHandshakeTimeout() {
+ return mRetrySecretHandshakeTimeout;
+ }
+
+ /**
+ * Wehther to log user manual retry.
+ */
+ public boolean getLogUserManualRetry() {
+ return mLogUserManualRetry;
+ }
+
+ /**
+ * Returns number of pairing failure counts.
+ */
+ public int getPairFailureCounts() {
+ return mPairFailureCounts;
+ }
+
+ /**
+ * Returns cached device address.
+ */
+ public String getCachedDeviceAddress() {
+ return mCachedDeviceAddress;
+ }
+
+ /**
+ * Returns possible cached device address.
+ */
+ public String getPossibleCachedDeviceAddress() {
+ return mPossibleCachedDeviceAddress;
+ }
+
+ /**
+ * Returns count of paired devices from the same model Id.
+ */
+ public int getSameModelIdPairedDeviceCount() {
+ return mSameModelIdPairedDeviceCount;
+ }
+
+ /**
+ * Whether the bonded device address is in the Cache .
+ */
+ public boolean getIsDeviceFinishCheckAddressFromCache() {
+ return mIsDeviceFinishCheckAddressFromCache;
+ }
+
+ /**
+ * Whether to log pairing info when cached model Id is hit.
+ */
+ public boolean getLogPairWithCachedModelId() {
+ return mLogPairWithCachedModelId;
+ }
+
+ /**
+ * Whether to directly connnect to a profile of a device, whose model Id is in cache.
+ */
+ public boolean getDirectConnectProfileIfModelIdInCache() {
+ return mDirectConnectProfileIfModelIdInCache;
+ }
+
+ /**
+ * Whether to auto-accept
+ * {@link android.bluetooth.BluetoothDevice#PAIRING_VARIANT_PASSKEY_CONFIRMATION}.
+ * Only the Fast Pair Simulator (which runs on an Android device) sends this. Since real
+ * Bluetooth headphones don't have displays, they use secure simple pairing (no pin code
+ * confirmation; we get no pairing request broadcast at all). So we may want to turn this off in
+ * prod.
+ */
+ public boolean getAcceptPasskey() {
+ return mAcceptPasskey;
+ }
+
+ /**
+ * Returns Uuids for supported profiles.
+ */
+ @SuppressWarnings("mutable")
+ public byte[] getSupportedProfileUuids() {
+ return mSupportedProfileUuids;
+ }
+
+ /**
+ * If true, after the Key-based Pairing BLE handshake, we wait for the headphones to send a
+ * pairing request to us; if false, we send the request to them.
+ */
+ public boolean getProviderInitiatesBondingIfSupported() {
+ return mProviderInitiatesBondingIfSupported;
+ }
+
+ /**
+ * If true, the first step will be attempting to connect directly to our supported profiles when
+ * a device has previously been bonded. This will help with performance on subsequent bondings
+ * and help to increase reliability in some cases.
+ */
+ public boolean getAttemptDirectConnectionWhenPreviouslyBonded() {
+ return mAttemptDirectConnectionWhenPreviouslyBonded;
+ }
+
+ /**
+ * If true, closed Gatt connections will be reopened when they are needed again. Otherwise, they
+ * will remain closed until they are explicitly reopened.
+ */
+ public boolean getAutomaticallyReconnectGattWhenNeeded() {
+ return mAutomaticallyReconnectGattWhenNeeded;
+ }
+
+ /**
+ * If true, we'll finish the pairing process after we've created a bond instead of after
+ * connecting a profile.
+ */
+ public boolean getSkipConnectingProfiles() {
+ return mSkipConnectingProfiles;
+ }
+
+ /**
+ * If true, continues the pairing process if we've timed out due to not receiving UUIDs from the
+ * headset. We can still attempt to connect to A2DP afterwards. If false, Fast Pair will fail
+ * after this step since we're expecting to receive the UUIDs.
+ */
+ public boolean getIgnoreUuidTimeoutAfterBonded() {
+ return mIgnoreUuidTimeoutAfterBonded;
+ }
+
+ /**
+ * If true, a specific transport type will be included in the create bond request, which will be
+ * used for dual mode devices. Otherwise, we'll use the platform defined default which is
+ * BluetoothDevice.TRANSPORT_AUTO. See {@link #getCreateBondTransportType()}.
+ */
+ public boolean getSpecifyCreateBondTransportType() {
+ return mSpecifyCreateBondTransportType;
+ }
+
+ /**
+ * The transport type to use when creating a bond when
+ * {@link #getSpecifyCreateBondTransportType() is true. This should be one of
+ * BluetoothDevice.TRANSPORT_AUTO, BluetoothDevice.TRANSPORT_BREDR,
+ * or BluetoothDevice.TRANSPORT_LE.
+ */
+ public int getCreateBondTransportType() {
+ return mCreateBondTransportType;
+ }
+
+ /**
+ * Whether to increase intent filter priority.
+ */
+ public boolean getIncreaseIntentFilterPriority() {
+ return mIncreaseIntentFilterPriority;
+ }
+
+ /**
+ * Whether to evaluate performance.
+ */
+ public boolean getEvaluatePerformance() {
+ return mEvaluatePerformance;
+ }
+
+ /**
+ * Returns extra logging information.
+ */
+ @Nullable
+ public ExtraLoggingInformation getExtraLoggingInformation() {
+ return mExtraLoggingInformation;
+ }
+
+ /**
+ * Whether to enable naming characteristic.
+ */
+ public boolean getEnableNamingCharacteristic() {
+ return mEnableNamingCharacteristic;
+ }
+
+ /**
+ * Whether to enable firmware version characteristic.
+ */
+ public boolean getEnableFirmwareVersionCharacteristic() {
+ return mEnableFirmwareVersionCharacteristic;
+ }
+
+ /**
+ * If true, even Fast Pair identifies a provider have paired with the account, still writes the
+ * identified account key to the provider.
+ */
+ public boolean getKeepSameAccountKeyWrite() {
+ return mKeepSameAccountKeyWrite;
+ }
+
+ /**
+ * If true, run retroactive pairing.
+ */
+ public boolean getIsRetroactivePairing() {
+ return mIsRetroactivePairing;
+ }
+
+ /**
+ * If it's larger than 0, {@link android.bluetooth.BluetoothDevice#fetchUuidsWithSdp} would be
+ * triggered with number of attempts after device is bonded and no profiles were automatically
+ * discovered".
+ */
+ public int getNumSdpAttemptsAfterBonded() {
+ return mNumSdpAttemptsAfterBonded;
+ }
+
+ /**
+ * If true, supports HID device for fastpair.
+ */
+ public boolean getSupportHidDevice() {
+ return mSupportHidDevice;
+ }
+
+ /**
+ * If true, we'll enable the pairing behavior to handle the state transition from BOND_BONDED to
+ * BOND_BONDING when directly connecting profiles.
+ */
+ public boolean getEnablePairingWhileDirectlyConnecting() {
+ return mEnablePairingWhileDirectlyConnecting;
+ }
+
+ /**
+ * If true, we will accept the user confirmation when bonding with FastPair 1.0 devices.
+ */
+ public boolean getAcceptConsentForFastPairOne() {
+ return mAcceptConsentForFastPairOne;
+ }
+
+ /**
+ * If it's larger than 0, we will retry connecting GATT within the timeout.
+ */
+ public int getGattConnectRetryTimeoutMillis() {
+ return mGattConnectRetryTimeoutMillis;
+ }
+
+ /**
+ * If true, then uses the new custom GATT characteristics {go/fastpair-128bit-gatt}.
+ */
+ public boolean getEnable128BitCustomGattCharacteristicsId() {
+ return mEnable128BitCustomGattCharacteristicsId;
+ }
+
+ /**
+ * If true, then sends the internal pair step or Exception to Validator by Intent.
+ */
+ public boolean getEnableSendExceptionStepToValidator() {
+ return mEnableSendExceptionStepToValidator;
+ }
+
+ /**
+ * If true, then adds the additional data type in the handshake packet when action over BLE.
+ */
+ public boolean getEnableAdditionalDataTypeWhenActionOverBle() {
+ return mEnableAdditionalDataTypeWhenActionOverBle;
+ }
+
+ /**
+ * If true, then checks the bond state when skips connecting profiles in the pairing shortcut.
+ */
+ public boolean getCheckBondStateWhenSkipConnectingProfiles() {
+ return mCheckBondStateWhenSkipConnectingProfiles;
+ }
+
+ /**
+ * If true, the passkey confirmation will be handled by the half-sheet UI.
+ */
+ public boolean getHandlePasskeyConfirmationByUi() {
+ return mHandlePasskeyConfirmationByUi;
+ }
+
+ /**
+ * If true, then use pair flow to show ui when pairing is finished without connecting profile.
+ */
+ public boolean getEnablePairFlowShowUiWithoutProfileConnection() {
+ return mEnablePairFlowShowUiWithoutProfileConnection;
+ }
+
+ @Override
+ public String toString() {
+ return "Preferences{"
+ + "gattOperationTimeoutSeconds=" + mGattOperationTimeoutSeconds + ", "
+ + "gattConnectionTimeoutSeconds=" + mGattConnectionTimeoutSeconds + ", "
+ + "bluetoothToggleTimeoutSeconds=" + mBluetoothToggleTimeoutSeconds + ", "
+ + "bluetoothToggleSleepSeconds=" + mBluetoothToggleSleepSeconds + ", "
+ + "classicDiscoveryTimeoutSeconds=" + mClassicDiscoveryTimeoutSeconds + ", "
+ + "numDiscoverAttempts=" + mNumDiscoverAttempts + ", "
+ + "discoveryRetrySleepSeconds=" + mDiscoveryRetrySleepSeconds + ", "
+ + "ignoreDiscoveryError=" + mIgnoreDiscoveryError + ", "
+ + "sdpTimeoutSeconds=" + mSdpTimeoutSeconds + ", "
+ + "numSdpAttempts=" + mNumSdpAttempts + ", "
+ + "numCreateBondAttempts=" + mNumCreateBondAttempts + ", "
+ + "numConnectAttempts=" + mNumConnectAttempts + ", "
+ + "numWriteAccountKeyAttempts=" + mNumWriteAccountKeyAttempts + ", "
+ + "toggleBluetoothOnFailure=" + mToggleBluetoothOnFailure + ", "
+ + "bluetoothStateUsesPolling=" + mBluetoothStateUsesPolling + ", "
+ + "bluetoothStatePollingMillis=" + mBluetoothStatePollingMillis + ", "
+ + "numAttempts=" + mNumAttempts + ", "
+ + "enableBrEdrHandover=" + mEnableBrEdrHandover + ", "
+ + "brHandoverDataCharacteristicId=" + mBrHandoverDataCharacteristicId + ", "
+ + "bluetoothSigDataCharacteristicId=" + mBluetoothSigDataCharacteristicId + ", "
+ + "firmwareVersionCharacteristicId=" + mFirmwareVersionCharacteristicId + ", "
+ + "brTransportBlockDataDescriptorId=" + mBrTransportBlockDataDescriptorId + ", "
+ + "waitForUuidsAfterBonding=" + mWaitForUuidsAfterBonding + ", "
+ + "receiveUuidsAndBondedEventBeforeClose=" + mReceiveUuidsAndBondedEventBeforeClose
+ + ", "
+ + "removeBondTimeoutSeconds=" + mRemoveBondTimeoutSeconds + ", "
+ + "removeBondSleepMillis=" + mRemoveBondSleepMillis + ", "
+ + "createBondTimeoutSeconds=" + mCreateBondTimeoutSeconds + ", "
+ + "hidCreateBondTimeoutSeconds=" + mHidCreateBondTimeoutSeconds + ", "
+ + "proxyTimeoutSeconds=" + mProxyTimeoutSeconds + ", "
+ + "rejectPhonebookAccess=" + mRejectPhonebookAccess + ", "
+ + "rejectMessageAccess=" + mRejectMessageAccess + ", "
+ + "rejectSimAccess=" + mRejectSimAccess + ", "
+ + "writeAccountKeySleepMillis=" + mWriteAccountKeySleepMillis + ", "
+ + "skipDisconnectingGattBeforeWritingAccountKey="
+ + mSkipDisconnectingGattBeforeWritingAccountKey + ", "
+ + "moreEventLogForQuality=" + mMoreEventLogForQuality + ", "
+ + "retryGattConnectionAndSecretHandshake=" + mRetryGattConnectionAndSecretHandshake
+ + ", "
+ + "gattConnectShortTimeoutMs=" + mGattConnectShortTimeoutMs + ", "
+ + "gattConnectLongTimeoutMs=" + mGattConnectLongTimeoutMs + ", "
+ + "gattConnectShortTimeoutRetryMaxSpentTimeMs="
+ + mGattConnectShortTimeoutRetryMaxSpentTimeMs + ", "
+ + "addressRotateRetryMaxSpentTimeMs=" + mAddressRotateRetryMaxSpentTimeMs + ", "
+ + "pairingRetryDelayMs=" + mPairingRetryDelayMs + ", "
+ + "secretHandshakeShortTimeoutMs=" + mSecretHandshakeShortTimeoutMs + ", "
+ + "secretHandshakeLongTimeoutMs=" + mSecretHandshakeLongTimeoutMs + ", "
+ + "secretHandshakeShortTimeoutRetryMaxSpentTimeMs="
+ + mSecretHandshakeShortTimeoutRetryMaxSpentTimeMs + ", "
+ + "secretHandshakeLongTimeoutRetryMaxSpentTimeMs="
+ + mSecretHandshakeLongTimeoutRetryMaxSpentTimeMs + ", "
+ + "secretHandshakeRetryAttempts=" + mSecretHandshakeRetryAttempts + ", "
+ + "secretHandshakeRetryGattConnectionMaxSpentTimeMs="
+ + mSecretHandshakeRetryGattConnectionMaxSpentTimeMs + ", "
+ + "signalLostRetryMaxSpentTimeMs=" + mSignalLostRetryMaxSpentTimeMs + ", "
+ + "gattConnectionAndSecretHandshakeNoRetryGattError="
+ + mGattConnectionAndSecretHandshakeNoRetryGattError + ", "
+ + "retrySecretHandshakeTimeout=" + mRetrySecretHandshakeTimeout + ", "
+ + "logUserManualRetry=" + mLogUserManualRetry + ", "
+ + "pairFailureCounts=" + mPairFailureCounts + ", "
+ + "cachedDeviceAddress=" + mCachedDeviceAddress + ", "
+ + "possibleCachedDeviceAddress=" + mPossibleCachedDeviceAddress + ", "
+ + "sameModelIdPairedDeviceCount=" + mSameModelIdPairedDeviceCount + ", "
+ + "isDeviceFinishCheckAddressFromCache=" + mIsDeviceFinishCheckAddressFromCache
+ + ", "
+ + "logPairWithCachedModelId=" + mLogPairWithCachedModelId + ", "
+ + "directConnectProfileIfModelIdInCache=" + mDirectConnectProfileIfModelIdInCache
+ + ", "
+ + "acceptPasskey=" + mAcceptPasskey + ", "
+ + "supportedProfileUuids=" + Arrays.toString(mSupportedProfileUuids) + ", "
+ + "providerInitiatesBondingIfSupported=" + mProviderInitiatesBondingIfSupported
+ + ", "
+ + "attemptDirectConnectionWhenPreviouslyBonded="
+ + mAttemptDirectConnectionWhenPreviouslyBonded + ", "
+ + "automaticallyReconnectGattWhenNeeded=" + mAutomaticallyReconnectGattWhenNeeded
+ + ", "
+ + "skipConnectingProfiles=" + mSkipConnectingProfiles + ", "
+ + "ignoreUuidTimeoutAfterBonded=" + mIgnoreUuidTimeoutAfterBonded + ", "
+ + "specifyCreateBondTransportType=" + mSpecifyCreateBondTransportType + ", "
+ + "createBondTransportType=" + mCreateBondTransportType + ", "
+ + "increaseIntentFilterPriority=" + mIncreaseIntentFilterPriority + ", "
+ + "evaluatePerformance=" + mEvaluatePerformance + ", "
+ + "extraLoggingInformation=" + mExtraLoggingInformation + ", "
+ + "enableNamingCharacteristic=" + mEnableNamingCharacteristic + ", "
+ + "enableFirmwareVersionCharacteristic=" + mEnableFirmwareVersionCharacteristic
+ + ", "
+ + "keepSameAccountKeyWrite=" + mKeepSameAccountKeyWrite + ", "
+ + "isRetroactivePairing=" + mIsRetroactivePairing + ", "
+ + "numSdpAttemptsAfterBonded=" + mNumSdpAttemptsAfterBonded + ", "
+ + "supportHidDevice=" + mSupportHidDevice + ", "
+ + "enablePairingWhileDirectlyConnecting=" + mEnablePairingWhileDirectlyConnecting
+ + ", "
+ + "acceptConsentForFastPairOne=" + mAcceptConsentForFastPairOne + ", "
+ + "gattConnectRetryTimeoutMillis=" + mGattConnectRetryTimeoutMillis + ", "
+ + "enable128BitCustomGattCharacteristicsId="
+ + mEnable128BitCustomGattCharacteristicsId + ", "
+ + "enableSendExceptionStepToValidator=" + mEnableSendExceptionStepToValidator + ", "
+ + "enableAdditionalDataTypeWhenActionOverBle="
+ + mEnableAdditionalDataTypeWhenActionOverBle + ", "
+ + "checkBondStateWhenSkipConnectingProfiles="
+ + mCheckBondStateWhenSkipConnectingProfiles + ", "
+ + "handlePasskeyConfirmationByUi=" + mHandlePasskeyConfirmationByUi + ", "
+ + "enablePairFlowShowUiWithoutProfileConnection="
+ + mEnablePairFlowShowUiWithoutProfileConnection
+ + "}";
+ }
+
+ /**
+ * Converts an instance to a builder.
+ */
+ public Builder toBuilder() {
+ return new Preferences.Builder(this);
+ }
+
+ /**
+ * Constructs a builder.
+ */
+ public static Builder builder() {
+ return new Preferences.Builder()
+ .setGattOperationTimeoutSeconds(3)
+ .setGattConnectionTimeoutSeconds(15)
+ .setBluetoothToggleTimeoutSeconds(10)
+ .setBluetoothToggleSleepSeconds(2)
+ .setClassicDiscoveryTimeoutSeconds(10)
+ .setNumDiscoverAttempts(3)
+ .setDiscoveryRetrySleepSeconds(1)
+ .setIgnoreDiscoveryError(false)
+ .setSdpTimeoutSeconds(10)
+ .setNumSdpAttempts(3)
+ .setNumCreateBondAttempts(3)
+ .setNumConnectAttempts(1)
+ .setNumWriteAccountKeyAttempts(3)
+ .setToggleBluetoothOnFailure(false)
+ .setBluetoothStateUsesPolling(true)
+ .setBluetoothStatePollingMillis(1000)
+ .setNumAttempts(2)
+ .setEnableBrEdrHandover(false)
+ .setBrHandoverDataCharacteristicId(get16BitUuid(
+ Constants.TransportDiscoveryService.BrHandoverDataCharacteristic.ID))
+ .setBluetoothSigDataCharacteristicId(get16BitUuid(
+ Constants.TransportDiscoveryService.BluetoothSigDataCharacteristic.ID))
+ .setFirmwareVersionCharacteristicId(get16BitUuid(FirmwareVersionCharacteristic.ID))
+ .setBrTransportBlockDataDescriptorId(
+ get16BitUuid(
+ Constants.TransportDiscoveryService.BluetoothSigDataCharacteristic
+ .BrTransportBlockDataDescriptor.ID))
+ .setWaitForUuidsAfterBonding(true)
+ .setReceiveUuidsAndBondedEventBeforeClose(true)
+ .setRemoveBondTimeoutSeconds(5)
+ .setRemoveBondSleepMillis(1000)
+ .setCreateBondTimeoutSeconds(15)
+ .setHidCreateBondTimeoutSeconds(40)
+ .setProxyTimeoutSeconds(2)
+ .setRejectPhonebookAccess(false)
+ .setRejectMessageAccess(false)
+ .setRejectSimAccess(false)
+ .setAcceptPasskey(true)
+ .setSupportedProfileUuids(Constants.getSupportedProfiles())
+ .setWriteAccountKeySleepMillis(2000)
+ .setProviderInitiatesBondingIfSupported(false)
+ .setAttemptDirectConnectionWhenPreviouslyBonded(false)
+ .setAutomaticallyReconnectGattWhenNeeded(false)
+ .setSkipDisconnectingGattBeforeWritingAccountKey(false)
+ .setSkipConnectingProfiles(false)
+ .setIgnoreUuidTimeoutAfterBonded(false)
+ .setSpecifyCreateBondTransportType(false)
+ .setCreateBondTransportType(0 /*BluetoothDevice.TRANSPORT_AUTO*/)
+ .setIncreaseIntentFilterPriority(true)
+ .setEvaluatePerformance(false)
+ .setKeepSameAccountKeyWrite(true)
+ .setEnableNamingCharacteristic(false)
+ .setEnableFirmwareVersionCharacteristic(false)
+ .setIsRetroactivePairing(false)
+ .setNumSdpAttemptsAfterBonded(1)
+ .setSupportHidDevice(false)
+ .setEnablePairingWhileDirectlyConnecting(true)
+ .setAcceptConsentForFastPairOne(true)
+ .setGattConnectRetryTimeoutMillis(0)
+ .setEnable128BitCustomGattCharacteristicsId(true)
+ .setEnableSendExceptionStepToValidator(true)
+ .setEnableAdditionalDataTypeWhenActionOverBle(true)
+ .setCheckBondStateWhenSkipConnectingProfiles(true)
+ .setHandlePasskeyConfirmationByUi(false)
+ .setMoreEventLogForQuality(true)
+ .setRetryGattConnectionAndSecretHandshake(true)
+ .setGattConnectShortTimeoutMs(7000)
+ .setGattConnectLongTimeoutMs(15000)
+ .setGattConnectShortTimeoutRetryMaxSpentTimeMs(10000)
+ .setAddressRotateRetryMaxSpentTimeMs(15000)
+ .setPairingRetryDelayMs(100)
+ .setSecretHandshakeShortTimeoutMs(3000)
+ .setSecretHandshakeLongTimeoutMs(10000)
+ .setSecretHandshakeShortTimeoutRetryMaxSpentTimeMs(5000)
+ .setSecretHandshakeLongTimeoutRetryMaxSpentTimeMs(7000)
+ .setSecretHandshakeRetryAttempts(3)
+ .setSecretHandshakeRetryGattConnectionMaxSpentTimeMs(15000)
+ .setSignalLostRetryMaxSpentTimeMs(15000)
+ .setGattConnectionAndSecretHandshakeNoRetryGattError(ImmutableSet.of())
+ .setRetrySecretHandshakeTimeout(false)
+ .setLogUserManualRetry(true)
+ .setPairFailureCounts(0)
+ .setEnablePairFlowShowUiWithoutProfileConnection(true)
+ .setPairFailureCounts(0)
+ .setLogPairWithCachedModelId(true)
+ .setDirectConnectProfileIfModelIdInCache(false)
+ .setCachedDeviceAddress("")
+ .setPossibleCachedDeviceAddress("")
+ .setSameModelIdPairedDeviceCount(0)
+ .setIsDeviceFinishCheckAddressFromCache(true);
+ }
+
+ /**
+ * Constructs a builder from GmsLog.
+ */
+ // TODO(b/206668142): remove this builder once api is ready.
+ public static Builder builderFromGmsLog() {
+ return new Preferences.Builder()
+ .setGattOperationTimeoutSeconds(10)
+ .setGattConnectionTimeoutSeconds(15)
+ .setBluetoothToggleTimeoutSeconds(10)
+ .setBluetoothToggleSleepSeconds(2)
+ .setClassicDiscoveryTimeoutSeconds(13)
+ .setNumDiscoverAttempts(3)
+ .setDiscoveryRetrySleepSeconds(1)
+ .setIgnoreDiscoveryError(true)
+ .setSdpTimeoutSeconds(10)
+ .setNumSdpAttempts(0)
+ .setNumCreateBondAttempts(3)
+ .setNumConnectAttempts(2)
+ .setNumWriteAccountKeyAttempts(3)
+ .setToggleBluetoothOnFailure(false)
+ .setBluetoothStateUsesPolling(true)
+ .setBluetoothStatePollingMillis(1000)
+ .setNumAttempts(2)
+ .setEnableBrEdrHandover(false)
+ .setBrHandoverDataCharacteristicId((short) 11265)
+ .setBluetoothSigDataCharacteristicId((short) 11266)
+ .setFirmwareVersionCharacteristicId((short) 10790)
+ .setBrTransportBlockDataDescriptorId((short) 11267)
+ .setWaitForUuidsAfterBonding(true)
+ .setReceiveUuidsAndBondedEventBeforeClose(true)
+ .setRemoveBondTimeoutSeconds(5)
+ .setRemoveBondSleepMillis(1000)
+ .setCreateBondTimeoutSeconds(15)
+ .setHidCreateBondTimeoutSeconds(40)
+ .setProxyTimeoutSeconds(2)
+ .setRejectPhonebookAccess(false)
+ .setRejectMessageAccess(false)
+ .setRejectSimAccess(false)
+ .setAcceptPasskey(true)
+ .setSupportedProfileUuids(Constants.getSupportedProfiles())
+ .setWriteAccountKeySleepMillis(2000)
+ .setProviderInitiatesBondingIfSupported(false)
+ .setAttemptDirectConnectionWhenPreviouslyBonded(true)
+ .setAutomaticallyReconnectGattWhenNeeded(true)
+ .setSkipDisconnectingGattBeforeWritingAccountKey(true)
+ .setSkipConnectingProfiles(false)
+ .setIgnoreUuidTimeoutAfterBonded(true)
+ .setSpecifyCreateBondTransportType(false)
+ .setCreateBondTransportType(0 /*BluetoothDevice.TRANSPORT_AUTO*/)
+ .setIncreaseIntentFilterPriority(true)
+ .setEvaluatePerformance(true)
+ .setKeepSameAccountKeyWrite(true)
+ .setEnableNamingCharacteristic(true)
+ .setEnableFirmwareVersionCharacteristic(true)
+ .setIsRetroactivePairing(false)
+ .setNumSdpAttemptsAfterBonded(1)
+ .setSupportHidDevice(false)
+ .setEnablePairingWhileDirectlyConnecting(true)
+ .setAcceptConsentForFastPairOne(true)
+ .setGattConnectRetryTimeoutMillis(18000)
+ .setEnable128BitCustomGattCharacteristicsId(true)
+ .setEnableSendExceptionStepToValidator(true)
+ .setEnableAdditionalDataTypeWhenActionOverBle(true)
+ .setCheckBondStateWhenSkipConnectingProfiles(true)
+ .setHandlePasskeyConfirmationByUi(false)
+ .setMoreEventLogForQuality(true)
+ .setRetryGattConnectionAndSecretHandshake(true)
+ .setGattConnectShortTimeoutMs(7000)
+ .setGattConnectLongTimeoutMs(15000)
+ .setGattConnectShortTimeoutRetryMaxSpentTimeMs(10000)
+ .setAddressRotateRetryMaxSpentTimeMs(15000)
+ .setPairingRetryDelayMs(100)
+ .setSecretHandshakeShortTimeoutMs(3000)
+ .setSecretHandshakeLongTimeoutMs(10000)
+ .setSecretHandshakeShortTimeoutRetryMaxSpentTimeMs(5000)
+ .setSecretHandshakeLongTimeoutRetryMaxSpentTimeMs(7000)
+ .setSecretHandshakeRetryAttempts(3)
+ .setSecretHandshakeRetryGattConnectionMaxSpentTimeMs(15000)
+ .setSignalLostRetryMaxSpentTimeMs(15000)
+ .setGattConnectionAndSecretHandshakeNoRetryGattError(ImmutableSet.of(257))
+ .setRetrySecretHandshakeTimeout(false)
+ .setLogUserManualRetry(true)
+ .setPairFailureCounts(0)
+ .setEnablePairFlowShowUiWithoutProfileConnection(true)
+ .setPairFailureCounts(0)
+ .setLogPairWithCachedModelId(true)
+ .setDirectConnectProfileIfModelIdInCache(true)
+ .setCachedDeviceAddress("")
+ .setPossibleCachedDeviceAddress("")
+ .setSameModelIdPairedDeviceCount(0)
+ .setIsDeviceFinishCheckAddressFromCache(true);
+ }
+
+ /**
+ * Preferences builder.
+ */
+ public static class Builder {
+
+ private int mGattOperationTimeoutSeconds;
+ private int mGattConnectionTimeoutSeconds;
+ private int mBluetoothToggleTimeoutSeconds;
+ private int mBluetoothToggleSleepSeconds;
+ private int mClassicDiscoveryTimeoutSeconds;
+ private int mNumDiscoverAttempts;
+ private int mDiscoveryRetrySleepSeconds;
+ private boolean mIgnoreDiscoveryError;
+ private int mSdpTimeoutSeconds;
+ private int mNumSdpAttempts;
+ private int mNumCreateBondAttempts;
+ private int mNumConnectAttempts;
+ private int mNumWriteAccountKeyAttempts;
+ private boolean mToggleBluetoothOnFailure;
+ private boolean mBluetoothStateUsesPolling;
+ private int mBluetoothStatePollingMillis;
+ private int mNumAttempts;
+ private boolean mEnableBrEdrHandover;
+ private short mBrHandoverDataCharacteristicId;
+ private short mBluetoothSigDataCharacteristicId;
+ private short mFirmwareVersionCharacteristicId;
+ private short mBrTransportBlockDataDescriptorId;
+ private boolean mWaitForUuidsAfterBonding;
+ private boolean mReceiveUuidsAndBondedEventBeforeClose;
+ private int mRemoveBondTimeoutSeconds;
+ private int mRemoveBondSleepMillis;
+ private int mCreateBondTimeoutSeconds;
+ private int mHidCreateBondTimeoutSeconds;
+ private int mProxyTimeoutSeconds;
+ private boolean mRejectPhonebookAccess;
+ private boolean mRejectMessageAccess;
+ private boolean mRejectSimAccess;
+ private int mWriteAccountKeySleepMillis;
+ private boolean mSkipDisconnectingGattBeforeWritingAccountKey;
+ private boolean mMoreEventLogForQuality;
+ private boolean mRetryGattConnectionAndSecretHandshake;
+ private long mGattConnectShortTimeoutMs;
+ private long mGattConnectLongTimeoutMs;
+ private long mGattConnectShortTimeoutRetryMaxSpentTimeMs;
+ private long mAddressRotateRetryMaxSpentTimeMs;
+ private long mPairingRetryDelayMs;
+ private long mSecretHandshakeShortTimeoutMs;
+ private long mSecretHandshakeLongTimeoutMs;
+ private long mSecretHandshakeShortTimeoutRetryMaxSpentTimeMs;
+ private long mSecretHandshakeLongTimeoutRetryMaxSpentTimeMs;
+ private long mSecretHandshakeRetryAttempts;
+ private long mSecretHandshakeRetryGattConnectionMaxSpentTimeMs;
+ private long mSignalLostRetryMaxSpentTimeMs;
+ private ImmutableSet<Integer> mGattConnectionAndSecretHandshakeNoRetryGattError;
+ private boolean mRetrySecretHandshakeTimeout;
+ private boolean mLogUserManualRetry;
+ private int mPairFailureCounts;
+ private String mCachedDeviceAddress;
+ private String mPossibleCachedDeviceAddress;
+ private int mSameModelIdPairedDeviceCount;
+ private boolean mIsDeviceFinishCheckAddressFromCache;
+ private boolean mLogPairWithCachedModelId;
+ private boolean mDirectConnectProfileIfModelIdInCache;
+ private boolean mAcceptPasskey;
+ private byte[] mSupportedProfileUuids;
+ private boolean mProviderInitiatesBondingIfSupported;
+ private boolean mAttemptDirectConnectionWhenPreviouslyBonded;
+ private boolean mAutomaticallyReconnectGattWhenNeeded;
+ private boolean mSkipConnectingProfiles;
+ private boolean mIgnoreUuidTimeoutAfterBonded;
+ private boolean mSpecifyCreateBondTransportType;
+ private int mCreateBondTransportType;
+ private boolean mIncreaseIntentFilterPriority;
+ private boolean mEvaluatePerformance;
+ private Preferences.ExtraLoggingInformation mExtraLoggingInformation;
+ private boolean mEnableNamingCharacteristic;
+ private boolean mEnableFirmwareVersionCharacteristic;
+ private boolean mKeepSameAccountKeyWrite;
+ private boolean mIsRetroactivePairing;
+ private int mNumSdpAttemptsAfterBonded;
+ private boolean mSupportHidDevice;
+ private boolean mEnablePairingWhileDirectlyConnecting;
+ private boolean mAcceptConsentForFastPairOne;
+ private int mGattConnectRetryTimeoutMillis;
+ private boolean mEnable128BitCustomGattCharacteristicsId;
+ private boolean mEnableSendExceptionStepToValidator;
+ private boolean mEnableAdditionalDataTypeWhenActionOverBle;
+ private boolean mCheckBondStateWhenSkipConnectingProfiles;
+ private boolean mHandlePasskeyConfirmationByUi;
+ private boolean mEnablePairFlowShowUiWithoutProfileConnection;
+
+ private Builder() {
+ }
+
+ private Builder(Preferences source) {
+ this.mGattOperationTimeoutSeconds = source.getGattOperationTimeoutSeconds();
+ this.mGattConnectionTimeoutSeconds = source.getGattConnectionTimeoutSeconds();
+ this.mBluetoothToggleTimeoutSeconds = source.getBluetoothToggleTimeoutSeconds();
+ this.mBluetoothToggleSleepSeconds = source.getBluetoothToggleSleepSeconds();
+ this.mClassicDiscoveryTimeoutSeconds = source.getClassicDiscoveryTimeoutSeconds();
+ this.mNumDiscoverAttempts = source.getNumDiscoverAttempts();
+ this.mDiscoveryRetrySleepSeconds = source.getDiscoveryRetrySleepSeconds();
+ this.mIgnoreDiscoveryError = source.getIgnoreDiscoveryError();
+ this.mSdpTimeoutSeconds = source.getSdpTimeoutSeconds();
+ this.mNumSdpAttempts = source.getNumSdpAttempts();
+ this.mNumCreateBondAttempts = source.getNumCreateBondAttempts();
+ this.mNumConnectAttempts = source.getNumConnectAttempts();
+ this.mNumWriteAccountKeyAttempts = source.getNumWriteAccountKeyAttempts();
+ this.mToggleBluetoothOnFailure = source.getToggleBluetoothOnFailure();
+ this.mBluetoothStateUsesPolling = source.getBluetoothStateUsesPolling();
+ this.mBluetoothStatePollingMillis = source.getBluetoothStatePollingMillis();
+ this.mNumAttempts = source.getNumAttempts();
+ this.mEnableBrEdrHandover = source.getEnableBrEdrHandover();
+ this.mBrHandoverDataCharacteristicId = source.getBrHandoverDataCharacteristicId();
+ this.mBluetoothSigDataCharacteristicId = source.getBluetoothSigDataCharacteristicId();
+ this.mFirmwareVersionCharacteristicId = source.getFirmwareVersionCharacteristicId();
+ this.mBrTransportBlockDataDescriptorId = source.getBrTransportBlockDataDescriptorId();
+ this.mWaitForUuidsAfterBonding = source.getWaitForUuidsAfterBonding();
+ this.mReceiveUuidsAndBondedEventBeforeClose = source
+ .getReceiveUuidsAndBondedEventBeforeClose();
+ this.mRemoveBondTimeoutSeconds = source.getRemoveBondTimeoutSeconds();
+ this.mRemoveBondSleepMillis = source.getRemoveBondSleepMillis();
+ this.mCreateBondTimeoutSeconds = source.getCreateBondTimeoutSeconds();
+ this.mHidCreateBondTimeoutSeconds = source.getHidCreateBondTimeoutSeconds();
+ this.mProxyTimeoutSeconds = source.getProxyTimeoutSeconds();
+ this.mRejectPhonebookAccess = source.getRejectPhonebookAccess();
+ this.mRejectMessageAccess = source.getRejectMessageAccess();
+ this.mRejectSimAccess = source.getRejectSimAccess();
+ this.mWriteAccountKeySleepMillis = source.getWriteAccountKeySleepMillis();
+ this.mSkipDisconnectingGattBeforeWritingAccountKey = source
+ .getSkipDisconnectingGattBeforeWritingAccountKey();
+ this.mMoreEventLogForQuality = source.getMoreEventLogForQuality();
+ this.mRetryGattConnectionAndSecretHandshake = source
+ .getRetryGattConnectionAndSecretHandshake();
+ this.mGattConnectShortTimeoutMs = source.getGattConnectShortTimeoutMs();
+ this.mGattConnectLongTimeoutMs = source.getGattConnectLongTimeoutMs();
+ this.mGattConnectShortTimeoutRetryMaxSpentTimeMs = source
+ .getGattConnectShortTimeoutRetryMaxSpentTimeMs();
+ this.mAddressRotateRetryMaxSpentTimeMs = source.getAddressRotateRetryMaxSpentTimeMs();
+ this.mPairingRetryDelayMs = source.getPairingRetryDelayMs();
+ this.mSecretHandshakeShortTimeoutMs = source.getSecretHandshakeShortTimeoutMs();
+ this.mSecretHandshakeLongTimeoutMs = source.getSecretHandshakeLongTimeoutMs();
+ this.mSecretHandshakeShortTimeoutRetryMaxSpentTimeMs = source
+ .getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs();
+ this.mSecretHandshakeLongTimeoutRetryMaxSpentTimeMs = source
+ .getSecretHandshakeLongTimeoutRetryMaxSpentTimeMs();
+ this.mSecretHandshakeRetryAttempts = source.getSecretHandshakeRetryAttempts();
+ this.mSecretHandshakeRetryGattConnectionMaxSpentTimeMs = source
+ .getSecretHandshakeRetryGattConnectionMaxSpentTimeMs();
+ this.mSignalLostRetryMaxSpentTimeMs = source.getSignalLostRetryMaxSpentTimeMs();
+ this.mGattConnectionAndSecretHandshakeNoRetryGattError = source
+ .getGattConnectionAndSecretHandshakeNoRetryGattError();
+ this.mRetrySecretHandshakeTimeout = source.getRetrySecretHandshakeTimeout();
+ this.mLogUserManualRetry = source.getLogUserManualRetry();
+ this.mPairFailureCounts = source.getPairFailureCounts();
+ this.mCachedDeviceAddress = source.getCachedDeviceAddress();
+ this.mPossibleCachedDeviceAddress = source.getPossibleCachedDeviceAddress();
+ this.mSameModelIdPairedDeviceCount = source.getSameModelIdPairedDeviceCount();
+ this.mIsDeviceFinishCheckAddressFromCache = source
+ .getIsDeviceFinishCheckAddressFromCache();
+ this.mLogPairWithCachedModelId = source.getLogPairWithCachedModelId();
+ this.mDirectConnectProfileIfModelIdInCache = source
+ .getDirectConnectProfileIfModelIdInCache();
+ this.mAcceptPasskey = source.getAcceptPasskey();
+ this.mSupportedProfileUuids = source.getSupportedProfileUuids();
+ this.mProviderInitiatesBondingIfSupported = source
+ .getProviderInitiatesBondingIfSupported();
+ this.mAttemptDirectConnectionWhenPreviouslyBonded = source
+ .getAttemptDirectConnectionWhenPreviouslyBonded();
+ this.mAutomaticallyReconnectGattWhenNeeded = source
+ .getAutomaticallyReconnectGattWhenNeeded();
+ this.mSkipConnectingProfiles = source.getSkipConnectingProfiles();
+ this.mIgnoreUuidTimeoutAfterBonded = source.getIgnoreUuidTimeoutAfterBonded();
+ this.mSpecifyCreateBondTransportType = source.getSpecifyCreateBondTransportType();
+ this.mCreateBondTransportType = source.getCreateBondTransportType();
+ this.mIncreaseIntentFilterPriority = source.getIncreaseIntentFilterPriority();
+ this.mEvaluatePerformance = source.getEvaluatePerformance();
+ this.mExtraLoggingInformation = source.getExtraLoggingInformation();
+ this.mEnableNamingCharacteristic = source.getEnableNamingCharacteristic();
+ this.mEnableFirmwareVersionCharacteristic = source
+ .getEnableFirmwareVersionCharacteristic();
+ this.mKeepSameAccountKeyWrite = source.getKeepSameAccountKeyWrite();
+ this.mIsRetroactivePairing = source.getIsRetroactivePairing();
+ this.mNumSdpAttemptsAfterBonded = source.getNumSdpAttemptsAfterBonded();
+ this.mSupportHidDevice = source.getSupportHidDevice();
+ this.mEnablePairingWhileDirectlyConnecting = source
+ .getEnablePairingWhileDirectlyConnecting();
+ this.mAcceptConsentForFastPairOne = source.getAcceptConsentForFastPairOne();
+ this.mGattConnectRetryTimeoutMillis = source.getGattConnectRetryTimeoutMillis();
+ this.mEnable128BitCustomGattCharacteristicsId = source
+ .getEnable128BitCustomGattCharacteristicsId();
+ this.mEnableSendExceptionStepToValidator = source
+ .getEnableSendExceptionStepToValidator();
+ this.mEnableAdditionalDataTypeWhenActionOverBle = source
+ .getEnableAdditionalDataTypeWhenActionOverBle();
+ this.mCheckBondStateWhenSkipConnectingProfiles = source
+ .getCheckBondStateWhenSkipConnectingProfiles();
+ this.mHandlePasskeyConfirmationByUi = source.getHandlePasskeyConfirmationByUi();
+ this.mEnablePairFlowShowUiWithoutProfileConnection = source
+ .getEnablePairFlowShowUiWithoutProfileConnection();
+ }
+
+ /**
+ * Set gatt operation timeout.
+ */
+ public Builder setGattOperationTimeoutSeconds(int value) {
+ this.mGattOperationTimeoutSeconds = value;
+ return this;
+ }
+
+ /**
+ * Set gatt connection timeout.
+ */
+ public Builder setGattConnectionTimeoutSeconds(int value) {
+ this.mGattConnectionTimeoutSeconds = value;
+ return this;
+ }
+
+ /**
+ * Set bluetooth toggle timeout.
+ */
+ public Builder setBluetoothToggleTimeoutSeconds(int value) {
+ this.mBluetoothToggleTimeoutSeconds = value;
+ return this;
+ }
+
+ /**
+ * Set bluetooth toggle sleep time.
+ */
+ public Builder setBluetoothToggleSleepSeconds(int value) {
+ this.mBluetoothToggleSleepSeconds = value;
+ return this;
+ }
+
+ /**
+ * Set classic discovery timeout.
+ */
+ public Builder setClassicDiscoveryTimeoutSeconds(int value) {
+ this.mClassicDiscoveryTimeoutSeconds = value;
+ return this;
+ }
+
+ /**
+ * Set number of discover attempts allowed.
+ */
+ public Builder setNumDiscoverAttempts(int value) {
+ this.mNumDiscoverAttempts = value;
+ return this;
+ }
+
+ /**
+ * Set discovery retry sleep time.
+ */
+ public Builder setDiscoveryRetrySleepSeconds(int value) {
+ this.mDiscoveryRetrySleepSeconds = value;
+ return this;
+ }
+
+ /**
+ * Set whether to ignore discovery error.
+ */
+ public Builder setIgnoreDiscoveryError(boolean value) {
+ this.mIgnoreDiscoveryError = value;
+ return this;
+ }
+
+ /**
+ * Set sdp timeout.
+ */
+ public Builder setSdpTimeoutSeconds(int value) {
+ this.mSdpTimeoutSeconds = value;
+ return this;
+ }
+
+ /**
+ * Set number of sdp attempts allowed.
+ */
+ public Builder setNumSdpAttempts(int value) {
+ this.mNumSdpAttempts = value;
+ return this;
+ }
+
+ /**
+ * Set number of allowed attempts to create bond.
+ */
+ public Builder setNumCreateBondAttempts(int value) {
+ this.mNumCreateBondAttempts = value;
+ return this;
+ }
+
+ /**
+ * Set number of connect attempts allowed.
+ */
+ public Builder setNumConnectAttempts(int value) {
+ this.mNumConnectAttempts = value;
+ return this;
+ }
+
+ /**
+ * Set number of write account key attempts allowed.
+ */
+ public Builder setNumWriteAccountKeyAttempts(int value) {
+ this.mNumWriteAccountKeyAttempts = value;
+ return this;
+ }
+
+ /**
+ * Set whether to retry by bluetooth toggle on failure.
+ */
+ public Builder setToggleBluetoothOnFailure(boolean value) {
+ this.mToggleBluetoothOnFailure = value;
+ return this;
+ }
+
+ /**
+ * Set whether to use polling to set bluetooth status.
+ */
+ public Builder setBluetoothStateUsesPolling(boolean value) {
+ this.mBluetoothStateUsesPolling = value;
+ return this;
+ }
+
+ /**
+ * Set Bluetooth state polling timeout.
+ */
+ public Builder setBluetoothStatePollingMillis(int value) {
+ this.mBluetoothStatePollingMillis = value;
+ return this;
+ }
+
+ /**
+ * Set number of attempts.
+ */
+ public Builder setNumAttempts(int value) {
+ this.mNumAttempts = value;
+ return this;
+ }
+
+ /**
+ * Set whether to enable BrEdr handover.
+ */
+ public Builder setEnableBrEdrHandover(boolean value) {
+ this.mEnableBrEdrHandover = value;
+ return this;
+ }
+
+ /**
+ * Set Br handover data characteristic Id.
+ */
+ public Builder setBrHandoverDataCharacteristicId(short value) {
+ this.mBrHandoverDataCharacteristicId = value;
+ return this;
+ }
+
+ /**
+ * Set Bluetooth Sig data characteristic Id.
+ */
+ public Builder setBluetoothSigDataCharacteristicId(short value) {
+ this.mBluetoothSigDataCharacteristicId = value;
+ return this;
+ }
+
+ /**
+ * Set Firmware version characteristic id.
+ */
+ public Builder setFirmwareVersionCharacteristicId(short value) {
+ this.mFirmwareVersionCharacteristicId = value;
+ return this;
+ }
+
+ /**
+ * Set Br transport block data descriptor id.
+ */
+ public Builder setBrTransportBlockDataDescriptorId(short value) {
+ this.mBrTransportBlockDataDescriptorId = value;
+ return this;
+ }
+
+ /**
+ * Set whether to wait for Uuids after bonding.
+ */
+ public Builder setWaitForUuidsAfterBonding(boolean value) {
+ this.mWaitForUuidsAfterBonding = value;
+ return this;
+ }
+
+ /**
+ * Set whether to receive Uuids and bonded event before close.
+ */
+ public Builder setReceiveUuidsAndBondedEventBeforeClose(boolean value) {
+ this.mReceiveUuidsAndBondedEventBeforeClose = value;
+ return this;
+ }
+
+ /**
+ * Set remove bond timeout.
+ */
+ public Builder setRemoveBondTimeoutSeconds(int value) {
+ this.mRemoveBondTimeoutSeconds = value;
+ return this;
+ }
+
+ /**
+ * Set remove bound sleep time.
+ */
+ public Builder setRemoveBondSleepMillis(int value) {
+ this.mRemoveBondSleepMillis = value;
+ return this;
+ }
+
+ /**
+ * Set create bond timeout.
+ */
+ public Builder setCreateBondTimeoutSeconds(int value) {
+ this.mCreateBondTimeoutSeconds = value;
+ return this;
+ }
+
+ /**
+ * Set Hid create bond timeout.
+ */
+ public Builder setHidCreateBondTimeoutSeconds(int value) {
+ this.mHidCreateBondTimeoutSeconds = value;
+ return this;
+ }
+
+ /**
+ * Set proxy timeout.
+ */
+ public Builder setProxyTimeoutSeconds(int value) {
+ this.mProxyTimeoutSeconds = value;
+ return this;
+ }
+
+ /**
+ * Set whether to reject phone book access.
+ */
+ public Builder setRejectPhonebookAccess(boolean value) {
+ this.mRejectPhonebookAccess = value;
+ return this;
+ }
+
+ /**
+ * Set whether to reject message access.
+ */
+ public Builder setRejectMessageAccess(boolean value) {
+ this.mRejectMessageAccess = value;
+ return this;
+ }
+
+ /**
+ * Set whether to reject slim access.
+ */
+ public Builder setRejectSimAccess(boolean value) {
+ this.mRejectSimAccess = value;
+ return this;
+ }
+
+ /**
+ * Set whether to accept passkey.
+ */
+ public Builder setAcceptPasskey(boolean value) {
+ this.mAcceptPasskey = value;
+ return this;
+ }
+
+ /**
+ * Set supported profile Uuids.
+ */
+ public Builder setSupportedProfileUuids(byte[] value) {
+ this.mSupportedProfileUuids = value;
+ return this;
+ }
+
+ /**
+ * Set whether to collect more event log for quality.
+ */
+ public Builder setMoreEventLogForQuality(boolean value) {
+ this.mMoreEventLogForQuality = value;
+ return this;
+ }
+
+ /**
+ * Set supported profile Uuids.
+ */
+ public Builder setSupportedProfileUuids(short... uuids) {
+ return setSupportedProfileUuids(Bytes.toBytes(ByteOrder.BIG_ENDIAN, uuids));
+ }
+
+ /**
+ * Set write account key sleep time.
+ */
+ public Builder setWriteAccountKeySleepMillis(int value) {
+ this.mWriteAccountKeySleepMillis = value;
+ return this;
+ }
+
+ /**
+ * Set whether to do provider initialized bonding if supported.
+ */
+ public Builder setProviderInitiatesBondingIfSupported(boolean value) {
+ this.mProviderInitiatesBondingIfSupported = value;
+ return this;
+ }
+
+ /**
+ * Set whether to try direct connection when the device is previously bonded.
+ */
+ public Builder setAttemptDirectConnectionWhenPreviouslyBonded(boolean value) {
+ this.mAttemptDirectConnectionWhenPreviouslyBonded = value;
+ return this;
+ }
+
+ /**
+ * Set whether to automatically reconnect gatt when needed.
+ */
+ public Builder setAutomaticallyReconnectGattWhenNeeded(boolean value) {
+ this.mAutomaticallyReconnectGattWhenNeeded = value;
+ return this;
+ }
+
+ /**
+ * Set whether to skip disconnecting gatt before writing account key.
+ */
+ public Builder setSkipDisconnectingGattBeforeWritingAccountKey(boolean value) {
+ this.mSkipDisconnectingGattBeforeWritingAccountKey = value;
+ return this;
+ }
+
+ /**
+ * Set whether to skip connecting profiles.
+ */
+ public Builder setSkipConnectingProfiles(boolean value) {
+ this.mSkipConnectingProfiles = value;
+ return this;
+ }
+
+ /**
+ * Set whether to ignore Uuid timeout after bonded.
+ */
+ public Builder setIgnoreUuidTimeoutAfterBonded(boolean value) {
+ this.mIgnoreUuidTimeoutAfterBonded = value;
+ return this;
+ }
+
+ /**
+ * Set whether to include transport type in create bound request.
+ */
+ public Builder setSpecifyCreateBondTransportType(boolean value) {
+ this.mSpecifyCreateBondTransportType = value;
+ return this;
+ }
+
+ /**
+ * Set transport type used in create bond request.
+ */
+ public Builder setCreateBondTransportType(int value) {
+ this.mCreateBondTransportType = value;
+ return this;
+ }
+
+ /**
+ * Set whether to increase intent filter priority.
+ */
+ public Builder setIncreaseIntentFilterPriority(boolean value) {
+ this.mIncreaseIntentFilterPriority = value;
+ return this;
+ }
+
+ /**
+ * Set whether to evaluate performance.
+ */
+ public Builder setEvaluatePerformance(boolean value) {
+ this.mEvaluatePerformance = value;
+ return this;
+ }
+
+ /**
+ * Set extra logging info.
+ */
+ public Builder setExtraLoggingInformation(ExtraLoggingInformation value) {
+ this.mExtraLoggingInformation = value;
+ return this;
+ }
+
+ /**
+ * Set whether to enable naming characteristic.
+ */
+ public Builder setEnableNamingCharacteristic(boolean value) {
+ this.mEnableNamingCharacteristic = value;
+ return this;
+ }
+
+ /**
+ * Set whether to keep writing the account key to the provider, that has already paired with
+ * the account.
+ */
+ public Builder setKeepSameAccountKeyWrite(boolean value) {
+ this.mKeepSameAccountKeyWrite = value;
+ return this;
+ }
+
+ /**
+ * Set whether to enable firmware version characteristic.
+ */
+ public Builder setEnableFirmwareVersionCharacteristic(boolean value) {
+ this.mEnableFirmwareVersionCharacteristic = value;
+ return this;
+ }
+
+ /**
+ * Set whether it is retroactive pairing.
+ */
+ public Builder setIsRetroactivePairing(boolean value) {
+ this.mIsRetroactivePairing = value;
+ return this;
+ }
+
+ /**
+ * Set number of allowed sdp attempts after bonded.
+ */
+ public Builder setNumSdpAttemptsAfterBonded(int value) {
+ this.mNumSdpAttemptsAfterBonded = value;
+ return this;
+ }
+
+ /**
+ * Set whether to support Hid device.
+ */
+ public Builder setSupportHidDevice(boolean value) {
+ this.mSupportHidDevice = value;
+ return this;
+ }
+
+ /**
+ * Set wehther to enable the pairing behavior to handle the state transition from
+ * BOND_BONDED to BOND_BONDING when directly connecting profiles.
+ */
+ public Builder setEnablePairingWhileDirectlyConnecting(boolean value) {
+ this.mEnablePairingWhileDirectlyConnecting = value;
+ return this;
+ }
+
+ /**
+ * Set whether to accept consent for fast pair one.
+ */
+ public Builder setAcceptConsentForFastPairOne(boolean value) {
+ this.mAcceptConsentForFastPairOne = value;
+ return this;
+ }
+
+ /**
+ * Set Gatt connect retry timeout.
+ */
+ public Builder setGattConnectRetryTimeoutMillis(int value) {
+ this.mGattConnectRetryTimeoutMillis = value;
+ return this;
+ }
+
+ /**
+ * Set whether to enable 128 bit custom gatt characteristic Id.
+ */
+ public Builder setEnable128BitCustomGattCharacteristicsId(boolean value) {
+ this.mEnable128BitCustomGattCharacteristicsId = value;
+ return this;
+ }
+
+ /**
+ * Set whether to send exception step to validator.
+ */
+ public Builder setEnableSendExceptionStepToValidator(boolean value) {
+ this.mEnableSendExceptionStepToValidator = value;
+ return this;
+ }
+
+ /**
+ * Set wehther to add the additional data type in the handshake when action over BLE.
+ */
+ public Builder setEnableAdditionalDataTypeWhenActionOverBle(boolean value) {
+ this.mEnableAdditionalDataTypeWhenActionOverBle = value;
+ return this;
+ }
+
+ /**
+ * Set whether to check bond state when skip connecting profiles.
+ */
+ public Builder setCheckBondStateWhenSkipConnectingProfiles(boolean value) {
+ this.mCheckBondStateWhenSkipConnectingProfiles = value;
+ return this;
+ }
+
+ /**
+ * Set whether to handle passkey confirmation by UI.
+ */
+ public Builder setHandlePasskeyConfirmationByUi(boolean value) {
+ this.mHandlePasskeyConfirmationByUi = value;
+ return this;
+ }
+
+ /**
+ * Set wehther to retry gatt connection and secret handshake.
+ */
+ public Builder setRetryGattConnectionAndSecretHandshake(boolean value) {
+ this.mRetryGattConnectionAndSecretHandshake = value;
+ return this;
+ }
+
+ /**
+ * Set gatt connect short timeout.
+ */
+ public Builder setGattConnectShortTimeoutMs(long value) {
+ this.mGattConnectShortTimeoutMs = value;
+ return this;
+ }
+
+ /**
+ * Set gatt connect long timeout.
+ */
+ public Builder setGattConnectLongTimeoutMs(long value) {
+ this.mGattConnectLongTimeoutMs = value;
+ return this;
+ }
+
+ /**
+ * Set gatt connection short timoutout, including retry.
+ */
+ public Builder setGattConnectShortTimeoutRetryMaxSpentTimeMs(long value) {
+ this.mGattConnectShortTimeoutRetryMaxSpentTimeMs = value;
+ return this;
+ }
+
+ /**
+ * Set address rotate timeout, including retry.
+ */
+ public Builder setAddressRotateRetryMaxSpentTimeMs(long value) {
+ this.mAddressRotateRetryMaxSpentTimeMs = value;
+ return this;
+ }
+
+ /**
+ * Set pairing retry delay time.
+ */
+ public Builder setPairingRetryDelayMs(long value) {
+ this.mPairingRetryDelayMs = value;
+ return this;
+ }
+
+ /**
+ * Set secret handshake short timeout.
+ */
+ public Builder setSecretHandshakeShortTimeoutMs(long value) {
+ this.mSecretHandshakeShortTimeoutMs = value;
+ return this;
+ }
+
+ /**
+ * Set secret handshake long timeout.
+ */
+ public Builder setSecretHandshakeLongTimeoutMs(long value) {
+ this.mSecretHandshakeLongTimeoutMs = value;
+ return this;
+ }
+
+ /**
+ * Set secret handshake short timeout retry max spent time.
+ */
+ public Builder setSecretHandshakeShortTimeoutRetryMaxSpentTimeMs(long value) {
+ this.mSecretHandshakeShortTimeoutRetryMaxSpentTimeMs = value;
+ return this;
+ }
+
+ /**
+ * Set secret handshake long timeout retry max spent time.
+ */
+ public Builder setSecretHandshakeLongTimeoutRetryMaxSpentTimeMs(long value) {
+ this.mSecretHandshakeLongTimeoutRetryMaxSpentTimeMs = value;
+ return this;
+ }
+
+ /**
+ * Set secret handshake retry attempts allowed.
+ */
+ public Builder setSecretHandshakeRetryAttempts(long value) {
+ this.mSecretHandshakeRetryAttempts = value;
+ return this;
+ }
+
+ /**
+ * Set secret handshake retry gatt connection max spent time.
+ */
+ public Builder setSecretHandshakeRetryGattConnectionMaxSpentTimeMs(long value) {
+ this.mSecretHandshakeRetryGattConnectionMaxSpentTimeMs = value;
+ return this;
+ }
+
+ /**
+ * Set signal loss retry max spent time.
+ */
+ public Builder setSignalLostRetryMaxSpentTimeMs(long value) {
+ this.mSignalLostRetryMaxSpentTimeMs = value;
+ return this;
+ }
+
+ /**
+ * Set gatt connection and secret handshake no retry gatt error.
+ */
+ public Builder setGattConnectionAndSecretHandshakeNoRetryGattError(
+ ImmutableSet<Integer> value) {
+ this.mGattConnectionAndSecretHandshakeNoRetryGattError = value;
+ return this;
+ }
+
+ /**
+ * Set retry secret handshake timeout.
+ */
+ public Builder setRetrySecretHandshakeTimeout(boolean value) {
+ this.mRetrySecretHandshakeTimeout = value;
+ return this;
+ }
+
+ /**
+ * Set whether to log user manual retry.
+ */
+ public Builder setLogUserManualRetry(boolean value) {
+ this.mLogUserManualRetry = value;
+ return this;
+ }
+
+ /**
+ * Set pair falure counts.
+ */
+ public Builder setPairFailureCounts(int counts) {
+ this.mPairFailureCounts = counts;
+ return this;
+ }
+
+ /**
+ * Set whether to use pair flow to show ui when pairing is finished without connecting
+ * profile..
+ */
+ public Builder setEnablePairFlowShowUiWithoutProfileConnection(boolean value) {
+ this.mEnablePairFlowShowUiWithoutProfileConnection = value;
+ return this;
+ }
+
+ /**
+ * Set whether to log pairing with cached module Id.
+ */
+ public Builder setLogPairWithCachedModelId(boolean value) {
+ this.mLogPairWithCachedModelId = value;
+ return this;
+ }
+
+ /**
+ * Set possible cached device address.
+ */
+ public Builder setPossibleCachedDeviceAddress(String value) {
+ this.mPossibleCachedDeviceAddress = value;
+ return this;
+ }
+
+ /**
+ * Set paired device count from the same module Id.
+ */
+ public Builder setSameModelIdPairedDeviceCount(int value) {
+ this.mSameModelIdPairedDeviceCount = value;
+ return this;
+ }
+
+ /**
+ * Set whether the bonded device address is from cache.
+ */
+ public Builder setIsDeviceFinishCheckAddressFromCache(boolean value) {
+ this.mIsDeviceFinishCheckAddressFromCache = value;
+ return this;
+ }
+
+ /**
+ * Set whether to directly connect profile if modelId is in cache.
+ */
+ public Builder setDirectConnectProfileIfModelIdInCache(boolean value) {
+ this.mDirectConnectProfileIfModelIdInCache = value;
+ return this;
+ }
+
+ /**
+ * Set cached device address.
+ */
+ public Builder setCachedDeviceAddress(String value) {
+ this.mCachedDeviceAddress = value;
+ return this;
+ }
+
+ /**
+ * Builds a Preferences instance.
+ */
+ public Preferences build() {
+ return new Preferences(
+ this.mGattOperationTimeoutSeconds,
+ this.mGattConnectionTimeoutSeconds,
+ this.mBluetoothToggleTimeoutSeconds,
+ this.mBluetoothToggleSleepSeconds,
+ this.mClassicDiscoveryTimeoutSeconds,
+ this.mNumDiscoverAttempts,
+ this.mDiscoveryRetrySleepSeconds,
+ this.mIgnoreDiscoveryError,
+ this.mSdpTimeoutSeconds,
+ this.mNumSdpAttempts,
+ this.mNumCreateBondAttempts,
+ this.mNumConnectAttempts,
+ this.mNumWriteAccountKeyAttempts,
+ this.mToggleBluetoothOnFailure,
+ this.mBluetoothStateUsesPolling,
+ this.mBluetoothStatePollingMillis,
+ this.mNumAttempts,
+ this.mEnableBrEdrHandover,
+ this.mBrHandoverDataCharacteristicId,
+ this.mBluetoothSigDataCharacteristicId,
+ this.mFirmwareVersionCharacteristicId,
+ this.mBrTransportBlockDataDescriptorId,
+ this.mWaitForUuidsAfterBonding,
+ this.mReceiveUuidsAndBondedEventBeforeClose,
+ this.mRemoveBondTimeoutSeconds,
+ this.mRemoveBondSleepMillis,
+ this.mCreateBondTimeoutSeconds,
+ this.mHidCreateBondTimeoutSeconds,
+ this.mProxyTimeoutSeconds,
+ this.mRejectPhonebookAccess,
+ this.mRejectMessageAccess,
+ this.mRejectSimAccess,
+ this.mWriteAccountKeySleepMillis,
+ this.mSkipDisconnectingGattBeforeWritingAccountKey,
+ this.mMoreEventLogForQuality,
+ this.mRetryGattConnectionAndSecretHandshake,
+ this.mGattConnectShortTimeoutMs,
+ this.mGattConnectLongTimeoutMs,
+ this.mGattConnectShortTimeoutRetryMaxSpentTimeMs,
+ this.mAddressRotateRetryMaxSpentTimeMs,
+ this.mPairingRetryDelayMs,
+ this.mSecretHandshakeShortTimeoutMs,
+ this.mSecretHandshakeLongTimeoutMs,
+ this.mSecretHandshakeShortTimeoutRetryMaxSpentTimeMs,
+ this.mSecretHandshakeLongTimeoutRetryMaxSpentTimeMs,
+ this.mSecretHandshakeRetryAttempts,
+ this.mSecretHandshakeRetryGattConnectionMaxSpentTimeMs,
+ this.mSignalLostRetryMaxSpentTimeMs,
+ this.mGattConnectionAndSecretHandshakeNoRetryGattError,
+ this.mRetrySecretHandshakeTimeout,
+ this.mLogUserManualRetry,
+ this.mPairFailureCounts,
+ this.mCachedDeviceAddress,
+ this.mPossibleCachedDeviceAddress,
+ this.mSameModelIdPairedDeviceCount,
+ this.mIsDeviceFinishCheckAddressFromCache,
+ this.mLogPairWithCachedModelId,
+ this.mDirectConnectProfileIfModelIdInCache,
+ this.mAcceptPasskey,
+ this.mSupportedProfileUuids,
+ this.mProviderInitiatesBondingIfSupported,
+ this.mAttemptDirectConnectionWhenPreviouslyBonded,
+ this.mAutomaticallyReconnectGattWhenNeeded,
+ this.mSkipConnectingProfiles,
+ this.mIgnoreUuidTimeoutAfterBonded,
+ this.mSpecifyCreateBondTransportType,
+ this.mCreateBondTransportType,
+ this.mIncreaseIntentFilterPriority,
+ this.mEvaluatePerformance,
+ this.mExtraLoggingInformation,
+ this.mEnableNamingCharacteristic,
+ this.mEnableFirmwareVersionCharacteristic,
+ this.mKeepSameAccountKeyWrite,
+ this.mIsRetroactivePairing,
+ this.mNumSdpAttemptsAfterBonded,
+ this.mSupportHidDevice,
+ this.mEnablePairingWhileDirectlyConnecting,
+ this.mAcceptConsentForFastPairOne,
+ this.mGattConnectRetryTimeoutMillis,
+ this.mEnable128BitCustomGattCharacteristicsId,
+ this.mEnableSendExceptionStepToValidator,
+ this.mEnableAdditionalDataTypeWhenActionOverBle,
+ this.mCheckBondStateWhenSkipConnectingProfiles,
+ this.mHandlePasskeyConfirmationByUi,
+ this.mEnablePairFlowShowUiWithoutProfileConnection);
+ }
+ }
+
+ /**
+ * Whether a given Uuid is supported.
+ */
+ public boolean isSupportedProfile(short profileUuid) {
+ return Constants.PROFILES.containsKey(profileUuid)
+ && Shorts.contains(
+ Bytes.toShorts(ByteOrder.BIG_ENDIAN, getSupportedProfileUuids()), profileUuid);
+ }
+
+ /**
+ * Information that will be used for logging.
+ */
+ public static class ExtraLoggingInformation {
+
+ private final String mModelId;
+
+ private ExtraLoggingInformation(String modelId) {
+ this.mModelId = modelId;
+ }
+
+ /**
+ * Returns model Id.
+ */
+ public String getModelId() {
+ return mModelId;
+ }
+
+ /**
+ * Converts an instance to a builder.
+ */
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ /**
+ * Creates a builder for ExtraLoggingInformation.
+ */
+ public static Builder builder() {
+ return new ExtraLoggingInformation.Builder();
+ }
+
+ @Override
+ public String toString() {
+ return "ExtraLoggingInformation{" + "modelId=" + mModelId + "}";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof ExtraLoggingInformation) {
+ Preferences.ExtraLoggingInformation that = (Preferences.ExtraLoggingInformation) o;
+ return this.mModelId.equals(that.getModelId());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mModelId);
+ }
+
+ /**
+ * Extra logging information builder.
+ */
+ public static class Builder {
+
+ private String mModelId;
+
+ private Builder() {
+ }
+
+ private Builder(ExtraLoggingInformation source) {
+ this.mModelId = source.getModelId();
+ }
+
+ /**
+ * Set model ID.
+ */
+ public Builder setModelId(String modelId) {
+ this.mModelId = modelId;
+ return this;
+ }
+
+ /**
+ * Builds extra logging information.
+ */
+ public ExtraLoggingInformation build() {
+ return new ExtraLoggingInformation(mModelId);
+ }
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Reflect.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Reflect.java
new file mode 100644
index 0000000..a2603b5
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Reflect.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import 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>
+ */
+// TODO(b/202549655): remove existing Reflect usage. New usage is not allowed! No exception!
+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/service/java/com/android/server/nearby/common/bluetooth/fastpair/ReflectionException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ReflectionException.java
new file mode 100644
index 0000000..1c20c55
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ReflectionException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+/**
+ * An exception thrown during a reflection operation. Like ReflectiveOperationException, except
+ * compatible on older API versions.
+ */
+public final class ReflectionException extends Exception {
+ ReflectionException(Throwable cause) {
+ super(cause.getMessage(), cause);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SignalLostException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SignalLostException.java
new file mode 100644
index 0000000..244ee66
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SignalLostException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+/** Base class for fast pair signal lost exceptions. */
+public class SignalLostException extends PairingException {
+ SignalLostException(String message, Exception e) {
+ super(message);
+ initCause(e);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SignalRotatedException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SignalRotatedException.java
new file mode 100644
index 0000000..d0d2a5d
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SignalRotatedException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+/** Base class for fast pair signal rotated exceptions. */
+public class SignalRotatedException extends PairingException {
+ private final String mNewAddress;
+
+ SignalRotatedException(String message, String newAddress, Exception e) {
+ super(message);
+ this.mNewAddress = newAddress;
+ initCause(e);
+ }
+
+ /** Returns the new BLE address for the model ID. */
+ public String getNewAddress() {
+ return mNewAddress;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SimpleBroadcastReceiver.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SimpleBroadcastReceiver.java
new file mode 100644
index 0000000..7f525a7
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/SimpleBroadcastReceiver.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build.VERSION_CODES;
+import android.os.Handler;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.util.Arrays;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Like {@link BroadcastReceiver}, but:
+ *
+ * <ul>
+ * <li>Simpler to create and register, with a list of actions.
+ * <li>Implements AutoCloseable. If used as a resource in try-with-resources (available on
+ * KitKat+), unregisters itself automatically.
+ * <li>Lets you block waiting for your state transition with {@link #await}.
+ * </ul>
+ */
+// AutoCloseable only available on KitKat+.
+@TargetApi(VERSION_CODES.KITKAT)
+public abstract class SimpleBroadcastReceiver extends BroadcastReceiver implements AutoCloseable {
+
+ private static final String TAG = SimpleBroadcastReceiver.class.getSimpleName();
+
+ /**
+ * Creates a one shot receiver.
+ */
+ public static SimpleBroadcastReceiver oneShotReceiver(
+ Context context, Preferences preferences, String... actions) {
+ return new SimpleBroadcastReceiver(context, preferences, actions) {
+ @Override
+ protected void onReceive(Intent intent) {
+ close();
+ }
+ };
+ }
+
+ private final Context mContext;
+ private final SettableFuture<Void> mIsClosedFuture = SettableFuture.create();
+ private long mAwaitExtendSecond;
+
+ // Nullness checker complains about 'this' being @UnderInitialization
+ @SuppressWarnings("nullness")
+ public SimpleBroadcastReceiver(
+ Context context, Preferences preferences, @Nullable Handler handler,
+ String... actions) {
+ Log.v(TAG, this + " listening for actions " + Arrays.toString(actions));
+ this.mContext = context;
+ IntentFilter intentFilter = new IntentFilter();
+ if (preferences.getIncreaseIntentFilterPriority()) {
+ intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ }
+ for (String action : actions) {
+ intentFilter.addAction(action);
+ }
+ context.registerReceiver(this, intentFilter, /* broadcastPermission= */ null, handler);
+ }
+
+ public SimpleBroadcastReceiver(Context context, Preferences preferences, String... actions) {
+ this(context, preferences, /* handler= */ null, actions);
+ }
+
+ /**
+ * Any exception thrown by this method will be delivered via {@link #await}.
+ */
+ protected abstract void onReceive(Intent intent) throws Exception;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.v(TAG, "Got intent with action= " + intent.getAction());
+ try {
+ onReceive(intent);
+ } catch (Exception e) {
+ closeWithError(e);
+ }
+ }
+
+ @Override
+ public void close() {
+ closeWithError(null);
+ }
+
+ void closeWithError(@Nullable Exception e) {
+ try {
+ mContext.unregisterReceiver(this);
+ } catch (IllegalArgumentException ignored) {
+ // Ignore. Happens if you unregister twice.
+ }
+ if (e == null) {
+ mIsClosedFuture.set(null);
+ } else {
+ mIsClosedFuture.setException(e);
+ }
+ }
+
+ /**
+ * Extends the awaiting time.
+ */
+ public void extendAwaitSecond(int awaitExtendSecond) {
+ this.mAwaitExtendSecond = awaitExtendSecond;
+ }
+
+ /**
+ * Blocks until this receiver has closed (i.e. the state transition that this receiver is
+ * interested in has completed). Throws an exception on any error.
+ */
+ public void await(long timeout, TimeUnit timeUnit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ Log.v(TAG, this + " waiting on future for " + timeout + " " + timeUnit);
+ try {
+ mIsClosedFuture.get(timeout, timeUnit);
+ } catch (TimeoutException e) {
+ if (mAwaitExtendSecond <= 0) {
+ throw e;
+ }
+ Log.i(TAG, "Extend timeout for " + mAwaitExtendSecond + " seconds");
+ mIsClosedFuture.get(mAwaitExtendSecond, TimeUnit.SECONDS);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/TdsException.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/TdsException.java
new file mode 100644
index 0000000..7382ff3
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/TdsException.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import com.android.server.nearby.intdefs.FastPairEventIntDefs.BrEdrHandoverErrorCode;
+
+import com.google.errorprone.annotations.FormatMethod;
+
+/**
+ * Thrown when BR/EDR Handover fails.
+ */
+public class TdsException extends Exception {
+
+ final @BrEdrHandoverErrorCode int mErrorCode;
+
+ @FormatMethod
+ TdsException(@BrEdrHandoverErrorCode int errorCode, String format, Object... objects) {
+ super(String.format(format, objects));
+ this.mErrorCode = errorCode;
+ }
+
+ /** Returns error code. */
+ public @BrEdrHandoverErrorCode int getErrorCode() {
+ return mErrorCode;
+ }
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/TimingLogger.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/TimingLogger.java
new file mode 100644
index 0000000..83ee309
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/TimingLogger.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.ArrayDeque;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * A profiler for performance metrics.
+ *
+ * <p>This class aim to break down the execution time for each steps of process to figure out the
+ * bottleneck.
+ */
+public class TimingLogger {
+
+ private static final String TAG = TimingLogger.class.getSimpleName();
+
+ /**
+ * The name of this session.
+ */
+ private final String mName;
+
+ private final Preferences mPreference;
+
+ /**
+ * The ordered timing sequence data. It's composed by a paired {@link Timing} generated from
+ * {@link #start} and {@link #end}.
+ */
+ private final List<Timing> mTimings;
+
+ private final long mStartTimestampMs;
+
+ /** Constructor. */
+ public TimingLogger(String name, Preferences mPreference) {
+ this.mName = name;
+ this.mPreference = mPreference;
+ mTimings = new CopyOnWriteArrayList<>();
+ mStartTimestampMs = SystemClock.elapsedRealtime();
+ }
+
+ @VisibleForTesting
+ List<Timing> getTimings() {
+ return mTimings;
+ }
+
+ /**
+ * Start a new paired timing.
+ *
+ * @param label The split name of paired timing.
+ */
+ public void start(String label) {
+ if (mPreference.getEvaluatePerformance()) {
+ mTimings.add(new Timing(label));
+ }
+ }
+
+ /**
+ * End a paired timing.
+ */
+ public void end() {
+ if (mPreference.getEvaluatePerformance()) {
+ mTimings.add(new Timing(Timing.END_LABEL));
+ }
+ }
+
+ /**
+ * Print out the timing data.
+ */
+ public void dump() {
+ if (!mPreference.getEvaluatePerformance()) {
+ return;
+ }
+
+ calculateTiming();
+ Log.i(TAG, mName + "[Exclusive time] / [Total time] ([Timestamp])");
+ int indentCount = 0;
+ for (Timing timing : mTimings) {
+ if (timing.isEndTiming()) {
+ indentCount--;
+ continue;
+ }
+ indentCount++;
+ if (timing.mExclusiveTime == timing.mTotalTime) {
+ Log.i(TAG, getIndentString(indentCount) + timing.mName + " " + timing.mExclusiveTime
+ + "ms (" + getRelativeTimestamp(timing.getTimestamp()) + ")");
+ } else {
+ Log.i(TAG, getIndentString(indentCount) + timing.mName + " " + timing.mExclusiveTime
+ + "ms / " + timing.mTotalTime + "ms (" + getRelativeTimestamp(
+ timing.getTimestamp()) + ")");
+ }
+ }
+ Log.i(TAG, mName + "end, " + getTotalTime() + "ms");
+ }
+
+ private void calculateTiming() {
+ ArrayDeque<Timing> arrayDeque = new ArrayDeque<>();
+ for (Timing timing : mTimings) {
+ if (timing.isStartTiming()) {
+ arrayDeque.addFirst(timing);
+ continue;
+ }
+
+ Timing timingStart = arrayDeque.removeFirst();
+ final long time = timing.mTimestamp - timingStart.mTimestamp;
+ timingStart.mExclusiveTime += time;
+ timingStart.mTotalTime += time;
+ if (!arrayDeque.isEmpty()) {
+ arrayDeque.peekFirst().mExclusiveTime -= time;
+ }
+ }
+ }
+
+ private String getIndentString(int indentCount) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < indentCount; i++) {
+ sb.append(" ");
+ }
+ return sb.toString();
+ }
+
+ private long getRelativeTimestamp(long timestamp) {
+ return timestamp - mTimings.get(0).mTimestamp;
+ }
+
+ @VisibleForTesting
+ long getTotalTime() {
+ return mTimings.get(mTimings.size() - 1).mTimestamp - mTimings.get(0).mTimestamp;
+ }
+
+ /**
+ * Gets the current latency since this object was created.
+ */
+ public long getLatencyMs() {
+ return SystemClock.elapsedRealtime() - mStartTimestampMs;
+ }
+
+ @VisibleForTesting
+ static class Timing {
+
+ private static final String END_LABEL = "END_LABEL";
+
+ /**
+ * The name of this paired timing.
+ */
+ private final String mName;
+
+ /**
+ * System uptime in millisecond.
+ */
+ private final long mTimestamp;
+
+ /**
+ * The execution time exclude inner split timings.
+ */
+ private long mExclusiveTime;
+
+ /**
+ * The execution time within a start and an end timing.
+ */
+ private long mTotalTime;
+
+ private Timing(String name) {
+ this.mName = name;
+ mTimestamp = SystemClock.elapsedRealtime();
+ mExclusiveTime = 0;
+ mTotalTime = 0;
+ }
+
+ @VisibleForTesting
+ String getName() {
+ return mName;
+ }
+
+ @VisibleForTesting
+ long getTimestamp() {
+ return mTimestamp;
+ }
+
+ @VisibleForTesting
+ long getExclusiveTime() {
+ return mExclusiveTime;
+ }
+
+ @VisibleForTesting
+ long getTotalTime() {
+ return mTotalTime;
+ }
+
+ @VisibleForTesting
+ boolean isStartTiming() {
+ return !isEndTiming();
+ }
+
+ @VisibleForTesting
+ boolean isEndTiming() {
+ return END_LABEL.equals(mName);
+ }
+ }
+
+ /**
+ * This class ensures each split timing is paired with a start and an end timing.
+ */
+ public static class ScopedTiming implements AutoCloseable {
+
+ private final TimingLogger mTimingLogger;
+
+ public ScopedTiming(TimingLogger logger, String label) {
+ mTimingLogger = logger;
+ mTimingLogger.start(label);
+ }
+
+ @Override
+ public void close() {
+ mTimingLogger.end();
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ToggleBluetoothTask.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ToggleBluetoothTask.java
new file mode 100644
index 0000000..41ac9f5
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/ToggleBluetoothTask.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/** Task for toggling Bluetooth on and back off again. */
+interface ToggleBluetoothTask {
+
+ /**
+ * Toggles the bluetooth adapter off and back on again to help improve connection reliability.
+ *
+ * @throws InterruptedException when waiting for the bluetooth adapter's state to be set has
+ * been interrupted.
+ * @throws ExecutionException when waiting for the bluetooth adapter's state to be set has
+ * failed.
+ * @throws TimeoutException when the bluetooth adapter's state fails to be set on or off.
+ */
+ void toggleBluetooth() throws InterruptedException, ExecutionException, TimeoutException;
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnection.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnection.java
new file mode 100644
index 0000000..de131e4
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnection.java
@@ -0,0 +1,781 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.gatt;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothStatusCodes;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.nearby.common.bluetooth.BluetoothConsts;
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.BluetoothGattException;
+import com.android.server.nearby.common.bluetooth.BluetoothTimeoutException;
+import com.android.server.nearby.common.bluetooth.ReservedUuids;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.ConnectionOptions;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.OperationType;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattWrapper;
+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;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.SynchronousOperation;
+
+import com.google.common.base.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.BlockingDeque;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Gatt connection to a Bluetooth device.
+ */
+public class BluetoothGattConnection implements AutoCloseable {
+
+ private static final String TAG = BluetoothGattConnection.class.getSimpleName();
+
+ @VisibleForTesting
+ static final long OPERATION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(1);
+ @VisibleForTesting
+ static final long SLOW_OPERATION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
+
+ @VisibleForTesting
+ static final int GATT_INTERNAL_ERROR = 129;
+ @VisibleForTesting
+ static final int GATT_ERROR = 133;
+
+ private final BluetoothGattWrapper mGatt;
+ private final BluetoothOperationExecutor mBluetoothOperationExecutor;
+ private final ConnectionOptions mConnectionOptions;
+
+ private volatile boolean mServicesDiscovered = false;
+
+ private volatile boolean mIsConnected = false;
+
+ private volatile int mMtu = BluetoothConsts.DEFAULT_MTU;
+
+ private final ConcurrentMap<BluetoothGattCharacteristic, ChangeObserver> mChangeObservers =
+ new ConcurrentHashMap<>();
+
+ private final List<ConnectionCloseListener> mCloseListeners = new ArrayList<>();
+
+ private long mOperationTimeoutMillis = OPERATION_TIMEOUT_MILLIS;
+
+ BluetoothGattConnection(
+ BluetoothGattWrapper gatt,
+ BluetoothOperationExecutor bluetoothOperationExecutor,
+ ConnectionOptions connectionOptions) {
+ mGatt = gatt;
+ mBluetoothOperationExecutor = bluetoothOperationExecutor;
+ mConnectionOptions = connectionOptions;
+ }
+
+ /**
+ * Set operation timeout.
+ */
+ public void setOperationTimeout(long timeoutMillis) {
+ Preconditions.checkArgument(timeoutMillis > 0, "invalid time out value");
+ mOperationTimeoutMillis = timeoutMillis;
+ }
+
+ /**
+ * Returns connected device.
+ */
+ public BluetoothDevice getDevice() {
+ return mGatt.getDevice();
+ }
+
+ public ConnectionOptions getConnectionOptions() {
+ return mConnectionOptions;
+ }
+
+ public boolean isConnected() {
+ return mIsConnected;
+ }
+
+ /**
+ * Get service.
+ */
+ public BluetoothGattService getService(UUID uuid) throws BluetoothException {
+ Log.d(TAG, String.format("Getting service %s.", uuid));
+ if (!mServicesDiscovered) {
+ discoverServices();
+ }
+ BluetoothGattService match = null;
+ for (BluetoothGattService service : mGatt.getServices()) {
+ if (service.getUuid().equals(uuid)) {
+ if (match != null) {
+ throw new BluetoothException(
+ String.format("More than one service %s found on device %s.",
+ uuid,
+ mGatt.getDevice()));
+ }
+ match = service;
+ }
+ }
+ if (match == null) {
+ throw new BluetoothException(String.format("Service %s not found on device %s.",
+ uuid,
+ mGatt.getDevice()));
+ }
+ Log.d(TAG, "Service found.");
+ return match;
+ }
+
+ /**
+ * Returns a list of all characteristics under a given service UUID.
+ */
+ private List<BluetoothGattCharacteristic> getCharacteristics(UUID serviceUuid)
+ throws BluetoothException {
+ if (!mServicesDiscovered) {
+ discoverServices();
+ }
+ ArrayList<BluetoothGattCharacteristic> characteristics = new ArrayList<>();
+ for (BluetoothGattService service : mGatt.getServices()) {
+ // Add all characteristics under this service if its service UUID matches.
+ if (service.getUuid().equals(serviceUuid)) {
+ characteristics.addAll(service.getCharacteristics());
+ }
+ }
+ return characteristics;
+ }
+
+ /**
+ * Get characteristic.
+ */
+ public BluetoothGattCharacteristic getCharacteristic(UUID serviceUuid,
+ UUID characteristicUuid) throws BluetoothException {
+ Log.d(TAG, String.format("Getting characteristic %s on service %s.", characteristicUuid,
+ serviceUuid));
+ BluetoothGattCharacteristic match = null;
+ for (BluetoothGattCharacteristic characteristic : getCharacteristics(serviceUuid)) {
+ if (characteristic.getUuid().equals(characteristicUuid)) {
+ if (match != null) {
+ throw new BluetoothException(String.format(
+ "More than one characteristic %s found on service %s on device %s.",
+ characteristicUuid,
+ serviceUuid,
+ mGatt.getDevice()));
+ }
+ match = characteristic;
+ }
+ }
+ if (match == null) {
+ throw new BluetoothException(String.format(
+ "Characteristic %s not found on service %s of device %s.",
+ characteristicUuid,
+ serviceUuid,
+ mGatt.getDevice()));
+ }
+ Log.d(TAG, "Characteristic found.");
+ return match;
+ }
+
+ /**
+ * Get descriptor.
+ */
+ public BluetoothGattDescriptor getDescriptor(UUID serviceUuid,
+ UUID characteristicUuid, UUID descriptorUuid) throws BluetoothException {
+ Log.d(TAG, String.format("Getting descriptor %s on characteristic %s on service %s.",
+ descriptorUuid, characteristicUuid, serviceUuid));
+ BluetoothGattDescriptor match = null;
+ for (BluetoothGattDescriptor descriptor :
+ getCharacteristic(serviceUuid, characteristicUuid).getDescriptors()) {
+ if (descriptor.getUuid().equals(descriptorUuid)) {
+ if (match != null) {
+ throw new BluetoothException(String.format("More than one descriptor %s found "
+ + "on characteristic %s service %s on device %s.",
+ descriptorUuid,
+ characteristicUuid,
+ serviceUuid,
+ mGatt.getDevice()));
+ }
+ match = descriptor;
+ }
+ }
+ if (match == null) {
+ throw new BluetoothException(String.format(
+ "Descriptor %s not found on characteristic %s on service %s of device %s.",
+ descriptorUuid,
+ characteristicUuid,
+ serviceUuid,
+ mGatt.getDevice()));
+ }
+ Log.d(TAG, "Descriptor found.");
+ return match;
+ }
+
+ /**
+ * Discover services.
+ */
+ public void discoverServices() throws BluetoothException {
+ mBluetoothOperationExecutor.execute(
+ new SynchronousOperation<Void>(OperationType.DISCOVER_SERVICES) {
+ @Nullable
+ @Override
+ public Void call() throws BluetoothException {
+ if (mServicesDiscovered) {
+ return null;
+ }
+ boolean forceRefresh = false;
+ try {
+ discoverServicesInternal();
+ } catch (BluetoothException e) {
+ if (!(e instanceof BluetoothGattException)) {
+ throw e;
+ }
+ int errorCode = ((BluetoothGattException) e).getGattErrorCode();
+ if (errorCode != GATT_ERROR && errorCode != GATT_INTERNAL_ERROR) {
+ throw e;
+ }
+ Log.e(TAG, e.getMessage()
+ + "\n Ignore the gatt error for post MNC apis and force "
+ + "a refresh");
+ forceRefresh = true;
+ }
+
+ forceRefreshServiceCacheIfNeeded(forceRefresh);
+
+ mServicesDiscovered = true;
+
+ return null;
+ }
+ });
+ }
+
+ private void discoverServicesInternal() throws BluetoothException {
+ Log.i(TAG, "Starting services discovery.");
+ long startTimeMillis = System.currentTimeMillis();
+ try {
+ mBluetoothOperationExecutor.execute(
+ new Operation<Void>(OperationType.DISCOVER_SERVICES_INTERNAL, mGatt) {
+ @Override
+ public void run() throws BluetoothException {
+ boolean success = mGatt.discoverServices();
+ if (!success) {
+ throw new BluetoothException(
+ "gatt.discoverServices returned false.");
+ }
+ }
+ },
+ SLOW_OPERATION_TIMEOUT_MILLIS);
+ Log.i(TAG, String.format("Services discovered successfully in %s ms.",
+ System.currentTimeMillis() - startTimeMillis));
+ } catch (BluetoothException e) {
+ if (e instanceof BluetoothGattException) {
+ throw new BluetoothGattException(String.format(
+ "Failed to discover services on device: %s.",
+ mGatt.getDevice()), ((BluetoothGattException) e).getGattErrorCode(), e);
+ } else {
+ throw new BluetoothException(String.format(
+ "Failed to discover services on device: %s.",
+ mGatt.getDevice()), e);
+ }
+ }
+ }
+
+ private boolean hasDynamicServices() {
+ BluetoothGattService gattService =
+ mGatt.getService(ReservedUuids.Services.GENERIC_ATTRIBUTE);
+ if (gattService != null) {
+ BluetoothGattCharacteristic serviceChange =
+ gattService.getCharacteristic(ReservedUuids.Characteristics.SERVICE_CHANGE);
+ if (serviceChange != null) {
+ return true;
+ }
+ }
+
+ // Check whether the server contains a self defined service dynamic characteristic.
+ gattService = mGatt.getService(BluetoothConsts.SERVICE_DYNAMIC_SERVICE);
+ if (gattService != null) {
+ BluetoothGattCharacteristic serviceChange =
+ gattService.getCharacteristic(BluetoothConsts.SERVICE_DYNAMIC_CHARACTERISTIC);
+ if (serviceChange != null) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void forceRefreshServiceCacheIfNeeded(boolean forceRefresh) throws BluetoothException {
+ if (mGatt.getDevice().getBondState() != BluetoothDevice.BOND_BONDED) {
+ // Device is not bonded, so services should not have been cached.
+ return;
+ }
+
+ if (!forceRefresh && !hasDynamicServices()) {
+ return;
+ }
+ Log.i(TAG, "Forcing a refresh of local cache of GATT services");
+ boolean success = mGatt.refresh();
+ if (!success) {
+ throw new BluetoothException("gatt.refresh returned false.");
+ }
+ discoverServicesInternal();
+ }
+
+ /**
+ * Read characteristic.
+ */
+ public byte[] readCharacteristic(UUID serviceUuid, UUID characteristicUuid)
+ throws BluetoothException {
+ return readCharacteristic(getCharacteristic(serviceUuid, characteristicUuid));
+ }
+
+ /**
+ * Read characteristic.
+ */
+ public byte[] readCharacteristic(final BluetoothGattCharacteristic characteristic)
+ throws BluetoothException {
+ try {
+ return mBluetoothOperationExecutor.executeNonnull(
+ new Operation<byte[]>(OperationType.READ_CHARACTERISTIC, mGatt,
+ characteristic) {
+ @Override
+ public void run() throws BluetoothException {
+ boolean success = mGatt.readCharacteristic(characteristic);
+ if (!success) {
+ throw new BluetoothException(
+ "gatt.readCharacteristic returned false.");
+ }
+ }
+ },
+ mOperationTimeoutMillis);
+ } catch (BluetoothException e) {
+ throw new BluetoothException(String.format(
+ "Failed to read %s on device %s.",
+ BluetoothGattUtils.toString(characteristic),
+ mGatt.getDevice()), e);
+ }
+ }
+
+ /**
+ * Writes Characteristic.
+ */
+ public void writeCharacteristic(UUID serviceUuid, UUID characteristicUuid, byte[] value)
+ throws BluetoothException {
+ writeCharacteristic(getCharacteristic(serviceUuid, characteristicUuid), value);
+ }
+
+ /**
+ * Writes Characteristic.
+ */
+ public void writeCharacteristic(final BluetoothGattCharacteristic characteristic,
+ final byte[] value) throws BluetoothException {
+ Log.d(TAG, String.format("Writing %d bytes on %s on device %s.",
+ value.length,
+ BluetoothGattUtils.toString(characteristic),
+ mGatt.getDevice()));
+ if ((characteristic.getProperties() & (BluetoothGattCharacteristic.PROPERTY_WRITE
+ | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)) == 0) {
+ throw new BluetoothException(String.format("%s is not writable!", characteristic));
+ }
+ try {
+ mBluetoothOperationExecutor.execute(
+ new Operation<Void>(OperationType.WRITE_CHARACTERISTIC, mGatt, characteristic) {
+ @Override
+ public void run() throws BluetoothException {
+ int writeCharacteristicResponseCode = mGatt.writeCharacteristic(
+ characteristic, value, characteristic.getWriteType());
+ if (writeCharacteristicResponseCode != BluetoothStatusCodes.SUCCESS) {
+ throw new BluetoothException(
+ "gatt.writeCharacteristic returned "
+ + writeCharacteristicResponseCode);
+ }
+ }
+ },
+ mOperationTimeoutMillis);
+ } catch (BluetoothException e) {
+ throw new BluetoothException(String.format(
+ "Failed to write %s on device %s.",
+ BluetoothGattUtils.toString(characteristic),
+ mGatt.getDevice()), e);
+ }
+ Log.d(TAG, "Writing characteristic done.");
+ }
+
+ /**
+ * Reads descriptor.
+ */
+ public byte[] readDescriptor(UUID serviceUuid, UUID characteristicUuid, UUID descriptorUuid)
+ throws BluetoothException {
+ return readDescriptor(getDescriptor(serviceUuid, characteristicUuid, descriptorUuid));
+ }
+
+ /**
+ * Reads descriptor.
+ */
+ public byte[] readDescriptor(final BluetoothGattDescriptor descriptor)
+ throws BluetoothException {
+ try {
+ return mBluetoothOperationExecutor.executeNonnull(
+ new Operation<byte[]>(OperationType.READ_DESCRIPTOR, mGatt, descriptor) {
+ @Override
+ public void run() throws BluetoothException {
+ boolean success = mGatt.readDescriptor(descriptor);
+ if (!success) {
+ throw new BluetoothException("gatt.readDescriptor returned false.");
+ }
+ }
+ },
+ mOperationTimeoutMillis);
+ } catch (BluetoothException e) {
+ throw new BluetoothException(String.format(
+ "Failed to read %s on %s on device %s.",
+ descriptor.getUuid(),
+ BluetoothGattUtils.toString(descriptor),
+ mGatt.getDevice()), e);
+ }
+ }
+
+ /**
+ * Writes descriptor.
+ */
+ public void writeDescriptor(UUID serviceUuid, UUID characteristicUuid, UUID descriptorUuid,
+ byte[] value) throws BluetoothException {
+ writeDescriptor(getDescriptor(serviceUuid, characteristicUuid, descriptorUuid), value);
+ }
+
+ /**
+ * Writes descriptor.
+ */
+ public void writeDescriptor(final BluetoothGattDescriptor descriptor, final byte[] value)
+ throws BluetoothException {
+ Log.d(TAG, String.format(
+ "Writing %d bytes on %s on device %s.",
+ value.length,
+ BluetoothGattUtils.toString(descriptor),
+ mGatt.getDevice()));
+ long startTimeMillis = System.currentTimeMillis();
+ try {
+ mBluetoothOperationExecutor.execute(
+ new Operation<Void>(OperationType.WRITE_DESCRIPTOR, mGatt, descriptor) {
+ @Override
+ public void run() throws BluetoothException {
+ int writeDescriptorResponseCode = mGatt.writeDescriptor(descriptor,
+ value);
+ if (writeDescriptorResponseCode != BluetoothStatusCodes.SUCCESS) {
+ throw new BluetoothException(
+ "gatt.writeDescriptor returned "
+ + writeDescriptorResponseCode);
+ }
+ }
+ },
+ mOperationTimeoutMillis);
+ Log.d(TAG, String.format("Writing descriptor done in %s ms.",
+ System.currentTimeMillis() - startTimeMillis));
+ } catch (BluetoothException e) {
+ throw new BluetoothException(String.format(
+ "Failed to write %s on device %s.",
+ BluetoothGattUtils.toString(descriptor),
+ mGatt.getDevice()), e);
+ }
+ }
+
+ /**
+ * Reads remote Rssi.
+ */
+ public int readRemoteRssi() throws BluetoothException {
+ try {
+ return mBluetoothOperationExecutor.executeNonnull(
+ new Operation<Integer>(OperationType.READ_RSSI, mGatt) {
+ @Override
+ public void run() throws BluetoothException {
+ boolean success = mGatt.readRemoteRssi();
+ if (!success) {
+ throw new BluetoothException("gatt.readRemoteRssi returned false.");
+ }
+ }
+ },
+ mOperationTimeoutMillis);
+ } catch (BluetoothException e) {
+ throw new BluetoothException(
+ String.format("Failed to read rssi on device %s.", mGatt.getDevice()), e);
+ }
+ }
+
+ public int getMtu() {
+ return mMtu;
+ }
+
+ /**
+ * Get max data packet size.
+ */
+ public int getMaxDataPacketSize() {
+ // Per BT specs (3.2.9), only MTU - 3 bytes can be used to transmit data
+ return mMtu - 3;
+ }
+
+ /** Set notification enabled or disabled. */
+ @VisibleForTesting
+ public void setNotificationEnabled(BluetoothGattCharacteristic characteristic, boolean enabled)
+ throws BluetoothException {
+ boolean isIndication;
+ int properties = characteristic.getProperties();
+ if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
+ isIndication = false;
+ } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) {
+ isIndication = true;
+ } else {
+ throw new BluetoothException(String.format(
+ "%s on device %s supports neither notifications nor indications.",
+ BluetoothGattUtils.toString(characteristic),
+ mGatt.getDevice()));
+ }
+ BluetoothGattDescriptor clientConfigDescriptor =
+ characteristic.getDescriptor(
+ ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION);
+ if (clientConfigDescriptor == null) {
+ throw new BluetoothException(String.format(
+ "%s on device %s is missing client config descriptor.",
+ BluetoothGattUtils.toString(characteristic),
+ mGatt.getDevice()));
+ }
+ long startTime = System.currentTimeMillis();
+ Log.d(TAG, String.format("%s %s on characteristic %s.", enabled ? "Enabling" : "Disabling",
+ isIndication ? "indication" : "notification", characteristic.getUuid()));
+
+ if (enabled) {
+ mGatt.setCharacteristicNotification(characteristic, enabled);
+ }
+ writeDescriptor(clientConfigDescriptor,
+ enabled
+ ? (isIndication
+ ? BluetoothGattDescriptor.ENABLE_INDICATION_VALUE :
+ BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
+ : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
+ if (!enabled) {
+ mGatt.setCharacteristicNotification(characteristic, enabled);
+ }
+
+ Log.d(TAG, String.format("Done in %d ms.", System.currentTimeMillis() - startTime));
+ }
+
+ /**
+ * Enables notification.
+ */
+ public ChangeObserver enableNotification(UUID serviceUuid, UUID characteristicUuid)
+ throws BluetoothException {
+ return enableNotification(getCharacteristic(serviceUuid, characteristicUuid));
+ }
+
+ /**
+ * Enables notification.
+ */
+ public ChangeObserver enableNotification(final BluetoothGattCharacteristic characteristic)
+ throws BluetoothException {
+ return mBluetoothOperationExecutor.executeNonnull(
+ new SynchronousOperation<ChangeObserver>(
+ OperationType.NOTIFICATION_CHANGE,
+ characteristic) {
+ @Override
+ public ChangeObserver call() throws BluetoothException {
+ ChangeObserver changeObserver = new ChangeObserver();
+ mChangeObservers.put(characteristic, changeObserver);
+ setNotificationEnabled(characteristic, true);
+ return changeObserver;
+ }
+ });
+ }
+
+ /**
+ * Disables notification.
+ */
+ public void disableNotification(UUID serviceUuid, UUID characteristicUuid)
+ throws BluetoothException {
+ disableNotification(getCharacteristic(serviceUuid, characteristicUuid));
+ }
+
+ /**
+ * Disables notification.
+ */
+ public void disableNotification(final BluetoothGattCharacteristic characteristic)
+ throws BluetoothException {
+ mBluetoothOperationExecutor.execute(
+ new SynchronousOperation<Void>(
+ OperationType.NOTIFICATION_CHANGE,
+ characteristic) {
+ @Nullable
+ @Override
+ public Void call() throws BluetoothException {
+ setNotificationEnabled(characteristic, false);
+ mChangeObservers.remove(characteristic);
+ return null;
+ }
+ });
+ }
+
+ /**
+ * Adds a close listener.
+ */
+ public void addCloseListener(ConnectionCloseListener listener) {
+ mCloseListeners.add(listener);
+ if (!mIsConnected) {
+ listener.onClose();
+ return;
+ }
+ }
+
+ /**
+ * Removes a close listener.
+ */
+ public void removeCloseListener(ConnectionCloseListener listener) {
+ mCloseListeners.remove(listener);
+ }
+
+ /** onCharacteristicChanged callback. */
+ public void onCharacteristicChanged(BluetoothGattCharacteristic characteristic, byte[] value) {
+ ChangeObserver observer = mChangeObservers.get(characteristic);
+ if (observer == null) {
+ return;
+ }
+ observer.onValueChange(value);
+ }
+
+ @Override
+ public void close() throws BluetoothException {
+ Log.d(TAG, "close");
+ try {
+ if (!mIsConnected) {
+ // Don't call disconnect on a closed connection, since Android framework won't
+ // provide any feedback.
+ return;
+ }
+ mBluetoothOperationExecutor.execute(
+ new Operation<Void>(OperationType.DISCONNECT, mGatt.getDevice()) {
+ @Override
+ public void run() throws BluetoothException {
+ mGatt.disconnect();
+ }
+ }, mOperationTimeoutMillis);
+ } finally {
+ mGatt.close();
+ }
+ }
+
+ /** onConnected callback. */
+ public void onConnected() {
+ Log.d(TAG, "onConnected");
+ mIsConnected = true;
+ }
+
+ /** onClosed callback. */
+ public void onClosed() {
+ Log.d(TAG, "onClosed");
+ if (!mIsConnected) {
+ return;
+ }
+ mIsConnected = false;
+ for (ConnectionCloseListener listener : mCloseListeners) {
+ listener.onClose();
+ }
+ mGatt.close();
+ }
+
+ /**
+ * Observer to wait or be called back when value change.
+ */
+ public static class ChangeObserver {
+
+ private final BlockingDeque<byte[]> mValues = new LinkedBlockingDeque<byte[]>();
+
+ @GuardedBy("mValues")
+ @Nullable
+ private volatile CharacteristicChangeListener mListener;
+
+ /**
+ * Set listener.
+ */
+ public void setListener(@Nullable CharacteristicChangeListener listener) {
+ synchronized (mValues) {
+ mListener = listener;
+ if (listener != null) {
+ byte[] value;
+ while ((value = mValues.poll()) != null) {
+ listener.onValueChange(value);
+ }
+ }
+ }
+ }
+
+ /**
+ * onValueChange callback.
+ */
+ public void onValueChange(byte[] newValue) {
+ synchronized (mValues) {
+ CharacteristicChangeListener listener = mListener;
+ if (listener == null) {
+ mValues.add(newValue);
+ } else {
+ listener.onValueChange(newValue);
+ }
+ }
+ }
+
+ /**
+ * Waits for update for a given time.
+ */
+ public byte[] waitForUpdate(long timeoutMillis) throws BluetoothException {
+ byte[] result;
+ try {
+ result = mValues.poll(timeoutMillis, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new BluetoothException("Operation interrupted.");
+ }
+ if (result == null) {
+ throw new BluetoothTimeoutException(
+ String.format("Operation timed out after %dms", timeoutMillis));
+ }
+ return result;
+ }
+ }
+
+ /**
+ * Listener for characteristic data changes over notifications or indications.
+ */
+ public interface CharacteristicChangeListener {
+
+ /**
+ * onValueChange callback.
+ */
+ void onValueChange(byte[] newValue);
+ }
+
+ /**
+ * Listener for connection close events.
+ */
+ public interface ConnectionCloseListener {
+
+ /**
+ * onClose callback.
+ */
+ void onClose();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelper.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelper.java
new file mode 100644
index 0000000..18a9f5f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelper.java
@@ -0,0 +1,690 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.gatt;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanSettings;
+import android.content.Context;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+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.BluetoothGattCallback;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattWrapper;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.BluetoothLeScanner;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.ScanCallback;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.ScanResult;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
+
+import com.google.common.base.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Wrapper of {@link BluetoothGattWrapper} that provides blocking methods, errors and timeout
+ * handling.
+ */
+@SuppressWarnings("Guava") // java.util.Optional is not available until API 24
+public class BluetoothGattHelper {
+
+ private static final String TAG = BluetoothGattHelper.class.getSimpleName();
+
+ @VisibleForTesting
+ static final long LOW_LATENCY_SCAN_MILLIS = TimeUnit.SECONDS.toMillis(5);
+ private static final long POLL_INTERVAL_MILLIS = 5L /* milliseconds */;
+
+ /**
+ * BT operation types that can be in flight.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ OperationType.SCAN,
+ OperationType.CONNECT,
+ OperationType.DISCOVER_SERVICES,
+ OperationType.DISCOVER_SERVICES_INTERNAL,
+ OperationType.NOTIFICATION_CHANGE,
+ OperationType.READ_CHARACTERISTIC,
+ OperationType.WRITE_CHARACTERISTIC,
+ OperationType.READ_DESCRIPTOR,
+ OperationType.WRITE_DESCRIPTOR,
+ OperationType.READ_RSSI,
+ OperationType.WRITE_RELIABLE,
+ OperationType.CHANGE_MTU,
+ OperationType.DISCONNECT,
+ })
+ public @interface OperationType {
+ int SCAN = 0;
+ int CONNECT = 1;
+ int DISCOVER_SERVICES = 2;
+ int DISCOVER_SERVICES_INTERNAL = 3;
+ int NOTIFICATION_CHANGE = 4;
+ int READ_CHARACTERISTIC = 5;
+ int WRITE_CHARACTERISTIC = 6;
+ int READ_DESCRIPTOR = 7;
+ int WRITE_DESCRIPTOR = 8;
+ int READ_RSSI = 9;
+ int WRITE_RELIABLE = 10;
+ int CHANGE_MTU = 11;
+ int DISCONNECT = 12;
+ }
+
+ @VisibleForTesting
+ final ScanCallback mScanCallback = new InternalScanCallback();
+ @VisibleForTesting
+ final BluetoothGattCallback mBluetoothGattCallback =
+ new InternalBluetoothGattCallback();
+ @VisibleForTesting
+ final ConcurrentMap<BluetoothGattWrapper, BluetoothGattConnection> mConnections =
+ new ConcurrentHashMap<>();
+
+ private final Context mApplicationContext;
+ private final BluetoothAdapter mBluetoothAdapter;
+ private final BluetoothOperationExecutor mBluetoothOperationExecutor;
+
+ @VisibleForTesting
+ BluetoothGattHelper(
+ Context applicationContext,
+ BluetoothAdapter bluetoothAdapter,
+ BluetoothOperationExecutor bluetoothOperationExecutor) {
+ mApplicationContext = applicationContext;
+ mBluetoothAdapter = bluetoothAdapter;
+ mBluetoothOperationExecutor = bluetoothOperationExecutor;
+ }
+
+ public BluetoothGattHelper(Context applicationContext, BluetoothAdapter bluetoothAdapter) {
+ this(
+ Preconditions.checkNotNull(applicationContext),
+ Preconditions.checkNotNull(bluetoothAdapter),
+ new BluetoothOperationExecutor(5));
+ }
+
+ /**
+ * Auto-connects a serice Uuid.
+ */
+ public BluetoothGattConnection autoConnect(final UUID serviceUuid) throws BluetoothException {
+ Log.d(TAG, String.format("Starting autoconnection to a device advertising service %s.",
+ serviceUuid));
+ BluetoothDevice device = null;
+ int retries = 3;
+ final BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner();
+ if (scanner == null) {
+ throw new BluetoothException("Bluetooth is disabled or LE is not supported.");
+ }
+ final ScanFilter serviceFilter = new ScanFilter.Builder()
+ .setServiceUuid(new ParcelUuid(serviceUuid))
+ .build();
+ ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder()
+ .setReportDelay(0);
+ final ScanSettings scanSettingsLowLatency = scanSettingsBuilder
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+ .build();
+ final ScanSettings scanSettingsLowPower = scanSettingsBuilder
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
+ .build();
+ while (true) {
+ long startTimeMillis = System.currentTimeMillis();
+ try {
+ Log.d(TAG, "Starting low latency scanning.");
+ device =
+ mBluetoothOperationExecutor.executeNonnull(
+ new Operation<BluetoothDevice>(OperationType.SCAN) {
+ @Override
+ public void run() throws BluetoothException {
+ scanner.startScan(Arrays.asList(serviceFilter),
+ scanSettingsLowLatency, mScanCallback);
+ }
+ }, LOW_LATENCY_SCAN_MILLIS);
+ } catch (BluetoothOperationTimeoutException e) {
+ Log.d(TAG, String.format(
+ "Cannot find a nearby device in low latency scanning after %s ms.",
+ LOW_LATENCY_SCAN_MILLIS));
+ } finally {
+ scanner.stopScan(mScanCallback);
+ }
+ if (device == null) {
+ Log.d(TAG, "Starting low power scanning.");
+ try {
+ device = mBluetoothOperationExecutor.executeNonnull(
+ new Operation<BluetoothDevice>(OperationType.SCAN) {
+ @Override
+ public void run() throws BluetoothException {
+ scanner.startScan(Arrays.asList(serviceFilter),
+ scanSettingsLowPower, mScanCallback);
+ }
+ });
+ } finally {
+ scanner.stopScan(mScanCallback);
+ }
+ }
+ Log.d(TAG, String.format("Scanning done in %d ms. Found device %s.",
+ System.currentTimeMillis() - startTimeMillis, device));
+
+ try {
+ return connect(device);
+ } catch (BluetoothException e) {
+ retries--;
+ if (retries == 0) {
+ throw e;
+ } else {
+ Log.d(TAG, String.format(
+ "Connection failed: %s. Retrying %d more times.", e, retries));
+ }
+ }
+ }
+ }
+
+ /**
+ * Connects to a device using default connection options.
+ */
+ public BluetoothGattConnection connect(BluetoothDevice bluetoothDevice)
+ throws BluetoothException {
+ return connect(bluetoothDevice, ConnectionOptions.builder().build());
+ }
+
+ /**
+ * Connects to a device using specifies connection options.
+ */
+ public BluetoothGattConnection connect(
+ BluetoothDevice bluetoothDevice, ConnectionOptions options) throws BluetoothException {
+ Log.d(TAG, String.format("Connecting to device %s.", bluetoothDevice));
+ long startTimeMillis = System.currentTimeMillis();
+
+ Operation<BluetoothGattConnection> connectOperation =
+ new Operation<BluetoothGattConnection>(OperationType.CONNECT, bluetoothDevice) {
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private boolean mIsCanceled = false;
+
+ @GuardedBy("mLock")
+ @Nullable(/* null before operation is executed */)
+ private BluetoothGattWrapper mBluetoothGatt;
+
+ @Override
+ public void run() throws BluetoothException {
+ synchronized (mLock) {
+ if (mIsCanceled) {
+ return;
+ }
+ BluetoothGattWrapper bluetoothGattWrapper;
+ Log.d(TAG, "Use LE transport");
+ bluetoothGattWrapper =
+ bluetoothDevice.connectGatt(
+ mApplicationContext,
+ options.autoConnect(),
+ mBluetoothGattCallback,
+ android.bluetooth.BluetoothDevice.TRANSPORT_LE);
+ if (bluetoothGattWrapper == null) {
+ throw new BluetoothException("connectGatt() returned null.");
+ }
+
+ try {
+ // Set connection priority without waiting for connection callback.
+ // Per code, btif_gatt_client.c, when priority is set before
+ // connection, this sets preferred connection parameters that will
+ // be used during the connection establishment.
+ Optional<Integer> connectionPriorityOption =
+ options.connectionPriority();
+ if (connectionPriorityOption.isPresent()) {
+ // requestConnectionPriority can only be called when
+ // BluetoothGatt is connected to the system BluetoothGatt
+ // service (see android/bluetooth/BluetoothGatt.java code).
+ // However, there is no callback to the app to inform when this
+ // is done. requestConnectionPriority will returns false with no
+ // side-effect before the service is connected, so we just poll
+ // here until true is returned.
+ int connectionPriority = connectionPriorityOption.get();
+ long startTimeMillis = System.currentTimeMillis();
+ while (!bluetoothGattWrapper.requestConnectionPriority(
+ connectionPriority)) {
+ if (System.currentTimeMillis() - startTimeMillis
+ > options.connectionTimeoutMillis()) {
+ throw new BluetoothException(
+ String.format(
+ Locale.US,
+ "Failed to set connectionPriority "
+ + "after %dms.",
+ options.connectionTimeoutMillis()));
+ }
+ try {
+ Thread.sleep(POLL_INTERVAL_MILLIS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new BluetoothException(
+ "connect() operation interrupted.");
+ }
+ }
+ }
+ } catch (Exception e) {
+ // Make sure to clean connection.
+ bluetoothGattWrapper.disconnect();
+ bluetoothGattWrapper.close();
+ throw e;
+ }
+
+ BluetoothGattConnection connection = new BluetoothGattConnection(
+ bluetoothGattWrapper, mBluetoothOperationExecutor, options);
+ mConnections.put(bluetoothGattWrapper, connection);
+ mBluetoothGatt = bluetoothGattWrapper;
+ }
+ }
+
+ @Override
+ public void cancel() {
+ // Clean connection if connection times out.
+ synchronized (mLock) {
+ if (mIsCanceled) {
+ return;
+ }
+ mIsCanceled = true;
+ BluetoothGattWrapper bluetoothGattWrapper = mBluetoothGatt;
+ if (bluetoothGattWrapper == null) {
+ return;
+ }
+ mConnections.remove(bluetoothGattWrapper);
+ bluetoothGattWrapper.disconnect();
+ bluetoothGattWrapper.close();
+ }
+ }
+ };
+ BluetoothGattConnection result;
+ if (options.autoConnect()) {
+ result = mBluetoothOperationExecutor.executeNonnull(connectOperation);
+ } else {
+ result =
+ mBluetoothOperationExecutor.executeNonnull(
+ connectOperation, options.connectionTimeoutMillis());
+ }
+ Log.d(TAG, String.format("Connection success in %d ms.",
+ System.currentTimeMillis() - startTimeMillis));
+ return result;
+ }
+
+ private BluetoothGattConnection getConnectionByGatt(BluetoothGattWrapper gatt)
+ throws BluetoothException {
+ BluetoothGattConnection connection = mConnections.get(gatt);
+ if (connection == null) {
+ throw new BluetoothException("Receive callback on unexpected device: " + gatt);
+ }
+ return connection;
+ }
+
+ private class InternalBluetoothGattCallback extends BluetoothGattCallback {
+
+ @Override
+ public void onConnectionStateChange(BluetoothGattWrapper gatt, int status, int newState) {
+ BluetoothGattConnection connection;
+ BluetoothDevice device = gatt.getDevice();
+ switch (newState) {
+ case BluetoothGatt.STATE_CONNECTED: {
+ connection = mConnections.get(gatt);
+ if (connection == null) {
+ Log.w(TAG, String.format(
+ "Received unexpected successful connection for dev %s! Ignoring.",
+ device));
+ break;
+ }
+
+ Operation<BluetoothGattConnection> operation =
+ new Operation<>(OperationType.CONNECT, device);
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ mConnections.remove(gatt);
+ gatt.disconnect();
+ gatt.close();
+ mBluetoothOperationExecutor.notifyCompletion(operation, status, null);
+ break;
+ }
+
+ // Process connection options
+ ConnectionOptions options = connection.getConnectionOptions();
+ Optional<Integer> mtuOption = options.mtu();
+ if (mtuOption.isPresent()) {
+ // Requesting MTU and waiting for MTU callback.
+ boolean success = gatt.requestMtu(mtuOption.get());
+ if (!success) {
+ mBluetoothOperationExecutor.notifyFailure(operation,
+ new BluetoothException(String.format(Locale.US,
+ "Failed to request MTU of %d for dev %s: "
+ + "returned false.",
+ mtuOption.get(), device)));
+ // Make sure to clean connection.
+ mConnections.remove(gatt);
+ gatt.disconnect();
+ gatt.close();
+ }
+ break;
+ }
+
+ // Connection successful
+ connection.onConnected();
+ mBluetoothOperationExecutor.notifyCompletion(operation, status, connection);
+ break;
+ }
+ case BluetoothGatt.STATE_DISCONNECTED: {
+ connection = mConnections.remove(gatt);
+ if (connection == null) {
+ Log.w(TAG, String.format("Received unexpected disconnection"
+ + " for device %s! Ignoring.", device));
+ break;
+ }
+ if (!connection.isConnected()) {
+ // This is a failed connection attempt
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ // This is weird... considering this as a failure
+ Log.w(TAG, String.format(
+ "Received a success for a failed connection "
+ + "attempt for device %s! Ignoring.", device));
+ status = BluetoothGatt.GATT_FAILURE;
+ }
+ mBluetoothOperationExecutor
+ .notifyCompletion(new Operation<BluetoothGattConnection>(
+ OperationType.CONNECT, device), status, null);
+ // Clean Gatt object in every case.
+ gatt.disconnect();
+ gatt.close();
+ break;
+ }
+ connection.onClosed();
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<>(OperationType.DISCONNECT, device), status);
+ break;
+ }
+ default:
+ Log.e(TAG, "Unexpected connection state: " + newState);
+ }
+ }
+
+ @Override
+ public void onMtuChanged(BluetoothGattWrapper gatt, int mtu, int status) {
+ BluetoothGattConnection connection = mConnections.get(gatt);
+ BluetoothDevice device = gatt.getDevice();
+ if (connection == null) {
+ Log.w(TAG, String.format(
+ "Received unexpected MTU change for device %s! Ignoring.", device));
+ return;
+ }
+ if (connection.isConnected()) {
+ // This is the callback for the deprecated BluetoothGattConnection.requestMtu.
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<>(OperationType.CHANGE_MTU, gatt), status, mtu);
+ } else {
+ // This is the callback when requesting MTU right after connecting.
+ connection.onConnected();
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<>(OperationType.CONNECT, device), status, connection);
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.w(TAG, String.format(
+ "%s responds MTU change failed, status %s.", device, status));
+ // Clean connection if it's failed.
+ mConnections.remove(gatt);
+ gatt.disconnect();
+ gatt.close();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onServicesDiscovered(BluetoothGattWrapper gatt, int status) {
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<Void>(OperationType.DISCOVER_SERVICES_INTERNAL, gatt), status);
+ }
+
+ @Override
+ public void onCharacteristicRead(BluetoothGattWrapper gatt,
+ BluetoothGattCharacteristic characteristic, int status) {
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<byte[]>(OperationType.READ_CHARACTERISTIC, gatt, characteristic),
+ status, characteristic.getValue());
+ }
+
+ @Override
+ public void onCharacteristicWrite(BluetoothGattWrapper gatt,
+ BluetoothGattCharacteristic characteristic, int status) {
+ mBluetoothOperationExecutor.notifyCompletion(new Operation<Void>(
+ OperationType.WRITE_CHARACTERISTIC, gatt, characteristic), status);
+ }
+
+ @Override
+ public void onDescriptorRead(BluetoothGattWrapper gatt, BluetoothGattDescriptor descriptor,
+ int status) {
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<byte[]>(OperationType.READ_DESCRIPTOR, gatt, descriptor), status,
+ descriptor.getValue());
+ }
+
+ @Override
+ public void onDescriptorWrite(BluetoothGattWrapper gatt, BluetoothGattDescriptor descriptor,
+ int status) {
+ Log.d(TAG, String.format("onDescriptorWrite %s, %s, %d",
+ gatt.getDevice(), descriptor.getUuid(), status));
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<Void>(OperationType.WRITE_DESCRIPTOR, gatt, descriptor), status);
+ }
+
+ @Override
+ public void onReadRemoteRssi(BluetoothGattWrapper gatt, int rssi, int status) {
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<Integer>(OperationType.READ_RSSI, gatt), status, rssi);
+ }
+
+ @Override
+ public void onReliableWriteCompleted(BluetoothGattWrapper gatt, int status) {
+ mBluetoothOperationExecutor.notifyCompletion(
+ new Operation<Void>(OperationType.WRITE_RELIABLE, gatt), status);
+ }
+
+ @Override
+ public void onCharacteristicChanged(BluetoothGattWrapper gatt,
+ BluetoothGattCharacteristic characteristic) {
+ byte[] value = characteristic.getValue();
+ if (value == null) {
+ // Value is not supposed to be null, but just to be safe...
+ value = new byte[0];
+ }
+ Log.d(TAG, String.format("Characteristic %s changed, Gatt device: %s",
+ characteristic.getUuid(), gatt.getDevice()));
+ try {
+ getConnectionByGatt(gatt).onCharacteristicChanged(characteristic, value);
+ } catch (BluetoothException e) {
+ Log.e(TAG, "Error in onCharacteristicChanged", e);
+ }
+ }
+ }
+
+ private class InternalScanCallback extends ScanCallback {
+
+ @Override
+ public void onScanFailed(int errorCode) {
+ String errorMessage;
+ switch (errorCode) {
+ case ScanCallback.SCAN_FAILED_ALREADY_STARTED:
+ errorMessage = "SCAN_FAILED_ALREADY_STARTED";
+ break;
+ case ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
+ errorMessage = "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED";
+ break;
+ case ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED:
+ errorMessage = "SCAN_FAILED_FEATURE_UNSUPPORTED";
+ break;
+ case ScanCallback.SCAN_FAILED_INTERNAL_ERROR:
+ errorMessage = "SCAN_FAILED_INTERNAL_ERROR";
+ break;
+ default:
+ errorMessage = "Unknown error code - " + errorCode;
+ }
+ mBluetoothOperationExecutor.notifyFailure(
+ new Operation<BluetoothDevice>(OperationType.SCAN),
+ new BluetoothException("Scan failed: " + errorMessage));
+ }
+
+ @Override
+ public void onScanResult(int callbackType, ScanResult result) {
+ mBluetoothOperationExecutor.notifySuccess(
+ new Operation<BluetoothDevice>(OperationType.SCAN), result.getDevice());
+ }
+ }
+
+ /**
+ * Options for {@link #connect}.
+ */
+ public static class ConnectionOptions {
+
+ private boolean mAutoConnect;
+ private long mConnectionTimeoutMillis;
+ private Optional<Integer> mConnectionPriority;
+ private Optional<Integer> mMtu;
+
+ private ConnectionOptions(boolean autoConnect, long connectionTimeoutMillis,
+ Optional<Integer> connectionPriority,
+ Optional<Integer> mtu) {
+ this.mAutoConnect = autoConnect;
+ this.mConnectionTimeoutMillis = connectionTimeoutMillis;
+ this.mConnectionPriority = connectionPriority;
+ this.mMtu = mtu;
+ }
+
+ boolean autoConnect() {
+ return mAutoConnect;
+ }
+
+ long connectionTimeoutMillis() {
+ return mConnectionTimeoutMillis;
+ }
+
+ Optional<Integer> connectionPriority() {
+ return mConnectionPriority;
+ }
+
+ Optional<Integer> mtu() {
+ return mMtu;
+ }
+
+ @Override
+ public String toString() {
+ return "ConnectionOptions{"
+ + "autoConnect=" + mAutoConnect + ", "
+ + "connectionTimeoutMillis=" + mConnectionTimeoutMillis + ", "
+ + "connectionPriority=" + mConnectionPriority + ", "
+ + "mtu=" + mMtu
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o instanceof ConnectionOptions) {
+ ConnectionOptions that = (ConnectionOptions) o;
+ return this.mAutoConnect == that.autoConnect()
+ && this.mConnectionTimeoutMillis == that.connectionTimeoutMillis()
+ && this.mConnectionPriority.equals(that.connectionPriority())
+ && this.mMtu.equals(that.mtu());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAutoConnect, mConnectionTimeoutMillis, mConnectionPriority, mMtu);
+ }
+
+ /**
+ * Creates a builder of ConnectionOptions.
+ */
+ public static Builder builder() {
+ return new ConnectionOptions.Builder()
+ .setAutoConnect(false)
+ .setConnectionTimeoutMillis(TimeUnit.SECONDS.toMillis(5));
+ }
+
+ /**
+ * Builder for {@link ConnectionOptions}.
+ */
+ public static class Builder {
+
+ private boolean mAutoConnect;
+ private long mConnectionTimeoutMillis;
+ private Optional<Integer> mConnectionPriority = Optional.empty();
+ private Optional<Integer> mMtu = Optional.empty();
+
+ /**
+ * See {@link android.bluetooth.BluetoothDevice#connectGatt}.
+ */
+ public Builder setAutoConnect(boolean autoConnect) {
+ this.mAutoConnect = autoConnect;
+ return this;
+ }
+
+ /**
+ * See {@link android.bluetooth.BluetoothGatt#requestConnectionPriority(int)}.
+ */
+ public Builder setConnectionPriority(int connectionPriority) {
+ this.mConnectionPriority = Optional.of(connectionPriority);
+ return this;
+ }
+
+ /**
+ * See {@link android.bluetooth.BluetoothGatt#requestMtu(int)}.
+ */
+ public Builder setMtu(int mtu) {
+ this.mMtu = Optional.of(mtu);
+ return this;
+ }
+
+ /**
+ * Sets the timeout for the GATT connection.
+ */
+ public Builder setConnectionTimeoutMillis(long connectionTimeoutMillis) {
+ this.mConnectionTimeoutMillis = connectionTimeoutMillis;
+ return this;
+ }
+
+ /**
+ * Builds ConnectionOptions.
+ */
+ public ConnectionOptions build() {
+ return new ConnectionOptions(mAutoConnect, mConnectionTimeoutMillis,
+ mConnectionPriority, mMtu);
+ }
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/NonnullProvider.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/NonnullProvider.java
new file mode 100644
index 0000000..16abd99
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/NonnullProvider.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.testability;
+
+/**
+ * Provider that returns non-null instances.
+ *
+ * @param <T> Type of provided instance.
+ */
+public interface NonnullProvider<T> {
+ /** Get a non-null instance. */
+ T get();
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/Testability.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/Testability.java
new file mode 100644
index 0000000..6cfdd78
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/Testability.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.testability;
+
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
+
+import javax.annotation.Nullable;
+
+/** Util class to convert from or to testable classes. */
+public class Testability {
+ /** Wraps a Bluetooth device. */
+ public static BluetoothDevice wrap(android.bluetooth.BluetoothDevice bluetoothDevice) {
+ return BluetoothDevice.wrap(bluetoothDevice);
+ }
+
+ /** Wraps a Bluetooth adapter. */
+ @Nullable
+ public static BluetoothAdapter wrap(
+ @Nullable android.bluetooth.BluetoothAdapter bluetoothAdapter) {
+ return BluetoothAdapter.wrap(bluetoothAdapter);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/TimeProvider.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/TimeProvider.java
new file mode 100644
index 0000000..a4de913
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/TimeProvider.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.testability;
+
+/** Provider of time for testability. */
+public class TimeProvider {
+ public long getTimeMillis() {
+ return System.currentTimeMillis();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/VersionProvider.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/VersionProvider.java
new file mode 100644
index 0000000..f46ea7a
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/VersionProvider.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.testability;
+
+import android.os.Build.VERSION;
+
+/**
+ * Provider of android sdk version for testability
+ */
+public class VersionProvider {
+ public int getSdkInt() {
+ return VERSION.SDK_INT;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothAdapter.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothAdapter.java
new file mode 100644
index 0000000..afa2a1b
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothAdapter.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.testability.android.bluetooth;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.BluetoothLeAdvertiser;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.BluetoothLeScanner;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Mockable wrapper of {@link android.bluetooth.BluetoothAdapter}.
+ */
+public class BluetoothAdapter {
+ /** See {@link android.bluetooth.BluetoothAdapter#ACTION_REQUEST_ENABLE}. */
+ public static final String ACTION_REQUEST_ENABLE =
+ android.bluetooth.BluetoothAdapter.ACTION_REQUEST_ENABLE;
+
+ /** See {@link android.bluetooth.BluetoothAdapter#ACTION_STATE_CHANGED}. */
+ public static final String ACTION_STATE_CHANGED =
+ android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
+
+ /** See {@link android.bluetooth.BluetoothAdapter#EXTRA_STATE}. */
+ public static final String EXTRA_STATE =
+ android.bluetooth.BluetoothAdapter.EXTRA_STATE;
+
+ /** See {@link android.bluetooth.BluetoothAdapter#STATE_OFF}. */
+ public static final int STATE_OFF =
+ android.bluetooth.BluetoothAdapter.STATE_OFF;
+
+ /** See {@link android.bluetooth.BluetoothAdapter#STATE_ON}. */
+ public static final int STATE_ON =
+ android.bluetooth.BluetoothAdapter.STATE_ON;
+
+ /** See {@link android.bluetooth.BluetoothAdapter#STATE_TURNING_OFF}. */
+ public static final int STATE_TURNING_OFF =
+ android.bluetooth.BluetoothAdapter.STATE_TURNING_OFF;
+
+ /** See {@link android.bluetooth.BluetoothAdapter#STATE_TURNING_ON}. */
+ public static final int STATE_TURNING_ON =
+ android.bluetooth.BluetoothAdapter.STATE_TURNING_ON;
+
+ private final android.bluetooth.BluetoothAdapter mWrappedBluetoothAdapter;
+
+ private BluetoothAdapter(android.bluetooth.BluetoothAdapter bluetoothAdapter) {
+ mWrappedBluetoothAdapter = bluetoothAdapter;
+ }
+
+ /** See {@link android.bluetooth.BluetoothAdapter#disable()}. */
+ public boolean disable() {
+ return mWrappedBluetoothAdapter.disable();
+ }
+
+ /** See {@link android.bluetooth.BluetoothAdapter#enable()}. */
+ public boolean enable() {
+ return mWrappedBluetoothAdapter.enable();
+ }
+
+ /** See {@link android.bluetooth.BluetoothAdapter#getBluetoothLeScanner}. */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Nullable
+ public BluetoothLeScanner getBluetoothLeScanner() {
+ return BluetoothLeScanner.wrap(mWrappedBluetoothAdapter.getBluetoothLeScanner());
+ }
+
+ /** See {@link android.bluetooth.BluetoothAdapter#getBluetoothLeAdvertiser()}. */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Nullable
+ public BluetoothLeAdvertiser getBluetoothLeAdvertiser() {
+ return BluetoothLeAdvertiser.wrap(mWrappedBluetoothAdapter.getBluetoothLeAdvertiser());
+ }
+
+ /** See {@link android.bluetooth.BluetoothAdapter#getBondedDevices()}. */
+ @Nullable
+ public Set<BluetoothDevice> getBondedDevices() {
+ Set<android.bluetooth.BluetoothDevice> bondedDevices =
+ mWrappedBluetoothAdapter.getBondedDevices();
+ if (bondedDevices == null) {
+ return null;
+ }
+ Set<BluetoothDevice> result = new HashSet<BluetoothDevice>();
+ for (android.bluetooth.BluetoothDevice device : bondedDevices) {
+ if (device == null) {
+ continue;
+ }
+ result.add(BluetoothDevice.wrap(device));
+ }
+ return Collections.unmodifiableSet(result);
+ }
+
+ /** See {@link android.bluetooth.BluetoothAdapter#getRemoteDevice(byte[])}. */
+ public BluetoothDevice getRemoteDevice(byte[] address) {
+ return BluetoothDevice.wrap(mWrappedBluetoothAdapter.getRemoteDevice(address));
+ }
+
+ /** See {@link android.bluetooth.BluetoothAdapter#getRemoteDevice(String)}. */
+ public BluetoothDevice getRemoteDevice(String address) {
+ return BluetoothDevice.wrap(mWrappedBluetoothAdapter.getRemoteDevice(address));
+ }
+
+ /** See {@link android.bluetooth.BluetoothAdapter#isEnabled()}. */
+ public boolean isEnabled() {
+ return mWrappedBluetoothAdapter.isEnabled();
+ }
+
+ /** See {@link android.bluetooth.BluetoothAdapter#isDiscovering()}. */
+ public boolean isDiscovering() {
+ return mWrappedBluetoothAdapter.isDiscovering();
+ }
+
+ /** See {@link android.bluetooth.BluetoothAdapter#startDiscovery()}. */
+ public boolean startDiscovery() {
+ return mWrappedBluetoothAdapter.startDiscovery();
+ }
+
+ /** See {@link android.bluetooth.BluetoothAdapter#cancelDiscovery()}. */
+ public boolean cancelDiscovery() {
+ return mWrappedBluetoothAdapter.cancelDiscovery();
+ }
+
+ /** See {@link android.bluetooth.BluetoothAdapter#getDefaultAdapter()}. */
+ @Nullable
+ public static BluetoothAdapter getDefaultAdapter() {
+ android.bluetooth.BluetoothAdapter adapter =
+ android.bluetooth.BluetoothAdapter.getDefaultAdapter();
+ if (adapter == null) {
+ return null;
+ }
+ return new BluetoothAdapter(adapter);
+ }
+
+ /** Wraps a Bluetooth adapter. */
+ @Nullable
+ public static BluetoothAdapter wrap(
+ @Nullable android.bluetooth.BluetoothAdapter bluetoothAdapter) {
+ if (bluetoothAdapter == null) {
+ return null;
+ }
+ return new BluetoothAdapter(bluetoothAdapter);
+ }
+
+ /** Unwraps a Bluetooth adapter. */
+ public android.bluetooth.BluetoothAdapter unwrap() {
+ return mWrappedBluetoothAdapter;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDevice.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDevice.java
new file mode 100644
index 0000000..5b45f61
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDevice.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.testability.android.bluetooth;
+
+import android.annotation.TargetApi;
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothSocket;
+import android.content.Context;
+import android.os.ParcelUuid;
+
+import java.io.IOException;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+/**
+ * Mockable wrapper of {@link android.bluetooth.BluetoothDevice}.
+ */
+@TargetApi(18)
+public class BluetoothDevice {
+ /** See {@link android.bluetooth.BluetoothDevice#BOND_BONDED}. */
+ public static final int BOND_BONDED = android.bluetooth.BluetoothDevice.BOND_BONDED;
+
+ /** See {@link android.bluetooth.BluetoothDevice#BOND_BONDING}. */
+ public static final int BOND_BONDING = android.bluetooth.BluetoothDevice.BOND_BONDING;
+
+ /** See {@link android.bluetooth.BluetoothDevice#BOND_NONE}. */
+ public static final int BOND_NONE = android.bluetooth.BluetoothDevice.BOND_NONE;
+
+ /** See {@link android.bluetooth.BluetoothDevice#ACTION_ACL_CONNECTED}. */
+ public static final String ACTION_ACL_CONNECTED =
+ android.bluetooth.BluetoothDevice.ACTION_ACL_CONNECTED;
+
+ /** See {@link android.bluetooth.BluetoothDevice#ACTION_ACL_DISCONNECT_REQUESTED}. */
+ public static final String ACTION_ACL_DISCONNECT_REQUESTED =
+ android.bluetooth.BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED;
+
+ /** See {@link android.bluetooth.BluetoothDevice#ACTION_ACL_DISCONNECTED}. */
+ public static final String ACTION_ACL_DISCONNECTED =
+ android.bluetooth.BluetoothDevice.ACTION_ACL_DISCONNECTED;
+
+ /** See {@link android.bluetooth.BluetoothDevice#ACTION_BOND_STATE_CHANGED}. */
+ public static final String ACTION_BOND_STATE_CHANGED =
+ android.bluetooth.BluetoothDevice.ACTION_BOND_STATE_CHANGED;
+
+ /** See {@link android.bluetooth.BluetoothDevice#ACTION_CLASS_CHANGED}. */
+ public static final String ACTION_CLASS_CHANGED =
+ android.bluetooth.BluetoothDevice.ACTION_CLASS_CHANGED;
+
+ /** See {@link android.bluetooth.BluetoothDevice#ACTION_FOUND}. */
+ public static final String ACTION_FOUND = android.bluetooth.BluetoothDevice.ACTION_FOUND;
+
+ /** See {@link android.bluetooth.BluetoothDevice#ACTION_NAME_CHANGED}. */
+ public static final String ACTION_NAME_CHANGED =
+ android.bluetooth.BluetoothDevice.ACTION_NAME_CHANGED;
+
+ /** See {@link android.bluetooth.BluetoothDevice#ACTION_PAIRING_REQUEST}. */
+ // API 19 only
+ public static final String ACTION_PAIRING_REQUEST =
+ "android.bluetooth.device.action.PAIRING_REQUEST";
+
+ /** See {@link android.bluetooth.BluetoothDevice#ACTION_UUID}. */
+ public static final String ACTION_UUID = android.bluetooth.BluetoothDevice.ACTION_UUID;
+
+ /** See {@link android.bluetooth.BluetoothDevice#DEVICE_TYPE_CLASSIC}. */
+ public static final int DEVICE_TYPE_CLASSIC =
+ android.bluetooth.BluetoothDevice.DEVICE_TYPE_CLASSIC;
+
+ /** See {@link android.bluetooth.BluetoothDevice#DEVICE_TYPE_DUAL}. */
+ public static final int DEVICE_TYPE_DUAL = android.bluetooth.BluetoothDevice.DEVICE_TYPE_DUAL;
+
+ /** See {@link android.bluetooth.BluetoothDevice#DEVICE_TYPE_LE}. */
+ public static final int DEVICE_TYPE_LE = android.bluetooth.BluetoothDevice.DEVICE_TYPE_LE;
+
+ /** See {@link android.bluetooth.BluetoothDevice#DEVICE_TYPE_UNKNOWN}. */
+ public static final int DEVICE_TYPE_UNKNOWN =
+ android.bluetooth.BluetoothDevice.DEVICE_TYPE_UNKNOWN;
+
+ /** See {@link android.bluetooth.BluetoothDevice#ERROR}. */
+ public static final int ERROR = android.bluetooth.BluetoothDevice.ERROR;
+
+ /** See {@link android.bluetooth.BluetoothDevice#EXTRA_BOND_STATE}. */
+ public static final String EXTRA_BOND_STATE =
+ android.bluetooth.BluetoothDevice.EXTRA_BOND_STATE;
+
+ /** See {@link android.bluetooth.BluetoothDevice#EXTRA_CLASS}. */
+ public static final String EXTRA_CLASS = android.bluetooth.BluetoothDevice.EXTRA_CLASS;
+
+ /** See {@link android.bluetooth.BluetoothDevice#EXTRA_DEVICE}. */
+ public static final String EXTRA_DEVICE = android.bluetooth.BluetoothDevice.EXTRA_DEVICE;
+
+ /** See {@link android.bluetooth.BluetoothDevice#EXTRA_NAME}. */
+ public static final String EXTRA_NAME = android.bluetooth.BluetoothDevice.EXTRA_NAME;
+
+ /** See {@link android.bluetooth.BluetoothDevice#EXTRA_PAIRING_KEY}. */
+ // API 19 only
+ public static final String EXTRA_PAIRING_KEY = "android.bluetooth.device.extra.PAIRING_KEY";
+
+ /** See {@link android.bluetooth.BluetoothDevice#EXTRA_PAIRING_VARIANT}. */
+ // API 19 only
+ public static final String EXTRA_PAIRING_VARIANT =
+ "android.bluetooth.device.extra.PAIRING_VARIANT";
+
+ /** See {@link android.bluetooth.BluetoothDevice#EXTRA_PREVIOUS_BOND_STATE}. */
+ public static final String EXTRA_PREVIOUS_BOND_STATE =
+ android.bluetooth.BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE;
+
+ /** See {@link android.bluetooth.BluetoothDevice#EXTRA_RSSI}. */
+ public static final String EXTRA_RSSI = android.bluetooth.BluetoothDevice.EXTRA_RSSI;
+
+ /** See {@link android.bluetooth.BluetoothDevice#EXTRA_UUID}. */
+ public static final String EXTRA_UUID = android.bluetooth.BluetoothDevice.EXTRA_UUID;
+
+ /** See {@link android.bluetooth.BluetoothDevice#PAIRING_VARIANT_PASSKEY_CONFIRMATION}. */
+ // API 19 only
+ public static final int PAIRING_VARIANT_PASSKEY_CONFIRMATION = 2;
+
+ /** See {@link android.bluetooth.BluetoothDevice#PAIRING_VARIANT_PIN}. */
+ // API 19 only
+ public static final int PAIRING_VARIANT_PIN = 0;
+
+ private final android.bluetooth.BluetoothDevice mWrappedBluetoothDevice;
+
+ private BluetoothDevice(android.bluetooth.BluetoothDevice bluetoothDevice) {
+ mWrappedBluetoothDevice = bluetoothDevice;
+ }
+
+ /**
+ * See {@link android.bluetooth.BluetoothDevice#connectGatt(Context, boolean,
+ * android.bluetooth.BluetoothGattCallback)}.
+ */
+ @Nullable(/* when bt service is not available */)
+ public BluetoothGattWrapper connectGatt(Context context, boolean autoConnect,
+ BluetoothGattCallback callback) {
+ android.bluetooth.BluetoothGatt gatt =
+ mWrappedBluetoothDevice.connectGatt(context, autoConnect, callback.unwrap());
+ if (gatt == null) {
+ return null;
+ }
+ return BluetoothGattWrapper.wrap(gatt);
+ }
+
+ /**
+ * See {@link android.bluetooth.BluetoothDevice#connectGatt(Context, boolean,
+ * android.bluetooth.BluetoothGattCallback, int)}.
+ */
+ @TargetApi(23)
+ @Nullable(/* when bt service is not available */)
+ public BluetoothGattWrapper connectGatt(Context context, boolean autoConnect,
+ BluetoothGattCallback callback, int transport) {
+ android.bluetooth.BluetoothGatt gatt =
+ mWrappedBluetoothDevice.connectGatt(
+ context, autoConnect, callback.unwrap(), transport);
+ if (gatt == null) {
+ return null;
+ }
+ return BluetoothGattWrapper.wrap(gatt);
+ }
+
+
+ /**
+ * See {@link android.bluetooth.BluetoothDevice#createRfcommSocketToServiceRecord(UUID)}.
+ */
+ public BluetoothSocket createRfcommSocketToServiceRecord(UUID uuid) throws IOException {
+ return mWrappedBluetoothDevice.createRfcommSocketToServiceRecord(uuid);
+ }
+
+ /**
+ * See
+ * {@link android.bluetooth.BluetoothDevice#createInsecureRfcommSocketToServiceRecord(UUID)}.
+ */
+ public BluetoothSocket createInsecureRfcommSocketToServiceRecord(UUID uuid) throws IOException {
+ return mWrappedBluetoothDevice.createInsecureRfcommSocketToServiceRecord(uuid);
+ }
+
+ /** See {@link android.bluetooth.BluetoothDevice#setPin(byte[])}. */
+ @TargetApi(19)
+ public boolean setPairingConfirmation(byte[] pin) {
+ return mWrappedBluetoothDevice.setPin(pin);
+ }
+
+ /** See {@link android.bluetooth.BluetoothDevice#setPairingConfirmation(boolean)}. */
+ public boolean setPairingConfirmation(boolean confirm) {
+ return mWrappedBluetoothDevice.setPairingConfirmation(confirm);
+ }
+
+ /** See {@link android.bluetooth.BluetoothDevice#fetchUuidsWithSdp()}. */
+ public boolean fetchUuidsWithSdp() {
+ return mWrappedBluetoothDevice.fetchUuidsWithSdp();
+ }
+
+ /** See {@link android.bluetooth.BluetoothDevice#createBond()}. */
+ public boolean createBond() {
+ return mWrappedBluetoothDevice.createBond();
+ }
+
+ /** See {@link android.bluetooth.BluetoothDevice#getUuids()}. */
+ @Nullable(/* on error */)
+ public ParcelUuid[] getUuids() {
+ return mWrappedBluetoothDevice.getUuids();
+ }
+
+ /** See {@link android.bluetooth.BluetoothDevice#getBondState()}. */
+ public int getBondState() {
+ return mWrappedBluetoothDevice.getBondState();
+ }
+
+ /** See {@link android.bluetooth.BluetoothDevice#getAddress()}. */
+ public String getAddress() {
+ return mWrappedBluetoothDevice.getAddress();
+ }
+
+ /** See {@link android.bluetooth.BluetoothDevice#getBluetoothClass()}. */
+ @Nullable(/* on error */)
+ public BluetoothClass getBluetoothClass() {
+ return mWrappedBluetoothDevice.getBluetoothClass();
+ }
+
+ /** See {@link android.bluetooth.BluetoothDevice#getType()}. */
+ public int getType() {
+ return mWrappedBluetoothDevice.getType();
+ }
+
+ /** See {@link android.bluetooth.BluetoothDevice#getName()}. */
+ @Nullable(/* on error */)
+ public String getName() {
+ return mWrappedBluetoothDevice.getName();
+ }
+
+ /** See {@link android.bluetooth.BluetoothDevice#toString()}. */
+ @Override
+ public String toString() {
+ return mWrappedBluetoothDevice.toString();
+ }
+
+ /** See {@link android.bluetooth.BluetoothDevice#hashCode()}. */
+ @Override
+ public int hashCode() {
+ return mWrappedBluetoothDevice.hashCode();
+ }
+
+ /** See {@link android.bluetooth.BluetoothDevice#equals(Object)}. */
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof BluetoothDevice)) {
+ return false;
+ }
+ return mWrappedBluetoothDevice.equals(((BluetoothDevice) o).unwrap());
+ }
+
+ /** Unwraps a Bluetooth device. */
+ public android.bluetooth.BluetoothDevice unwrap() {
+ return mWrappedBluetoothDevice;
+ }
+
+ /** Wraps a Bluetooth device. */
+ public static BluetoothDevice wrap(android.bluetooth.BluetoothDevice bluetoothDevice) {
+ return new BluetoothDevice(bluetoothDevice);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattCallback.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattCallback.java
new file mode 100644
index 0000000..d36cfa2
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattCallback.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.testability.android.bluetooth;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+
+/**
+ * Wrapper of {@link android.bluetooth.BluetoothGattCallback} that uses mockable objects.
+ */
+public abstract class BluetoothGattCallback {
+
+ private final android.bluetooth.BluetoothGattCallback mWrappedBluetoothGattCallback =
+ new InternalBluetoothGattCallback();
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattCallback#onConnectionStateChange(
+ * android.bluetooth.BluetoothGatt, int, int)}
+ */
+ public void onConnectionStateChange(BluetoothGattWrapper gatt, int status, int newState) {}
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattCallback#onServicesDiscovered(
+ * android.bluetooth.BluetoothGatt,int)}
+ */
+ public void onServicesDiscovered(BluetoothGattWrapper gatt, int status) {}
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattCallback#onCharacteristicRead(
+ * android.bluetooth.BluetoothGatt, BluetoothGattCharacteristic, int)}
+ */
+ public void onCharacteristicRead(BluetoothGattWrapper gatt, BluetoothGattCharacteristic
+ characteristic, int status) {}
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattCallback#onCharacteristicWrite(
+ * android.bluetooth.BluetoothGatt, BluetoothGattCharacteristic, int)}
+ */
+ public void onCharacteristicWrite(BluetoothGattWrapper gatt,
+ BluetoothGattCharacteristic characteristic, int status) {}
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattCallback#onDescriptorRead(
+ * android.bluetooth.BluetoothGatt, BluetoothGattDescriptor, int)}
+ */
+ public void onDescriptorRead(
+ BluetoothGattWrapper gatt, BluetoothGattDescriptor descriptor, int status) {}
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattCallback#onDescriptorWrite(
+ * android.bluetooth.BluetoothGatt, BluetoothGattDescriptor, int)}
+ */
+ public void onDescriptorWrite(BluetoothGattWrapper gatt, BluetoothGattDescriptor descriptor,
+ int status) {}
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattCallback#onReadRemoteRssi(
+ * android.bluetooth.BluetoothGatt, int, int)}
+ */
+ public void onReadRemoteRssi(BluetoothGattWrapper gatt, int rssi, int status) {}
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattCallback#onReliableWriteCompleted(
+ * android.bluetooth.BluetoothGatt, int)}
+ */
+ public void onReliableWriteCompleted(BluetoothGattWrapper gatt, int status) {}
+
+ /**
+ * See
+ * {@link android.bluetooth.BluetoothGattCallback#onMtuChanged(android.bluetooth.BluetoothGatt,
+ * int, int)}
+ */
+ public void onMtuChanged(BluetoothGattWrapper gatt, int mtu, int status) {}
+
+ /**
+ * See
+ * {@link android.bluetooth.BluetoothGattCallback#onCharacteristicChanged(
+ * android.bluetooth.BluetoothGatt, BluetoothGattCharacteristic)}
+ */
+ public void onCharacteristicChanged(BluetoothGattWrapper gatt,
+ BluetoothGattCharacteristic characteristic) {}
+
+ /** Unwraps a Bluetooth Gatt callback. */
+ public android.bluetooth.BluetoothGattCallback unwrap() {
+ return mWrappedBluetoothGattCallback;
+ }
+
+ /** Forward callback to testable instance. */
+ private class InternalBluetoothGattCallback extends android.bluetooth.BluetoothGattCallback {
+ @Override
+ public void onConnectionStateChange(android.bluetooth.BluetoothGatt gatt, int status,
+ int newState) {
+ BluetoothGattCallback.this.onConnectionStateChange(BluetoothGattWrapper.wrap(gatt),
+ status, newState);
+ }
+
+ @Override
+ public void onServicesDiscovered(android.bluetooth.BluetoothGatt gatt, int status) {
+ BluetoothGattCallback.this.onServicesDiscovered(BluetoothGattWrapper.wrap(gatt),
+ status);
+ }
+
+ @Override
+ public void onCharacteristicRead(android.bluetooth.BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic, int status) {
+ BluetoothGattCallback.this.onCharacteristicRead(
+ BluetoothGattWrapper.wrap(gatt), characteristic, status);
+ }
+
+ @Override
+ public void onCharacteristicWrite(android.bluetooth.BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic, int status) {
+ BluetoothGattCallback.this.onCharacteristicWrite(
+ BluetoothGattWrapper.wrap(gatt), characteristic, status);
+ }
+
+ @Override
+ public void onDescriptorRead(android.bluetooth.BluetoothGatt gatt,
+ BluetoothGattDescriptor descriptor, int status) {
+ BluetoothGattCallback.this.onDescriptorRead(
+ BluetoothGattWrapper.wrap(gatt), descriptor, status);
+ }
+
+ @Override
+ public void onDescriptorWrite(android.bluetooth.BluetoothGatt gatt,
+ BluetoothGattDescriptor descriptor, int status) {
+ BluetoothGattCallback.this.onDescriptorWrite(
+ BluetoothGattWrapper.wrap(gatt), descriptor, status);
+ }
+
+ @Override
+ public void onReadRemoteRssi(android.bluetooth.BluetoothGatt gatt, int rssi, int status) {
+ BluetoothGattCallback.this.onReadRemoteRssi(BluetoothGattWrapper.wrap(gatt), rssi,
+ status);
+ }
+
+ @Override
+ public void onReliableWriteCompleted(android.bluetooth.BluetoothGatt gatt, int status) {
+ BluetoothGattCallback.this.onReliableWriteCompleted(BluetoothGattWrapper.wrap(gatt),
+ status);
+ }
+
+ @Override
+ public void onMtuChanged(android.bluetooth.BluetoothGatt gatt, int mtu, int status) {
+ BluetoothGattCallback.this.onMtuChanged(BluetoothGattWrapper.wrap(gatt), mtu, status);
+ }
+
+ @Override
+ public void onCharacteristicChanged(android.bluetooth.BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic) {
+ BluetoothGattCallback.this.onCharacteristicChanged(
+ BluetoothGattWrapper.wrap(gatt), characteristic);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServer.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServer.java
new file mode 100644
index 0000000..3f6f361
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServer.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.testability.android.bluetooth;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
+
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+/**
+ * Mockable wrapper of {@link android.bluetooth.BluetoothGattServer}.
+ */
+public class BluetoothGattServer {
+
+ /** See {@link android.bluetooth.BluetoothGattServer#STATE_CONNECTED}. */
+ public static final int STATE_CONNECTED = android.bluetooth.BluetoothGattServer.STATE_CONNECTED;
+
+ /** See {@link android.bluetooth.BluetoothGattServer#STATE_DISCONNECTED}. */
+ public static final int STATE_DISCONNECTED =
+ android.bluetooth.BluetoothGattServer.STATE_DISCONNECTED;
+
+ private android.bluetooth.BluetoothGattServer mWrappedInstance;
+
+ private BluetoothGattServer(android.bluetooth.BluetoothGattServer instance) {
+ mWrappedInstance = instance;
+ }
+
+ /** Wraps a Bluetooth Gatt server. */
+ @Nullable
+ public static BluetoothGattServer wrap(
+ @Nullable android.bluetooth.BluetoothGattServer instance) {
+ if (instance == null) {
+ return null;
+ }
+ return new BluetoothGattServer(instance);
+ }
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattServer#connect(
+ * android.bluetooth.BluetoothDevice, boolean)}
+ */
+ public boolean connect(BluetoothDevice device, boolean autoConnect) {
+ return mWrappedInstance.connect(device.unwrap(), autoConnect);
+ }
+
+ /** See {@link android.bluetooth.BluetoothGattServer#addService(BluetoothGattService)}. */
+ public boolean addService(BluetoothGattService service) {
+ return mWrappedInstance.addService(service);
+ }
+
+ /** See {@link android.bluetooth.BluetoothGattServer#clearServices()}. */
+ public void clearServices() {
+ mWrappedInstance.clearServices();
+ }
+
+ /** See {@link android.bluetooth.BluetoothGattServer#close()}. */
+ public void close() {
+ mWrappedInstance.close();
+ }
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattServer#notifyCharacteristicChanged(
+ * android.bluetooth.BluetoothDevice, BluetoothGattCharacteristic, boolean)}.
+ */
+ public boolean notifyCharacteristicChanged(BluetoothDevice device,
+ BluetoothGattCharacteristic characteristic, boolean confirm) {
+ return mWrappedInstance.notifyCharacteristicChanged(
+ device.unwrap(), characteristic, confirm);
+ }
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattServer#sendResponse(
+ * android.bluetooth.BluetoothDevice, int, int, int, byte[])}.
+ */
+ public void sendResponse(BluetoothDevice device, int requestId, int status, int offset,
+ @Nullable byte[] value) {
+ mWrappedInstance.sendResponse(device.unwrap(), requestId, status, offset, value);
+ }
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattServer#cancelConnection(
+ * android.bluetooth.BluetoothDevice)}.
+ */
+ public void cancelConnection(BluetoothDevice device) {
+ mWrappedInstance.cancelConnection(device.unwrap());
+ }
+
+ /** See {@link android.bluetooth.BluetoothGattServer#getService(UUID uuid)}. */
+ public BluetoothGattService getService(UUID uuid) {
+ return mWrappedInstance.getService(uuid);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerCallback.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerCallback.java
new file mode 100644
index 0000000..875dad5
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerCallback.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.testability.android.bluetooth;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+
+/**
+ * Wrapper of {@link android.bluetooth.BluetoothGattServerCallback} that uses mockable objects.
+ */
+public abstract class BluetoothGattServerCallback {
+
+ private final android.bluetooth.BluetoothGattServerCallback mWrappedInstance =
+ new InternalBluetoothGattServerCallback();
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattServerCallback#onCharacteristicReadRequest(
+ * android.bluetooth.BluetoothDevice, int, int, BluetoothGattCharacteristic)}
+ */
+ public void onCharacteristicReadRequest(BluetoothDevice device, int requestId,
+ int offset, BluetoothGattCharacteristic characteristic) {}
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattServerCallback#onCharacteristicWriteRequest(
+ * android.bluetooth.BluetoothDevice, int, BluetoothGattCharacteristic, boolean, boolean, int,
+ * byte[])}
+ */
+ public void onCharacteristicWriteRequest(BluetoothDevice device,
+ int requestId,
+ BluetoothGattCharacteristic characteristic,
+ boolean preparedWrite,
+ boolean responseNeeded,
+ int offset,
+ byte[] value) {}
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattServerCallback#onConnectionStateChange(
+ * android.bluetooth.BluetoothDevice, int, int)}
+ */
+ public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {}
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattServerCallback#onDescriptorReadRequest(
+ * android.bluetooth.BluetoothDevice, int, int, BluetoothGattDescriptor)}
+ */
+ public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset,
+ BluetoothGattDescriptor descriptor) {}
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattServerCallback#onDescriptorWriteRequest(
+ * android.bluetooth.BluetoothDevice, int, BluetoothGattDescriptor, boolean, boolean, int,
+ * byte[])}
+ */
+ public void onDescriptorWriteRequest(BluetoothDevice device,
+ int requestId,
+ BluetoothGattDescriptor descriptor,
+ boolean preparedWrite,
+ boolean responseNeeded,
+ int offset,
+ byte[] value) {}
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattServerCallback#onExecuteWrite(
+ * android.bluetooth.BluetoothDevice, int, boolean)}
+ */
+ public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {}
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattServerCallback#onMtuChanged(
+ * android.bluetooth.BluetoothDevice, int)}
+ */
+ public void onMtuChanged(BluetoothDevice device, int mtu) {}
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattServerCallback#onNotificationSent(
+ * android.bluetooth.BluetoothDevice, int)}
+ */
+ public void onNotificationSent(BluetoothDevice device, int status) {}
+
+ /**
+ * See {@link android.bluetooth.BluetoothGattServerCallback#onServiceAdded(int,
+ * BluetoothGattService)}
+ */
+ public void onServiceAdded(int status, BluetoothGattService service) {}
+
+ /** Unwraps a Bluetooth Gatt server callback. */
+ public android.bluetooth.BluetoothGattServerCallback unwrap() {
+ return mWrappedInstance;
+ }
+
+ /** Forward callback to testable instance. */
+ private class InternalBluetoothGattServerCallback extends
+ android.bluetooth.BluetoothGattServerCallback {
+ @Override
+ public void onCharacteristicReadRequest(android.bluetooth.BluetoothDevice device,
+ int requestId, int offset, BluetoothGattCharacteristic characteristic) {
+ BluetoothGattServerCallback.this.onCharacteristicReadRequest(
+ BluetoothDevice.wrap(device), requestId, offset, characteristic);
+ }
+
+ @Override
+ public void onCharacteristicWriteRequest(android.bluetooth.BluetoothDevice device,
+ int requestId,
+ BluetoothGattCharacteristic characteristic,
+ boolean preparedWrite,
+ boolean responseNeeded,
+ int offset,
+ byte[] value) {
+ BluetoothGattServerCallback.this.onCharacteristicWriteRequest(
+ BluetoothDevice.wrap(device),
+ requestId,
+ characteristic,
+ preparedWrite,
+ responseNeeded,
+ offset,
+ value);
+ }
+
+ @Override
+ public void onConnectionStateChange(android.bluetooth.BluetoothDevice device, int status,
+ int newState) {
+ BluetoothGattServerCallback.this.onConnectionStateChange(
+ BluetoothDevice.wrap(device), status, newState);
+ }
+
+ @Override
+ public void onDescriptorReadRequest(android.bluetooth.BluetoothDevice device, int requestId,
+ int offset, BluetoothGattDescriptor descriptor) {
+ BluetoothGattServerCallback.this.onDescriptorReadRequest(BluetoothDevice.wrap(device),
+ requestId, offset, descriptor);
+ }
+
+ @Override
+ public void onDescriptorWriteRequest(android.bluetooth.BluetoothDevice device,
+ int requestId,
+ BluetoothGattDescriptor descriptor,
+ boolean preparedWrite,
+ boolean responseNeeded,
+ int offset,
+ byte[] value) {
+ BluetoothGattServerCallback.this.onDescriptorWriteRequest(BluetoothDevice.wrap(device),
+ requestId,
+ descriptor,
+ preparedWrite,
+ responseNeeded,
+ offset,
+ value);
+ }
+
+ @Override
+ public void onExecuteWrite(android.bluetooth.BluetoothDevice device, int requestId,
+ boolean execute) {
+ BluetoothGattServerCallback.this.onExecuteWrite(BluetoothDevice.wrap(device), requestId,
+ execute);
+ }
+
+ @Override
+ public void onMtuChanged(android.bluetooth.BluetoothDevice device, int mtu) {
+ BluetoothGattServerCallback.this.onMtuChanged(BluetoothDevice.wrap(device), mtu);
+ }
+
+ @Override
+ public void onNotificationSent(android.bluetooth.BluetoothDevice device, int status) {
+ BluetoothGattServerCallback.this.onNotificationSent(
+ BluetoothDevice.wrap(device), status);
+ }
+
+ @Override
+ public void onServiceAdded(int status, BluetoothGattService service) {
+ BluetoothGattServerCallback.this.onServiceAdded(status, service);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattWrapper.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattWrapper.java
new file mode 100644
index 0000000..453ee5d
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattWrapper.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.testability.android.bluetooth;
+
+import android.annotation.TargetApi;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.os.Build;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+/** Mockable wrapper of {@link android.bluetooth.BluetoothGatt}. */
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
+public class BluetoothGattWrapper {
+ private final android.bluetooth.BluetoothGatt mWrappedBluetoothGatt;
+
+ private BluetoothGattWrapper(android.bluetooth.BluetoothGatt bluetoothGatt) {
+ mWrappedBluetoothGatt = bluetoothGatt;
+ }
+
+ /** See {@link android.bluetooth.BluetoothGatt#getDevice()}. */
+ public BluetoothDevice getDevice() {
+ return BluetoothDevice.wrap(mWrappedBluetoothGatt.getDevice());
+ }
+
+ /** See {@link android.bluetooth.BluetoothGatt#getServices()}. */
+ public List<BluetoothGattService> getServices() {
+ return mWrappedBluetoothGatt.getServices();
+ }
+
+ /** See {@link android.bluetooth.BluetoothGatt#getService(UUID)}. */
+ @Nullable(/* null if service is not found */)
+ public BluetoothGattService getService(UUID uuid) {
+ return mWrappedBluetoothGatt.getService(uuid);
+ }
+
+ /** See {@link android.bluetooth.BluetoothGatt#discoverServices()}. */
+ public boolean discoverServices() {
+ return mWrappedBluetoothGatt.discoverServices();
+ }
+
+ /**
+ * Hidden method. Clears the internal cache and forces a refresh of the services from the remote
+ * device.
+ */
+ // TODO(b/201300471): remove refresh call using reflection.
+ public boolean refresh() {
+ try {
+ Method refreshMethod = android.bluetooth.BluetoothGatt.class.getMethod("refresh");
+ return (Boolean) refreshMethod.invoke(mWrappedBluetoothGatt);
+ } catch (NoSuchMethodException
+ | IllegalAccessException
+ | IllegalArgumentException
+ | InvocationTargetException e) {
+ return false;
+ }
+ }
+
+ /**
+ * See {@link android.bluetooth.BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)}.
+ */
+ public boolean readCharacteristic(BluetoothGattCharacteristic characteristic) {
+ return mWrappedBluetoothGatt.readCharacteristic(characteristic);
+ }
+
+ /**
+ * See {@link android.bluetooth.BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic,
+ * byte[], int)} .
+ */
+ public int writeCharacteristic(BluetoothGattCharacteristic characteristic, byte[] value,
+ int writeType) {
+ return mWrappedBluetoothGatt.writeCharacteristic(characteristic, value, writeType);
+ }
+
+ /** See {@link android.bluetooth.BluetoothGatt#readDescriptor(BluetoothGattDescriptor)}. */
+ public boolean readDescriptor(BluetoothGattDescriptor descriptor) {
+ return mWrappedBluetoothGatt.readDescriptor(descriptor);
+ }
+
+ /**
+ * See {@link android.bluetooth.BluetoothGatt#writeDescriptor(BluetoothGattDescriptor,
+ * byte[])}.
+ */
+ public int writeDescriptor(BluetoothGattDescriptor descriptor, byte[] value) {
+ return mWrappedBluetoothGatt.writeDescriptor(descriptor, value);
+ }
+
+ /** See {@link android.bluetooth.BluetoothGatt#readRemoteRssi()}. */
+ public boolean readRemoteRssi() {
+ return mWrappedBluetoothGatt.readRemoteRssi();
+ }
+
+ /** See {@link android.bluetooth.BluetoothGatt#requestConnectionPriority(int)}. */
+ public boolean requestConnectionPriority(int connectionPriority) {
+ return mWrappedBluetoothGatt.requestConnectionPriority(connectionPriority);
+ }
+
+ /** See {@link android.bluetooth.BluetoothGatt#requestMtu(int)}. */
+ public boolean requestMtu(int mtu) {
+ return mWrappedBluetoothGatt.requestMtu(mtu);
+ }
+
+ /** See {@link android.bluetooth.BluetoothGatt#setCharacteristicNotification}. */
+ public boolean setCharacteristicNotification(
+ BluetoothGattCharacteristic characteristic, boolean enable) {
+ return mWrappedBluetoothGatt.setCharacteristicNotification(characteristic, enable);
+ }
+
+ /** See {@link android.bluetooth.BluetoothGatt#disconnect()}. */
+ public void disconnect() {
+ mWrappedBluetoothGatt.disconnect();
+ }
+
+ /** See {@link android.bluetooth.BluetoothGatt#close()}. */
+ public void close() {
+ mWrappedBluetoothGatt.close();
+ }
+
+ /** See {@link android.bluetooth.BluetoothGatt#hashCode()}. */
+ @Override
+ public int hashCode() {
+ return mWrappedBluetoothGatt.hashCode();
+ }
+
+ /** See {@link android.bluetooth.BluetoothGatt#equals(Object)}. */
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof BluetoothGattWrapper)) {
+ return false;
+ }
+ return mWrappedBluetoothGatt.equals(((BluetoothGattWrapper) o).unwrap());
+ }
+
+ /** Unwraps a Bluetooth Gatt instance. */
+ public android.bluetooth.BluetoothGatt unwrap() {
+ return mWrappedBluetoothGatt;
+ }
+
+ /** Wraps a Bluetooth Gatt instance. */
+ public static BluetoothGattWrapper wrap(android.bluetooth.BluetoothGatt gatt) {
+ return new BluetoothGattWrapper(gatt);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeAdvertiser.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeAdvertiser.java
new file mode 100644
index 0000000..6fe4432
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeAdvertiser.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le;
+
+import android.annotation.TargetApi;
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
+import android.os.Build;
+
+import javax.annotation.Nullable;
+
+/**
+ * Mockable wrapper of {@link android.bluetooth.le.BluetoothLeAdvertiser}.
+ */
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+public class BluetoothLeAdvertiser {
+
+ private final android.bluetooth.le.BluetoothLeAdvertiser mWrappedInstance;
+
+ private BluetoothLeAdvertiser(
+ android.bluetooth.le.BluetoothLeAdvertiser bluetoothLeAdvertiser) {
+ mWrappedInstance = bluetoothLeAdvertiser;
+ }
+
+ /**
+ * See {@link android.bluetooth.le.BluetoothLeAdvertiser#startAdvertising(AdvertiseSettings,
+ * AdvertiseData, AdvertiseCallback)}.
+ */
+ public void startAdvertising(AdvertiseSettings settings, AdvertiseData advertiseData,
+ AdvertiseCallback callback) {
+ mWrappedInstance.startAdvertising(settings, advertiseData, callback);
+ }
+
+ /**
+ * See {@link android.bluetooth.le.BluetoothLeAdvertiser#startAdvertising(AdvertiseSettings,
+ * AdvertiseData, AdvertiseData, AdvertiseCallback)}.
+ */
+ public void startAdvertising(AdvertiseSettings settings, AdvertiseData advertiseData,
+ AdvertiseData scanResponse, AdvertiseCallback callback) {
+ mWrappedInstance.startAdvertising(settings, advertiseData, scanResponse, callback);
+ }
+
+ /**
+ * See {@link android.bluetooth.le.BluetoothLeAdvertiser#stopAdvertising(AdvertiseCallback)}.
+ */
+ public void stopAdvertising(AdvertiseCallback callback) {
+ mWrappedInstance.stopAdvertising(callback);
+ }
+
+ /** Wraps a Bluetooth LE advertiser. */
+ @Nullable
+ public static BluetoothLeAdvertiser wrap(
+ @Nullable android.bluetooth.le.BluetoothLeAdvertiser bluetoothLeAdvertiser) {
+ if (bluetoothLeAdvertiser == null) {
+ return null;
+ }
+ return new BluetoothLeAdvertiser(bluetoothLeAdvertiser);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScanner.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScanner.java
new file mode 100644
index 0000000..8a13abe
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScanner.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanSettings;
+import android.os.Build;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * Mockable wrapper of {@link android.bluetooth.le.BluetoothLeScanner}.
+ */
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+public class BluetoothLeScanner {
+
+ private final android.bluetooth.le.BluetoothLeScanner mWrappedBluetoothLeScanner;
+
+ private BluetoothLeScanner(android.bluetooth.le.BluetoothLeScanner bluetoothLeScanner) {
+ mWrappedBluetoothLeScanner = bluetoothLeScanner;
+ }
+
+ /**
+ * See {@link android.bluetooth.le.BluetoothLeScanner#startScan(List, ScanSettings,
+ * android.bluetooth.le.ScanCallback)}.
+ */
+ public void startScan(List<ScanFilter> filters, ScanSettings settings,
+ ScanCallback callback) {
+ mWrappedBluetoothLeScanner.startScan(filters, settings, callback.unwrap());
+ }
+
+ /**
+ * See {@link android.bluetooth.le.BluetoothLeScanner#startScan(List, ScanSettings,
+ * PendingIntent)}.
+ */
+ public void startScan(
+ List<ScanFilter> filters, ScanSettings settings, PendingIntent callbackIntent) {
+ mWrappedBluetoothLeScanner.startScan(filters, settings, callbackIntent);
+ }
+
+ /**
+ * See {@link
+ * android.bluetooth.le.BluetoothLeScanner#startScan(android.bluetooth.le.ScanCallback)}.
+ */
+ public void startScan(ScanCallback callback) {
+ mWrappedBluetoothLeScanner.startScan(callback.unwrap());
+ }
+
+ /**
+ * See
+ * {@link android.bluetooth.le.BluetoothLeScanner#stopScan(android.bluetooth.le.ScanCallback)}.
+ */
+ public void stopScan(ScanCallback callback) {
+ mWrappedBluetoothLeScanner.stopScan(callback.unwrap());
+ }
+
+ /** See {@link android.bluetooth.le.BluetoothLeScanner#stopScan(PendingIntent)}. */
+ public void stopScan(PendingIntent callbackIntent) {
+ mWrappedBluetoothLeScanner.stopScan(callbackIntent);
+ }
+
+ /** Wraps a Bluetooth LE scanner. */
+ @Nullable
+ public static BluetoothLeScanner wrap(
+ @Nullable android.bluetooth.le.BluetoothLeScanner bluetoothLeScanner) {
+ if (bluetoothLeScanner == null) {
+ return null;
+ }
+ return new BluetoothLeScanner(bluetoothLeScanner);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanCallback.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanCallback.java
new file mode 100644
index 0000000..70926a7
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanCallback.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Wrapper of {@link android.bluetooth.le.ScanCallback} that uses mockable objects.
+ */
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+public abstract class ScanCallback {
+
+ /** See {@link android.bluetooth.le.ScanCallback#SCAN_FAILED_ALREADY_STARTED} */
+ public static final int SCAN_FAILED_ALREADY_STARTED =
+ android.bluetooth.le.ScanCallback.SCAN_FAILED_ALREADY_STARTED;
+
+ /** See {@link android.bluetooth.le.ScanCallback#SCAN_FAILED_APPLICATION_REGISTRATION_FAILED} */
+ public static final int SCAN_FAILED_APPLICATION_REGISTRATION_FAILED =
+ android.bluetooth.le.ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED;
+
+ /** See {@link android.bluetooth.le.ScanCallback#SCAN_FAILED_FEATURE_UNSUPPORTED} */
+ public static final int SCAN_FAILED_FEATURE_UNSUPPORTED =
+ android.bluetooth.le.ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED;
+
+ /** See {@link android.bluetooth.le.ScanCallback#SCAN_FAILED_INTERNAL_ERROR} */
+ public static final int SCAN_FAILED_INTERNAL_ERROR =
+ android.bluetooth.le.ScanCallback.SCAN_FAILED_INTERNAL_ERROR;
+
+ private final android.bluetooth.le.ScanCallback mWrappedScanCallback =
+ new InternalScanCallback();
+
+ /**
+ * See {@link android.bluetooth.le.ScanCallback#onScanFailed(int)}
+ */
+ public void onScanFailed(int errorCode) {}
+
+ /**
+ * See
+ * {@link android.bluetooth.le.ScanCallback#onScanResult(int, android.bluetooth.le.ScanResult)}.
+ */
+ public void onScanResult(int callbackType, ScanResult result) {}
+
+ /**
+ * See {@link
+ * android.bluetooth.le.ScanCallback#onBatchScanResult(List<android.bluetooth.le.ScanResult>)}.
+ */
+ public void onBatchScanResults(List<ScanResult> results) {}
+
+ /** Unwraps scan callback. */
+ public android.bluetooth.le.ScanCallback unwrap() {
+ return mWrappedScanCallback;
+ }
+
+ /** Forward callback to testable instance. */
+ private class InternalScanCallback extends android.bluetooth.le.ScanCallback {
+ @Override
+ public void onScanFailed(int errorCode) {
+ ScanCallback.this.onScanFailed(errorCode);
+ }
+
+ @Override
+ public void onScanResult(int callbackType, android.bluetooth.le.ScanResult result) {
+ ScanCallback.this.onScanResult(callbackType, ScanResult.wrap(result));
+ }
+
+ @Override
+ public void onBatchScanResults(List<android.bluetooth.le.ScanResult> results) {
+ List<ScanResult> wrappedScanResults = new ArrayList<>();
+ for (android.bluetooth.le.ScanResult result : results) {
+ wrappedScanResults.add(ScanResult.wrap(result));
+ }
+ ScanCallback.this.onBatchScanResults(wrappedScanResults);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanResult.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanResult.java
new file mode 100644
index 0000000..1a6b7b3
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanResult.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le;
+
+import android.annotation.TargetApi;
+import android.bluetooth.le.ScanRecord;
+import android.os.Build;
+
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
+
+import javax.annotation.Nullable;
+
+/**
+ * Mockable wrapper of {@link android.bluetooth.le.ScanResult}.
+ */
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+public class ScanResult {
+
+ private final android.bluetooth.le.ScanResult mWrappedScanResult;
+
+ private ScanResult(android.bluetooth.le.ScanResult scanResult) {
+ mWrappedScanResult = scanResult;
+ }
+
+ /** See {@link android.bluetooth.le.ScanResult#getScanRecord()}. */
+ @Nullable
+ public ScanRecord getScanRecord() {
+ return mWrappedScanResult.getScanRecord();
+ }
+
+ /** See {@link android.bluetooth.le.ScanResult#getRssi()}. */
+ public int getRssi() {
+ return mWrappedScanResult.getRssi();
+ }
+
+ /** See {@link android.bluetooth.le.ScanResult#getTimestampNanos()}. */
+ public long getTimestampNanos() {
+ return mWrappedScanResult.getTimestampNanos();
+ }
+
+ /** See {@link android.bluetooth.le.ScanResult#getDevice()}. */
+ public BluetoothDevice getDevice() {
+ return BluetoothDevice.wrap(mWrappedScanResult.getDevice());
+ }
+
+ /** Creates a wrapper of scan result. */
+ public static ScanResult wrap(android.bluetooth.le.ScanResult scanResult) {
+ return new ScanResult(scanResult);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtils.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtils.java
new file mode 100644
index 0000000..bb51920
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtils.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.util;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utils for Gatt profile.
+ */
+public class BluetoothGattUtils {
+
+ /**
+ * Returns a string message for a BluetoothGatt status codes.
+ */
+ public static String getMessageForStatusCode(int statusCode) {
+ switch (statusCode) {
+ case BluetoothGatt.GATT_SUCCESS:
+ return "GATT_SUCCESS";
+ case BluetoothGatt.GATT_FAILURE:
+ return "GATT_FAILURE";
+ case BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION:
+ return "GATT_INSUFFICIENT_AUTHENTICATION";
+ case BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION:
+ return "GATT_INSUFFICIENT_AUTHORIZATION";
+ case BluetoothGatt.GATT_INSUFFICIENT_ENCRYPTION:
+ return "GATT_INSUFFICIENT_ENCRYPTION";
+ case BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH:
+ return "GATT_INVALID_ATTRIBUTE_LENGTH";
+ case BluetoothGatt.GATT_INVALID_OFFSET:
+ return "GATT_INVALID_OFFSET";
+ case BluetoothGatt.GATT_READ_NOT_PERMITTED:
+ return "GATT_READ_NOT_PERMITTED";
+ case BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED:
+ return "GATT_REQUEST_NOT_SUPPORTED";
+ case BluetoothGatt.GATT_WRITE_NOT_PERMITTED:
+ return "GATT_WRITE_NOT_PERMITTED";
+ case BluetoothGatt.GATT_CONNECTION_CONGESTED:
+ return "GATT_CONNECTION_CONGESTED";
+ default:
+ return "Unknown error code";
+ }
+ }
+
+ /** Creates a user-readable string from a {@link BluetoothGattDescriptor}. */
+ public static String toString(@Nullable BluetoothGattDescriptor descriptor) {
+ if (descriptor == null) {
+ return "null descriptor";
+ }
+ return String.format("descriptor %s on %s",
+ descriptor.getUuid(),
+ toString(descriptor.getCharacteristic()));
+ }
+
+ /** Creates a user-readable string from a {@link BluetoothGattCharacteristic}. */
+ public static String toString(@Nullable BluetoothGattCharacteristic characteristic) {
+ if (characteristic == null) {
+ return "null characteristic";
+ }
+ return String.format("characteristic %s on %s",
+ characteristic.getUuid(),
+ toString(characteristic.getService()));
+ }
+
+ /** Creates a user-readable string from a {@link BluetoothGattService}. */
+ public static String toString(@Nullable BluetoothGattService service) {
+ if (service == null) {
+ return "null service";
+ }
+ return String.format("service %s", service.getUuid());
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/util/BluetoothOperationExecutor.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/util/BluetoothOperationExecutor.java
new file mode 100644
index 0000000..fecf483
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/util/BluetoothOperationExecutor.java
@@ -0,0 +1,548 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.util;
+
+import android.bluetooth.BluetoothGatt;
+import android.util.Log;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.BluetoothGattException;
+import com.android.server.nearby.common.bluetooth.testability.NonnullProvider;
+import com.android.server.nearby.common.bluetooth.testability.TimeProvider;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.annotation.Nullable;
+
+/**
+ * Scheduler to coordinate parallel bluetooth operations.
+ */
+public class BluetoothOperationExecutor {
+
+ private static final String TAG = BluetoothOperationExecutor.class.getSimpleName();
+
+ /**
+ * Special value to indicate that the result is null (since {@link BlockingQueue} doesn't allow
+ * null elements).
+ */
+ private static final Object NULL_RESULT = new Object();
+
+ /**
+ * Special value to indicate that there should be no timeout on the operation.
+ */
+ private static final long NO_TIMEOUT = -1;
+
+ private final NonnullProvider<BlockingQueue<Object>> mBlockingQueueProvider;
+ private final TimeProvider mTimeProvider;
+ @VisibleForTesting
+ final Map<Operation<?>, Queue<Object>> mOperationResultQueues = new HashMap<>();
+ private final Semaphore mOperationSemaphore;
+
+ /**
+ * New instance that limits concurrent operations to maxConcurrentOperations.
+ */
+ public BluetoothOperationExecutor(int maxConcurrentOperations) {
+ this(
+ new Semaphore(maxConcurrentOperations, true),
+ new TimeProvider(),
+ new NonnullProvider<BlockingQueue<Object>>() {
+ @Override
+ public BlockingQueue<Object> get() {
+ return new LinkedBlockingDeque<Object>();
+ }
+ });
+ }
+
+ /**
+ * Constructor for unit tests.
+ */
+ @VisibleForTesting
+ BluetoothOperationExecutor(Semaphore operationSemaphore,
+ TimeProvider timeProvider,
+ NonnullProvider<BlockingQueue<Object>> blockingQueueProvider) {
+ mOperationSemaphore = operationSemaphore;
+ mTimeProvider = timeProvider;
+ mBlockingQueueProvider = blockingQueueProvider;
+ }
+
+ /**
+ * Executes the operation and waits for its completion.
+ */
+ @Nullable
+ public <T> T execute(Operation<T> operation) throws BluetoothException {
+ return getResult(schedule(operation));
+ }
+
+ /**
+ * Executes the operation and waits for its completion and returns a non-null result.
+ */
+ public <T> T executeNonnull(Operation<T> operation) throws BluetoothException {
+ T result = getResult(schedule(operation));
+ if (result == null) {
+ throw new BluetoothException(
+ String.format(Locale.US, "Operation %s returned a null result.", operation));
+ }
+ return result;
+ }
+
+ /**
+ * Executes the operation and waits for its completion with a timeout.
+ */
+ @Nullable
+ public <T> T execute(Operation<T> bluetoothOperation, long timeoutMillis)
+ throws BluetoothException, BluetoothOperationTimeoutException {
+ return getResult(schedule(bluetoothOperation), timeoutMillis);
+ }
+
+ /**
+ * Executes the operation and waits for its completion with a timeout and returns a non-null
+ * result.
+ */
+ public <T> T executeNonnull(Operation<T> bluetoothOperation, long timeoutMillis)
+ throws BluetoothException {
+ T result = getResult(schedule(bluetoothOperation), timeoutMillis);
+ if (result == null) {
+ throw new BluetoothException(
+ String.format(Locale.US, "Operation %s returned a null result.",
+ bluetoothOperation));
+ }
+ return result;
+ }
+
+ /**
+ * Schedules an operation and returns a {@link Future} that waits on operation completion and
+ * gets its result.
+ */
+ public <T> Future<T> schedule(Operation<T> bluetoothOperation) {
+ BlockingQueue<Object> resultQueue = mBlockingQueueProvider.get();
+ mOperationResultQueues.put(bluetoothOperation, resultQueue);
+
+ boolean semaphoreAcquired = mOperationSemaphore.tryAcquire();
+ Log.d(TAG, String.format(Locale.US,
+ "Scheduling operation %s; %d permits available; Semaphore acquired: %b",
+ bluetoothOperation,
+ mOperationSemaphore.availablePermits(),
+ semaphoreAcquired));
+
+ if (semaphoreAcquired) {
+ bluetoothOperation.execute(this);
+ }
+ return new BluetoothOperationFuture<T>(resultQueue, bluetoothOperation, semaphoreAcquired);
+ }
+
+ /**
+ * Notifies that this operation has completed with success.
+ */
+ public void notifySuccess(Operation<Void> bluetoothOperation) {
+ postResult(bluetoothOperation, null);
+ }
+
+ /**
+ * Notifies that this operation has completed with success and with a result.
+ */
+ public <T> void notifySuccess(Operation<T> bluetoothOperation, T result) {
+ postResult(bluetoothOperation, result);
+ }
+
+ /**
+ * Notifies that this operation has completed with the given BluetoothGatt status code (which
+ * may indicate success or failure).
+ */
+ public void notifyCompletion(Operation<Void> bluetoothOperation, int status) {
+ notifyCompletion(bluetoothOperation, status, null);
+ }
+
+ /**
+ * Notifies that this operation has completed with the given BluetoothGatt status code (which
+ * may indicate success or failure) and with a result.
+ */
+ public <T> void notifyCompletion(Operation<T> bluetoothOperation, int status,
+ @Nullable T result) {
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ notifyFailure(bluetoothOperation, new BluetoothGattException(
+ String.format(Locale.US,
+ "Operation %s failed: %d - %s.", bluetoothOperation, status,
+ BluetoothGattUtils.getMessageForStatusCode(status)),
+ status));
+ return;
+ }
+ postResult(bluetoothOperation, result);
+ }
+
+ /**
+ * Notifies that this operation has completed with failure.
+ */
+ public void notifyFailure(Operation<?> bluetoothOperation, BluetoothException exception) {
+ postResult(bluetoothOperation, exception);
+ }
+
+ private void postResult(Operation<?> bluetoothOperation, @Nullable Object result) {
+ Queue<Object> resultQueue = mOperationResultQueues.get(bluetoothOperation);
+ if (resultQueue == null) {
+ Log.e(TAG, String.format(Locale.US,
+ "Receive completion for unexpected operation: %s.", bluetoothOperation));
+ return;
+ }
+ resultQueue.add(result == null ? NULL_RESULT : result);
+ mOperationResultQueues.remove(bluetoothOperation);
+ mOperationSemaphore.release();
+ Log.d(TAG, String.format(Locale.US,
+ "Released semaphore for operation %s. There are %d permits left",
+ bluetoothOperation, mOperationSemaphore.availablePermits()));
+ }
+
+ /**
+ * Waits for all future on the list to complete, ignoring the results.
+ */
+ public <T> void waitFor(List<Future<T>> futures) throws BluetoothException {
+ for (Future<T> future : futures) {
+ if (future == null) {
+ continue;
+ }
+ getResult(future);
+ }
+ }
+
+ /**
+ * Waits with timeout for all future on the list to complete, ignoring the results.
+ */
+ public <T> void waitFor(List<Future<T>> futures, long timeoutMillis)
+ throws BluetoothException {
+ long startTime = mTimeProvider.getTimeMillis();
+ for (Future<T> future : futures) {
+ if (future == null) {
+ continue;
+ }
+ getResult(future,
+ timeoutMillis - (mTimeProvider.getTimeMillis() - startTime));
+ }
+ }
+
+ /**
+ * Waits for a future to complete and returns the result.
+ */
+ @Nullable
+ public static <T> T getResult(Future<T> future) throws BluetoothException {
+ return getResultInternal(future, NO_TIMEOUT);
+ }
+
+ /**
+ * Waits for a future to complete and returns the result with timeout.
+ */
+ @Nullable
+ public static <T> T getResult(Future<T> future, long timeoutMillis) throws BluetoothException {
+ return getResultInternal(future, Math.max(0, timeoutMillis));
+ }
+
+ @Nullable
+ private static <T> T getResultInternal(Future<T> future, long timeoutMillis)
+ throws BluetoothException {
+ try {
+ if (timeoutMillis == NO_TIMEOUT) {
+ return future.get();
+ } else {
+ return future.get(timeoutMillis, TimeUnit.MILLISECONDS);
+ }
+ } catch (InterruptedException e) {
+ try {
+ boolean cancelSuccess = future.cancel(true);
+ if (!cancelSuccess && future.isDone()) {
+ // Operation has succeeded before we send cancel to it.
+ return getResultInternal(future, NO_TIMEOUT);
+ }
+ } finally {
+ // Re-interrupt the thread last since we're recursively calling getResultInternal.
+ // We know the future is done, so there's no need to be interrupted while we call.
+ Thread.currentThread().interrupt();
+ }
+ throw new BluetoothException("Wait interrupted");
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof BluetoothException) {
+ throw (BluetoothException) cause;
+ }
+ throw new RuntimeException(e);
+ } catch (TimeoutException e) {
+ boolean cancelSuccess = future.cancel(true);
+ if (!cancelSuccess && future.isDone()) {
+ // Operation has succeeded before we send cancel to it.
+ return getResultInternal(future, NO_TIMEOUT);
+ }
+ throw new BluetoothOperationTimeoutException(
+ String.format(Locale.US, "Wait timed out after %s ms.", timeoutMillis), e);
+ }
+ }
+
+ /**
+ * Asynchronous bluetooth operation to schedule.
+ *
+ * <p>An instance that doesn't implemented run() can be used to notify operation result.
+ *
+ * @param <T> Type of provided instance.
+ */
+ public static class Operation<T> {
+
+ private Object[] mElements;
+
+ public Operation(Object... elements) {
+ mElements = elements;
+ }
+
+ /**
+ * Executes operation using executor.
+ */
+ public void execute(BluetoothOperationExecutor executor) {
+ try {
+ run();
+ } catch (BluetoothException e) {
+ executor.postResult(this, e);
+ }
+ }
+
+ /**
+ * Run function. Not supported.
+ */
+ @SuppressWarnings("unused")
+ public void run() throws BluetoothException {
+ throw new RuntimeException("Not implemented");
+ }
+
+ /**
+ * Try to cancel operation when a timeout occurs.
+ */
+ public void cancel() {
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o == null) {
+ return false;
+ }
+ if (!Operation.class.isInstance(o)) {
+ return false;
+ }
+ Operation<?> other = (Operation<?>) o;
+ return Arrays.equals(mElements, other.mElements);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mElements);
+ }
+
+ @Override
+ public String toString() {
+ return Joiner.on('-').join(mElements);
+ }
+ }
+
+ /**
+ * Synchronous bluetooth operation to schedule.
+ *
+ * @param <T> Type of provided instance.
+ */
+ public static class SynchronousOperation<T> extends Operation<T> {
+
+ public SynchronousOperation(Object... elements) {
+ super(elements);
+ }
+
+ @Override
+ public void execute(BluetoothOperationExecutor executor) {
+ try {
+ Object result = call();
+ if (result == null) {
+ result = NULL_RESULT;
+ }
+ executor.postResult(this, result);
+ } catch (BluetoothException e) {
+ executor.postResult(this, e);
+ }
+ }
+
+ /**
+ * Call function. Not supported.
+ */
+ @SuppressWarnings("unused")
+ @Nullable
+ public T call() throws BluetoothException {
+ throw new RuntimeException("Not implemented");
+ }
+ }
+
+ /**
+ * {@link Future} to wait / get result of an operation.
+ *
+ * <li>Waits for operation to complete
+ * <li>Handles timeouts if needed
+ * <li>Queues identical Bluetooth operations
+ * <li>Unwraps Exceptions and null values
+ */
+ private class BluetoothOperationFuture<T> implements Future<T> {
+
+ private final Object mLock = new Object();
+
+ /**
+ * Queue that will be used to store the result. It should normally contains one element
+ * maximum, but using a queue avoid some race conditions.
+ */
+ private final BlockingQueue<Object> mResultQueue;
+ private final Operation<T> mBluetoothOperation;
+ private final boolean mOperationExecuted;
+ private boolean mIsCancelled = false;
+ private boolean mIsDone = false;
+
+ BluetoothOperationFuture(BlockingQueue<Object> resultQueue,
+ Operation<T> bluetoothOperation, boolean operationExecuted) {
+ mResultQueue = resultQueue;
+ mBluetoothOperation = bluetoothOperation;
+ mOperationExecuted = operationExecuted;
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ synchronized (mLock) {
+ if (mIsDone) {
+ return false;
+ }
+ if (mIsCancelled) {
+ return true;
+ }
+ mBluetoothOperation.cancel();
+ mIsCancelled = true;
+ notifyFailure(mBluetoothOperation, new BluetoothException("Operation cancelled."));
+ return true;
+ }
+ }
+
+ @Override
+ public boolean isCancelled() {
+ synchronized (mLock) {
+ return mIsCancelled;
+ }
+ }
+
+ @Override
+ public boolean isDone() {
+ synchronized (mLock) {
+ return mIsDone;
+ }
+ }
+
+ @Override
+ @Nullable
+ public T get() throws InterruptedException, ExecutionException {
+ try {
+ return getInternal(NO_TIMEOUT, TimeUnit.MILLISECONDS);
+ } catch (TimeoutException e) {
+ throw new RuntimeException(e); // This is not supposed to be thrown
+ }
+ }
+
+ @Override
+ @Nullable
+ public T get(long timeoutMillis, TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ return getInternal(Math.max(0, timeoutMillis), unit);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Nullable
+ private T getInternal(long timeoutMillis, TimeUnit unit)
+ throws ExecutionException, InterruptedException, TimeoutException {
+ // Prevent parallel executions of this method.
+ long startTime = mTimeProvider.getTimeMillis();
+ synchronized (this) {
+ synchronized (mLock) {
+ if (mIsDone) {
+ throw new ExecutionException(
+ new BluetoothException("get() called twice..."));
+ }
+ }
+ if (!mOperationExecuted) {
+ if (timeoutMillis == NO_TIMEOUT) {
+ mOperationSemaphore.acquire();
+ } else {
+ if (!mOperationSemaphore.tryAcquire(timeoutMillis
+ - (mTimeProvider.getTimeMillis() - startTime), unit)) {
+ throw new TimeoutException(String.format(Locale.US,
+ "A timeout occurred when processing %s after %s %s.",
+ mBluetoothOperation, timeoutMillis, unit));
+ }
+ }
+ mBluetoothOperation.execute(BluetoothOperationExecutor.this);
+ }
+ Object result;
+
+ if (timeoutMillis == NO_TIMEOUT) {
+ result = mResultQueue.take();
+ } else {
+ result = mResultQueue.poll(
+ timeoutMillis - (mTimeProvider.getTimeMillis() - startTime), unit);
+ }
+
+ if (result == null) {
+ throw new TimeoutException(String.format(Locale.US,
+ "A timeout occurred when processing %s after %s ms.",
+ mBluetoothOperation, timeoutMillis));
+ }
+ synchronized (mLock) {
+ mIsDone = true;
+ }
+ if (result instanceof BluetoothException) {
+ throw new ExecutionException((BluetoothException) result);
+ }
+ if (result == NULL_RESULT) {
+ result = null;
+ }
+ return (T) result;
+ }
+ }
+ }
+
+ /**
+ * Exception thrown when an operation execution times out. Since state of the system is unknown
+ * afterward (operation may still complete or not), it is recommended to disconnect and
+ * reconnect.
+ */
+ public static class BluetoothOperationTimeoutException extends BluetoothException {
+
+ public BluetoothOperationTimeoutException(String message) {
+ super(message);
+ }
+
+ public BluetoothOperationTimeoutException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/eventloop/Annotations.java b/nearby/service/java/com/android/server/nearby/common/eventloop/Annotations.java
new file mode 100644
index 0000000..44c9422
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/eventloop/Annotations.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.eventloop;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.BinderThread;
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * A collection of threading annotations relating to EventLoop. These should be used in conjunction
+ * with {@link UiThread}, {@link BinderThread}, {@link WorkerThread}, and {@link AnyThread}.
+ */
+public class Annotations {
+
+ /**
+ * Denotes that the annotated method or constructor should only be called on the EventLoop
+ * thread.
+ */
+ @Retention(CLASS)
+ @Target({METHOD, CONSTRUCTOR, TYPE})
+ public @interface EventThread {
+ }
+
+ /** Denotes that the annotated method or constructor should only be called on a Network
+ * thread. */
+ @Retention(CLASS)
+ @Target({METHOD, CONSTRUCTOR, TYPE})
+ public @interface NetworkThread {
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/eventloop/EventLoop.java b/nearby/service/java/com/android/server/nearby/common/eventloop/EventLoop.java
new file mode 100644
index 0000000..c89366f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/eventloop/EventLoop.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.eventloop;
+
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+
+/**
+ * Handles executing runnables on a background thread.
+ *
+ * <p>Nearby services follow an event loop model where events can be queued and delivered in the
+ * future. All code that is run in this EventLoop is guaranteed to be run on this thread. The main
+ * advantage of this model is that all modules don't have to deal with synchronization and race
+ * conditions, while making it easy to handle the several asynchronous tasks that are expected to be
+ * needed for this type of provider (such as starting a WiFi scan and waiting for the result,
+ * starting BLE scans, doing a server request and waiting for the response etc.).
+ *
+ * <p>Code that needs to wait for an event should not spawn a new thread nor sleep. It should simply
+ * deliver a new message to the event queue when the reply of the event happens.
+ */
+// TODO(b/177675274): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class EventLoop {
+
+ private final Interface mImpl;
+
+ private EventLoop(Interface impl) {
+ this.mImpl = impl;
+ }
+
+ protected EventLoop(String name) {
+ this(new HandlerEventLoopImpl(name));
+ }
+
+ /** Creates an EventLoop. */
+ public static EventLoop newInstance(String name) {
+ return new EventLoop(name);
+ }
+
+ /** Creates an EventLoop. */
+ public static EventLoop newInstance(String name, Looper looper) {
+ return new EventLoop(new HandlerEventLoopImpl(name, looper));
+ }
+
+ /** Marks the EventLoop as destroyed. Any further messages received will be ignored. */
+ public void destroy() {
+ mImpl.destroy();
+ }
+
+ /**
+ * Posts a runnable to this event loop, blocking until the runnable has been executed. This
+ * should
+ * be used rarely. It could be useful, for example, for a runnable that initializes the system
+ * and
+ * must block the posting of all other runnables.
+ *
+ * @param runnable a Runnable to post. This method will not return until the run() method of the
+ * given runnable has executed on the background thread.
+ */
+ public void postAndWait(final NamedRunnable runnable) throws InterruptedException {
+ mImpl.postAndWait(runnable);
+ }
+
+ /**
+ * Posts a runnable to this to the front of the event loop, blocking until the runnable has been
+ * executed. This should be used rarely, as it can starve the event loop.
+ *
+ * @param runnable a Runnable to post. This method will not return until the run() method of the
+ * given runnable has executed on the background thread.
+ */
+ public void postToFrontAndWait(final NamedRunnable runnable) throws InterruptedException {
+ mImpl.postToFrontAndWait(runnable);
+ }
+
+ /** Checks if there are any pending posts of the Runnable in the queue. */
+ public boolean isPosted(NamedRunnable runnable) {
+ return mImpl.isPosted(runnable);
+ }
+
+ /**
+ * Run code on the event loop thread.
+ *
+ * @param runnable the runnable to execute.
+ */
+ public void postRunnable(NamedRunnable runnable) {
+ mImpl.postRunnable(runnable);
+ }
+
+ /**
+ * Run code to be executed when there is no runnable scheduled.
+ *
+ * @param runnable last runnable to execute.
+ */
+ public void postEmptyQueueRunnable(final NamedRunnable runnable) {
+ mImpl.postEmptyQueueRunnable(runnable);
+ }
+
+ /**
+ * Run code on the event loop thread after delayedMillis.
+ *
+ * @param runnable the runnable to execute.
+ * @param delayedMillis the number of milliseconds before executing the runnable.
+ */
+ public void postRunnableDelayed(NamedRunnable runnable, long delayedMillis) {
+ mImpl.postRunnableDelayed(runnable, delayedMillis);
+ }
+
+ /**
+ * Removes and cancels the specified {@code runnable} if it had not posted/started yet. Calling
+ * with null does nothing.
+ */
+ public void removeRunnable(@Nullable NamedRunnable runnable) {
+ mImpl.removeRunnable(runnable);
+ }
+
+ /** Asserts that the current operation is being executed in the Event Loop's thread. */
+ public void checkThread() {
+ mImpl.checkThread();
+ }
+
+ public Handler getHandler() {
+ return mImpl.getHandler();
+ }
+
+ interface Interface {
+ void destroy();
+
+ void postAndWait(NamedRunnable runnable) throws InterruptedException;
+
+ void postToFrontAndWait(NamedRunnable runnable) throws InterruptedException;
+
+ boolean isPosted(NamedRunnable runnable);
+
+ void postRunnable(NamedRunnable runnable);
+
+ void postEmptyQueueRunnable(NamedRunnable runnable);
+
+ void postRunnableDelayed(NamedRunnable runnable, long delayedMillis);
+
+ void removeRunnable(NamedRunnable runnable);
+
+ void checkThread();
+
+ Handler getHandler();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/eventloop/HandlerEventLoopImpl.java b/nearby/service/java/com/android/server/nearby/common/eventloop/HandlerEventLoopImpl.java
new file mode 100644
index 0000000..018dcdb
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/eventloop/HandlerEventLoopImpl.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.eventloop;
+
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * Handles executing runnables on a background thread.
+ *
+ * <p>Nearby services follow an event loop model where events can be queued and delivered in the
+ * future. All code that is run in this package is guaranteed to be run on this thread. The main
+ * advantage of this model is that all modules don't have to deal with synchronization and race
+ * conditions, while making it easy to handle the several asynchronous tasks that are expected to be
+ * needed for this type of provider (such as starting a WiFi scan and waiting for the result,
+ * starting BLE scans, doing a server request and waiting for the response etc.).
+ *
+ * <p>Code that needs to wait for an event should not spawn a new thread nor sleep. It should simply
+ * deliver a new message to the event queue when the reply of the event happens.
+ *
+ * <p>
+ */
+// TODO(b/203471261) use executor instead of handler
+// TODO(b/177675274): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+final class HandlerEventLoopImpl implements EventLoop.Interface {
+ /** The {@link Message#what} code for all messages that we post to the EventLoop. */
+ private static final int WHAT = 0;
+
+ private static final long ELAPSED_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(5);
+ private static final long RUNNABLE_DELAY_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(2);
+ private static final String TAG = HandlerEventLoopImpl.class.getSimpleName();
+ private final MyHandler mHandler;
+
+ private volatile boolean mIsDestroyed = false;
+
+ /** Constructs an EventLoop. */
+ HandlerEventLoopImpl(String name) {
+ this(name, createHandlerThread(name));
+ }
+
+ HandlerEventLoopImpl(String name, Looper looper) {
+
+ mHandler = new MyHandler(looper);
+ Log.d(TAG,
+ "Created EventLoop for thread '" + looper.getThread().getName()
+ + "(id: " + looper.getThread().getId() + ")'");
+ }
+
+ private static Looper createHandlerThread(String name) {
+ HandlerThread handlerThread = new HandlerThread(name, Process.THREAD_PRIORITY_BACKGROUND);
+ handlerThread.start();
+
+ return handlerThread.getLooper();
+ }
+
+ /**
+ * Wrapper to satisfy Android Lint. {@link Looper#getQueue()} is public and available since ICS,
+ * but was marked @hide until Marshmallow. Tested that this code doesn't crash pre-Marshmallow.
+ * /aosp-ics/frameworks/base/core/java/android/os/Looper.java?l=218
+ */
+ @SuppressLint("NewApi")
+ private static MessageQueue getQueue(Handler handler) {
+ return handler.getLooper().getQueue();
+ }
+
+ /** Marks the EventLoop as destroyed. Any further messages received will be ignored. */
+ @Override
+ public void destroy() {
+ Looper looper = mHandler.getLooper();
+ Log.d(TAG,
+ "Destroying EventLoop for thread " + looper.getThread().getName()
+ + " (id: " + looper.getThread().getId() + ")");
+ looper.quit();
+ mIsDestroyed = true;
+ }
+
+ /**
+ * Posts a runnable to this event loop, blocking until the runnable has been executed. This
+ * should
+ * be used rarely. It could be useful, for example, for a runnable that initializes the system
+ * and
+ * must block the posting of all other runnables.
+ *
+ * @param runnable a Runnable to post. This method will not return until the run() method of the
+ * given runnable has executed on the background thread.
+ */
+ @Override
+ public void postAndWait(final NamedRunnable runnable) throws InterruptedException {
+ internalPostAndWait(runnable, false);
+ }
+
+ @Override
+ public void postToFrontAndWait(final NamedRunnable runnable) throws InterruptedException {
+ internalPostAndWait(runnable, true);
+ }
+
+ /** Checks if there are any pending posts of the Runnable in the queue. */
+ @Override
+ public boolean isPosted(NamedRunnable runnable) {
+ return mHandler.hasMessages(WHAT, runnable);
+ }
+
+ /**
+ * Run code on the event loop thread.
+ *
+ * @param runnable the runnable to execute.
+ */
+ @Override
+ public void postRunnable(NamedRunnable runnable) {
+ Log.d(TAG, "Posting " + runnable);
+ mHandler.post(runnable, 0L, false);
+ }
+
+ /**
+ * Run code to be executed when there is no runnable scheduled.
+ *
+ * @param runnable last runnable to execute.
+ */
+ @Override
+ public void postEmptyQueueRunnable(final NamedRunnable runnable) {
+ mHandler.post(
+ () ->
+ getQueue(mHandler)
+ .addIdleHandler(
+ () -> {
+ if (mHandler.hasMessages(WHAT)) {
+ return true;
+ } else {
+ // Only stop if start has not been called since
+ // this was queued
+ runnable.run();
+ return false;
+ }
+ }));
+ }
+
+ /**
+ * Run code on the event loop thread after delayedMillis.
+ *
+ * @param runnable the runnable to execute.
+ * @param delayedMillis the number of milliseconds before executing the runnable.
+ */
+ @Override
+ public void postRunnableDelayed(NamedRunnable runnable, long delayedMillis) {
+ Log.d(TAG, "Posting " + runnable + " [delay " + delayedMillis + "]");
+ mHandler.post(runnable, delayedMillis, false);
+ }
+
+ /**
+ * Removes and cancels the specified {@code runnable} if it had not posted/started yet. Calling
+ * with null does nothing.
+ */
+ @Override
+ public void removeRunnable(@Nullable NamedRunnable runnable) {
+ if (runnable != null) {
+ // Removes any pending sent messages where what=WHAT and obj=runnable. We can't use
+ // removeCallbacks(runnable) because we're not posting the runnable directly, we're
+ // sending a Message with the runnable as its obj.
+ mHandler.removeMessages(WHAT, runnable);
+ }
+ }
+
+ /** Asserts that the current operation is being executed in the Event Loop's thread. */
+ @Override
+ public void checkThread() {
+
+ Thread currentThread = Looper.myLooper().getThread();
+ Thread expectedThread = mHandler.getLooper().getThread();
+ if (currentThread.getId() != expectedThread.getId()) {
+ throw new IllegalStateException(
+ String.format(
+ "This method must run in the EventLoop thread '%s (id: %s)'. "
+ + "Was called from thread '%s (id: %s)'.",
+ expectedThread.getName(),
+ expectedThread.getId(),
+ currentThread.getName(),
+ currentThread.getId()));
+ }
+
+ }
+
+ @Override
+ public Handler getHandler() {
+ return mHandler;
+ }
+
+ private void internalPostAndWait(final NamedRunnable runnable, boolean postToFront)
+ throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ NamedRunnable delegate =
+ new NamedRunnable(runnable.name) {
+ @Override
+ public void run() {
+ try {
+ runnable.run();
+ } finally {
+ latch.countDown();
+ }
+ }
+ };
+
+ Log.d(TAG, "Posting " + delegate + " and wait");
+ if (!mHandler.post(delegate, 0L, postToFront)) {
+ // Do not wait if delegate is not posted.
+ Log.d(TAG, delegate + " not posted");
+ latch.countDown();
+ }
+ latch.await();
+ }
+
+ /** Handler that executes code on a private event loop thread. */
+ private class MyHandler extends Handler {
+
+ MyHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ NamedRunnable runnable = (NamedRunnable) msg.obj;
+
+ if (mIsDestroyed) {
+ Log.w(TAG, "Runnable " + runnable
+ + " attempted to run after the EventLoop was destroyed. Ignoring");
+ return;
+ }
+ Log.i(TAG, "Executing " + runnable);
+
+ // Did this runnable start much later than we expected it to? If so, then log.
+ long expectedStartTime = (long) msg.arg1 << 32 | (msg.arg2 & 0xFFFFFFFFL);
+ logIfExceedsThreshold(
+ RUNNABLE_DELAY_THRESHOLD_MS, expectedStartTime, runnable, "was delayed for");
+
+ long startTimeMillis = SystemClock.elapsedRealtime();
+ try {
+ runnable.run();
+ } catch (Exception t) {
+ Log.e(TAG, runnable + "crashed.");
+ throw t;
+ } finally {
+ logIfExceedsThreshold(ELAPSED_THRESHOLD_MS, startTimeMillis, runnable, "ran for");
+ }
+ }
+
+ private boolean post(NamedRunnable runnable, long delayedMillis, boolean postToFront) {
+ if (mIsDestroyed) {
+ Log.w(TAG, runnable + " not posted since EventLoop is destroyed");
+ return false;
+ }
+ long expectedStartTime = SystemClock.elapsedRealtime() + delayedMillis;
+ int arg1 = (int) (expectedStartTime >> 32);
+ int arg2 = (int) expectedStartTime;
+ Message message = obtainMessage(WHAT, arg1, arg2, runnable /* obj */);
+ boolean sent =
+ postToFront
+ ? sendMessageAtFrontOfQueue(message)
+ : sendMessageDelayed(message, delayedMillis);
+ if (!sent) {
+ Log.w(TAG, runnable + "not posted since looper is exiting");
+ }
+ return sent;
+ }
+
+ private void logIfExceedsThreshold(
+ long thresholdMillis, long startTimeMillis, NamedRunnable runnable,
+ String message) {
+ long elapsedMillis = SystemClock.elapsedRealtime() - startTimeMillis;
+ if (elapsedMillis > thresholdMillis) {
+ String elapsedFormatted =
+ new SimpleDateFormat("mm:ss.SSS", Locale.US).format(elapsedMillis);
+ Log.w(TAG, runnable + " " + message + " " + elapsedFormatted);
+ }
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/eventloop/NamedRunnable.java b/nearby/service/java/com/android/server/nearby/common/eventloop/NamedRunnable.java
new file mode 100644
index 0000000..578e3f6
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/eventloop/NamedRunnable.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.eventloop;
+
+/** A Runnable with a name, for logging purposes. */
+public abstract class NamedRunnable implements Runnable {
+ public final String name;
+
+ public NamedRunnable(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return "Runnable[" + name + "]";
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/fastpair/IconUtils.java b/nearby/service/java/com/android/server/nearby/common/fastpair/IconUtils.java
new file mode 100644
index 0000000..35a1a9f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/fastpair/IconUtils.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.fastpair;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.core.graphics.ColorUtils;
+
+/** Utility methods for icon size verification. */
+public class IconUtils {
+ private static final int MIN_ICON_SIZE = 16;
+ private static final int DESIRED_ICON_SIZE = 32;
+ private static final double NOTIFICATION_BACKGROUND_PADDING_PERCENTAGE = 0.125;
+ private static final double NOTIFICATION_BACKGROUND_ALPHA = 0.7;
+
+ /**
+ * Verify that the icon is non null and falls in the small bucket. Just because an icon isn't
+ * small doesn't guarantee it is large or exists.
+ */
+ @VisibleForTesting
+ static boolean isIconSizedSmall(@Nullable Bitmap bitmap) {
+ if (bitmap == null) {
+ return false;
+ }
+ int min = MIN_ICON_SIZE;
+ int desired = DESIRED_ICON_SIZE;
+ return bitmap.getWidth() >= min
+ && bitmap.getWidth() < desired
+ && bitmap.getHeight() >= min
+ && bitmap.getHeight() < desired;
+ }
+
+ /**
+ * Verify that the icon is non null and falls in the regular / default size bucket. Doesn't
+ * guarantee if not regular then it is small.
+ */
+ @VisibleForTesting
+ static boolean isIconSizedRegular(@Nullable Bitmap bitmap) {
+ if (bitmap == null) {
+ return false;
+ }
+ return bitmap.getWidth() >= DESIRED_ICON_SIZE
+ && bitmap.getHeight() >= DESIRED_ICON_SIZE;
+ }
+
+ // All icons that are sized correctly (larger than the min icon size) are resize on the server
+ // to the desired icon size so that they appear correct in notifications.
+
+ /**
+ * All icons that are sized correctly (larger than the min icon size) are resize on the server
+ * to the desired icon size so that they appear correct in notifications.
+ */
+ public static boolean isIconSizeCorrect(@Nullable Bitmap bitmap) {
+ if (bitmap == null) {
+ return false;
+ }
+ return isIconSizedSmall(bitmap) || isIconSizedRegular(bitmap);
+ }
+
+ /** Adds a circular, white background to the bitmap. */
+ @Nullable
+ public static Bitmap addWhiteCircleBackground(Context context, @Nullable Bitmap bitmap) {
+ if (bitmap == null) {
+ return null;
+ }
+
+ if (bitmap.getWidth() != bitmap.getHeight()) {
+ return bitmap;
+ }
+
+ int padding = (int) (bitmap.getWidth() * NOTIFICATION_BACKGROUND_PADDING_PERCENTAGE);
+ Bitmap bitmapWithBackground =
+ Bitmap.createBitmap(
+ bitmap.getWidth() + (2 * padding),
+ bitmap.getHeight() + (2 * padding),
+ bitmap.getConfig());
+ Canvas canvas = new Canvas(bitmapWithBackground);
+ Paint paint = new Paint();
+ paint.setColor(
+ ColorUtils.setAlphaComponent(
+ Color.WHITE, (int) (255 * NOTIFICATION_BACKGROUND_ALPHA)));
+ paint.setStyle(Paint.Style.FILL);
+ paint.setAntiAlias(true);
+ canvas.drawCircle(
+ bitmapWithBackground.getWidth() / 2f,
+ bitmapWithBackground.getHeight() / 2f,
+ bitmapWithBackground.getWidth() / 2f,
+ paint);
+ canvas.drawBitmap(bitmap, padding, padding, null);
+ return bitmapWithBackground;
+ }
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/common/fastpair/service/UserActionHandlerBase.java b/nearby/service/java/com/android/server/nearby/common/fastpair/service/UserActionHandlerBase.java
new file mode 100644
index 0000000..67d87e3
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/fastpair/service/UserActionHandlerBase.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.fastpair.service;
+
+/** Handles intents to {@link com.android.server.nearby.fastpair.FastPairManager}. */
+public class UserActionHandlerBase {
+ public static final String PREFIX = "com.android.server.nearby.fastpair.";
+ public static final String ACTION_PREFIX = "com.android.server.nearby:";
+
+ public static final String EXTRA_ITEM_ID = PREFIX + "EXTRA_ITEM_ID";
+ public static final String EXTRA_COMPANION_APP = ACTION_PREFIX + "EXTRA_COMPANION_APP";
+ public static final String EXTRA_MAC_ADDRESS = PREFIX + "EXTRA_MAC_ADDRESS";
+
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/common/locator/Locator.java b/nearby/service/java/com/android/server/nearby/common/locator/Locator.java
new file mode 100644
index 0000000..f8b43a6
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/locator/Locator.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.locator;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Collection of bindings that map service types to their respective implementation(s). */
+public class Locator {
+ private static final Object UNBOUND = new Object();
+ private final Context mContext;
+ @Nullable
+ private Locator mParent;
+ private final String mTag; // For debugging
+ private final Map<Class<?>, Object> mBindings = new HashMap<>();
+ private final ArrayList<Module> mModules = new ArrayList<>();
+
+ /** Thrown upon attempt to bind an interface twice. */
+ public static class DuplicateBindingException extends RuntimeException {
+ DuplicateBindingException(String msg) {
+ super(msg);
+ }
+ }
+
+ /** Constructor with a null parent. */
+ public Locator(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Constructor. Supply a valid context and the Locator's parent.
+ *
+ * <p>To find a suitable parent you may want to use findLocator.
+ */
+ public Locator(Context context, @Nullable Locator parent) {
+ this.mContext = context;
+ this.mParent = parent;
+ this.mTag = context.getClass().getName();
+ }
+
+ /** Attaches the parent to the locator. */
+ public void attachParent(Locator parent) {
+ this.mParent = parent;
+ }
+
+ /** Associates the specified type with the supplied instance. */
+ public <T extends Object> Locator bind(Class<T> type, T instance) {
+ bindKeyValue(type, instance);
+ return this;
+ }
+
+ /** For tests only. Disassociates the specified type from any instance. */
+ @VisibleForTesting
+ public <T extends Object> Locator overrideBindingForTest(Class<T> type, T instance) {
+ mBindings.remove(type);
+ return bind(type, instance);
+ }
+
+ /** For tests only. Force Locator to return null when try to get an instance. */
+ @VisibleForTesting
+ public <T> Locator removeBindingForTest(Class<T> type) {
+ Locator locator = this;
+ do {
+ locator.mBindings.put(type, UNBOUND);
+ locator = locator.mParent;
+ } while (locator != null);
+ return this;
+ }
+
+ /** Binds a module. */
+ public synchronized Locator bind(Module module) {
+ mModules.add(module);
+ return this;
+ }
+
+ /**
+ * Searches the chain of locators for a binding for the given type.
+ *
+ * @throws IllegalStateException if no binding is found.
+ */
+ public <T> T get(Class<T> type) {
+ T instance = getOptional(type);
+ if (instance != null) {
+ return instance;
+ }
+
+ String errorMessage = getUnboundErrorMessage(type);
+ throw new IllegalStateException(errorMessage);
+ }
+
+ private String getUnboundErrorMessage(Class<?> type) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Unbound type: ").append(type.getName()).append("\n").append(
+ "Searched locators:\n");
+ Locator locator = this;
+ while (true) {
+ sb.append(locator.mTag);
+ locator = locator.mParent;
+ if (locator == null) {
+ break;
+ }
+ sb.append(" ->\n");
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Searches the chain of locators for a binding for the given type. Returns null if no locator
+ * was
+ * found.
+ */
+ @Nullable
+ public <T> T getOptional(Class<T> type) {
+ Locator locator = this;
+ do {
+ T instance = locator.getInstance(type);
+ if (instance != null) {
+ return instance;
+ }
+ locator = locator.mParent;
+ } while (locator != null);
+ return null;
+ }
+
+ private synchronized <T extends Object> void bindKeyValue(Class<T> key, T value) {
+ Object boundInstance = mBindings.get(key);
+ if (boundInstance != null) {
+ if (boundInstance == UNBOUND) {
+ Log.w(mTag, "Bind call too late - someone already tried to get: " + key);
+ } else {
+ throw new DuplicateBindingException("Duplicate binding: " + key);
+ }
+ }
+ mBindings.put(key, value);
+ }
+
+ // Suppress warning of cast from Object -> T
+ @SuppressWarnings("unchecked")
+ @Nullable
+ private synchronized <T> T getInstance(Class<T> type) {
+ if (mContext == null) {
+ throw new IllegalStateException("Locator not initialized yet.");
+ }
+
+ T instance = (T) mBindings.get(type);
+ if (instance != null) {
+ return instance != UNBOUND ? instance : null;
+ }
+
+ // Ask modules to supply a binding
+ int moduleCount = mModules.size();
+ for (int i = 0; i < moduleCount; i++) {
+ mModules.get(i).configure(mContext, type, this);
+ }
+
+ instance = (T) mBindings.get(type);
+ if (instance == null) {
+ mBindings.put(type, UNBOUND);
+ }
+ return instance;
+ }
+
+ /**
+ * Iterates over all bound objects and gives the modules a chance to clean up the objects they
+ * have created.
+ */
+ public synchronized void destroy() {
+ for (Class<?> type : mBindings.keySet()) {
+ Object instance = mBindings.get(type);
+ if (instance == UNBOUND) {
+ continue;
+ }
+
+ for (Module module : mModules) {
+ module.destroy(mContext, type, instance);
+ }
+ }
+ mBindings.clear();
+ }
+
+ /** Returns true if there are no bindings. */
+ public boolean isEmpty() {
+ return mBindings.isEmpty();
+ }
+
+ /** Returns the parent locator or null if no parent. */
+ @Nullable
+ public Locator getParent() {
+ return mParent;
+ }
+
+ /**
+ * Finds the first locator, then searches the chain of locators for a binding for the given
+ * type.
+ *
+ * @throws IllegalStateException if no binding is found.
+ */
+ public static <T> T get(Context context, Class<T> type) {
+ Locator locator = findLocator(context);
+ if (locator == null) {
+ throw new IllegalStateException("No locator found in context " + context);
+ }
+ return locator.get(type);
+ }
+
+ /**
+ * Find the first locator from the context wrapper.
+ */
+ public static <T> T getFromContextWrapper(LocatorContextWrapper wrapper, Class<T> type) {
+ Locator locator = wrapper.getLocator();
+ if (locator == null) {
+ throw new IllegalStateException("No locator found in context wrapper");
+ }
+ return locator.get(type);
+ }
+
+ /**
+ * Finds the first locator, then searches the chain of locators for a binding for the given
+ * type.
+ * Returns null if no binding was found.
+ */
+ @Nullable
+ public static <T> T getOptional(Context context, Class<T> type) {
+ Locator locator = findLocator(context);
+ if (locator == null) {
+ return null;
+ }
+ return locator.getOptional(type);
+ }
+
+ /** Finds the first locator in the context hierarchy. */
+ @Nullable
+ public static Locator findLocator(Context context) {
+ Context applicationContext = context.getApplicationContext();
+ boolean applicationContextVisited = false;
+
+ Context searchContext = context;
+ do {
+ Locator locator = tryGetLocator(searchContext);
+ if (locator != null) {
+ return locator;
+ }
+
+ applicationContextVisited |= (searchContext == applicationContext);
+
+ if (searchContext instanceof ContextWrapper) {
+ searchContext = ((ContextWrapper) context).getBaseContext();
+
+ if (searchContext == null) {
+ throw new IllegalStateException(
+ "Invalid ContextWrapper -- If this is a Robolectric test, "
+ + "have you called ActivityController.create()?");
+ }
+ } else if (!applicationContextVisited) {
+ searchContext = applicationContext;
+ } else {
+ searchContext = null;
+ }
+ } while (searchContext != null);
+
+ return null;
+ }
+
+ @Nullable
+ private static Locator tryGetLocator(Object object) {
+ if (object instanceof LocatorContext) {
+ Locator locator = ((LocatorContext) object).getLocator();
+ if (locator == null) {
+ throw new IllegalStateException(
+ "LocatorContext must not return null Locator: " + object);
+ }
+ return locator;
+ }
+ return null;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/locator/LocatorContext.java b/nearby/service/java/com/android/server/nearby/common/locator/LocatorContext.java
new file mode 100644
index 0000000..06eef8a
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/locator/LocatorContext.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.locator;
+
+/**
+ * An object that has a {@link Locator}. The locator can be used to resolve service types to their
+ * respective implementation(s).
+ */
+public interface LocatorContext {
+ /** Returns the locator. May not return null. */
+ Locator getLocator();
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/locator/LocatorContextWrapper.java b/nearby/service/java/com/android/server/nearby/common/locator/LocatorContextWrapper.java
new file mode 100644
index 0000000..03df33f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/locator/LocatorContextWrapper.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.locator;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.ContextWrapper;
+
+/**
+ * Wraps a Context and associates it with a Locator, optionally linking it with a parent locator.
+ */
+public class LocatorContextWrapper extends ContextWrapper implements LocatorContext {
+ private final Locator mLocator;
+ private final Context mContext;
+ /** Constructs a context wrapper with a Locator linked to the passed locator. */
+ public LocatorContextWrapper(Context context, @Nullable Locator parentLocator) {
+ super(context);
+ mContext = context;
+ // Assigning under initialization object, but it's safe, since locator is used lazily.
+ this.mLocator = new Locator(this, parentLocator);
+ }
+
+ /**
+ * Constructs a context wrapper.
+ *
+ * <p>Uses the Locator associated with the passed context as the parent.
+ */
+ public LocatorContextWrapper(Context context) {
+ this(context, Locator.findLocator(context));
+ }
+
+ /**
+ * Get the context of the context wrapper.
+ */
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ public Locator getLocator() {
+ return mLocator;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/locator/Module.java b/nearby/service/java/com/android/server/nearby/common/locator/Module.java
new file mode 100644
index 0000000..0131c44
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/locator/Module.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.locator;
+
+import android.content.Context;
+
+/** Configures late bindings of service types to their concrete implementations. */
+public abstract class Module {
+ /**
+ * Configures the binding between the {@code type} and its implementation by calling methods on
+ * the {@code locator}, for example:
+ *
+ * <pre>{@code
+ * void configure(Context context, Class<?> type, Locator locator) {
+ * if (type == MyService.class) {
+ * locator.bind(MyService.class, new MyImplementation(context));
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>If the module does not recognize the specified type, the method does not have to do
+ * anything.
+ */
+ public abstract void configure(Context context, Class<?> type, Locator locator);
+
+ /**
+ * Notifies you that a binding of class {@code type} is no longer needed and can now release
+ * everything it was holding on to, such as a database connection.
+ *
+ * <pre>{@code
+ * void destroy(Context context, Class<?> type, Object instance) {
+ * if (type == MyService.class) {
+ * ((MyService) instance).destroy();
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>If the module does not recognize the specified type, the method does not have to do
+ * anything.
+ */
+ public void destroy(Context context, Class<?> type, Object instance) {}
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/common/servicemonitor/CurrentUserServiceProvider.java b/nearby/service/java/com/android/server/nearby/common/servicemonitor/CurrentUserServiceProvider.java
new file mode 100644
index 0000000..80248e8
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/servicemonitor/CurrentUserServiceProvider.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.servicemonitor;
+
+import static android.content.pm.PackageManager.GET_META_DATA;
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO;
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.nearby.common.servicemonitor.ServiceMonitor.ServiceChangedListener;
+import com.android.server.nearby.common.servicemonitor.ServiceMonitor.ServiceProvider;
+
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * This is mostly borrowed from frameworks CurrentUserServiceSupplier.
+ * Provides services based on the current active user and version as defined in the service
+ * manifest. This implementation uses {@link android.content.pm.PackageManager#MATCH_SYSTEM_ONLY} to
+ * ensure only system (ie, privileged) services are matched. It also handles services that are not
+ * direct boot aware, and will automatically pick the best service as the user's direct boot state
+ * changes.
+ */
+public final class CurrentUserServiceProvider extends BroadcastReceiver implements
+ ServiceProvider<CurrentUserServiceProvider.BoundServiceInfo> {
+
+ private static final String TAG = "CurrentUserServiceProvider";
+
+ private static final String EXTRA_SERVICE_VERSION = "serviceVersion";
+
+ // This is equal to the hidden Intent.ACTION_USER_SWITCHED.
+ private static final String ACTION_USER_SWITCHED = "android.intent.action.USER_SWITCHED";
+ // This is equal to the hidden Intent.EXTRA_USER_HANDLE.
+ private static final String EXTRA_USER_HANDLE = "android.intent.extra.user_handle";
+ // This is equal to the hidden UserHandle.USER_NULL.
+ private static final int USER_NULL = -10000;
+
+ private static final Comparator<BoundServiceInfo> sBoundServiceInfoComparator = (o1, o2) -> {
+ if (o1 == o2) {
+ return 0;
+ } else if (o1 == null) {
+ return -1;
+ } else if (o2 == null) {
+ return 1;
+ }
+
+ // ServiceInfos with higher version numbers always win.
+ return Integer.compare(o1.getVersion(), o2.getVersion());
+ };
+
+ /** Bound service information with version information. */
+ public static class BoundServiceInfo extends ServiceMonitor.BoundServiceInfo {
+
+ private static int parseUid(ResolveInfo resolveInfo) {
+ return resolveInfo.serviceInfo.applicationInfo.uid;
+ }
+
+ private static int parseVersion(ResolveInfo resolveInfo) {
+ int version = Integer.MIN_VALUE;
+ if (resolveInfo.serviceInfo.metaData != null) {
+ version = resolveInfo.serviceInfo.metaData.getInt(EXTRA_SERVICE_VERSION, version);
+ }
+ return version;
+ }
+
+ private final int mVersion;
+
+ protected BoundServiceInfo(String action, ResolveInfo resolveInfo) {
+ this(
+ action,
+ parseUid(resolveInfo),
+ new ComponentName(
+ resolveInfo.serviceInfo.packageName,
+ resolveInfo.serviceInfo.name),
+ parseVersion(resolveInfo));
+ }
+
+ protected BoundServiceInfo(String action, int uid, ComponentName componentName,
+ int version) {
+ super(action, uid, componentName);
+ mVersion = version;
+ }
+
+ public int getVersion() {
+ return mVersion;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + "@" + mVersion;
+ }
+ }
+
+ /**
+ * Creates an instance with the specific service details.
+ *
+ * @param context the context the provider is to use
+ * @param action the action the service must declare in its intent-filter
+ */
+ public static CurrentUserServiceProvider create(Context context, String action) {
+ return new CurrentUserServiceProvider(context, action);
+ }
+
+ private final Context mContext;
+ private final Intent mIntent;
+ private volatile ServiceChangedListener mListener;
+
+ private CurrentUserServiceProvider(Context context, String action) {
+ mContext = context;
+ mIntent = new Intent(action);
+ }
+
+ @Override
+ public boolean hasMatchingService() {
+ int intentQueryFlags =
+ MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE | MATCH_SYSTEM_ONLY;
+ List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServicesAsUser(
+ mIntent, intentQueryFlags, UserHandle.SYSTEM);
+ return !resolveInfos.isEmpty();
+ }
+
+ @Override
+ public void register(ServiceChangedListener listener) {
+ Preconditions.checkState(mListener == null);
+
+ mListener = listener;
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(ACTION_USER_SWITCHED);
+ intentFilter.addAction(Intent.ACTION_USER_UNLOCKED);
+ mContext.registerReceiverForAllUsers(this, intentFilter, null,
+ ForegroundThread.getHandler());
+ }
+
+ @Override
+ public void unregister() {
+ Preconditions.checkArgument(mListener != null);
+
+ mListener = null;
+ mContext.unregisterReceiver(this);
+ }
+
+ @Override
+ public BoundServiceInfo getServiceInfo() {
+ BoundServiceInfo bestServiceInfo = null;
+
+ // only allow services in the correct direct boot state to match
+ int intentQueryFlags = MATCH_DIRECT_BOOT_AUTO | GET_META_DATA | MATCH_SYSTEM_ONLY;
+ List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServicesAsUser(
+ mIntent, intentQueryFlags, UserHandle.of(ActivityManager.getCurrentUser()));
+ for (ResolveInfo resolveInfo : resolveInfos) {
+ BoundServiceInfo serviceInfo =
+ new BoundServiceInfo(mIntent.getAction(), resolveInfo);
+
+ if (sBoundServiceInfoComparator.compare(serviceInfo, bestServiceInfo) > 0) {
+ bestServiceInfo = serviceInfo;
+ }
+ }
+
+ return bestServiceInfo;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action == null) {
+ return;
+ }
+ int userId = intent.getIntExtra(EXTRA_USER_HANDLE, USER_NULL);
+ if (userId == USER_NULL) {
+ return;
+ }
+ ServiceChangedListener listener = mListener;
+ if (listener == null) {
+ return;
+ }
+
+ switch (action) {
+ case ACTION_USER_SWITCHED:
+ listener.onServiceChanged();
+ break;
+ case Intent.ACTION_USER_UNLOCKED:
+ // user unlocked implies direct boot mode may have changed
+ if (userId == ActivityManager.getCurrentUser()) {
+ listener.onServiceChanged();
+ }
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/servicemonitor/ForegroundThread.java b/nearby/service/java/com/android/server/nearby/common/servicemonitor/ForegroundThread.java
new file mode 100644
index 0000000..2c363f8
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/servicemonitor/ForegroundThread.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.servicemonitor;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import com.android.modules.utils.HandlerExecutor;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Thread for asynchronous event processing. This thread is configured as
+ * {@link android.os.Process#THREAD_PRIORITY_FOREGROUND}, which means more CPU
+ * resources will be dedicated to it, and it will be treated like "a user
+ * interface that the user is interacting with."
+ * <p>
+ * This thread is best suited for tasks that the user is actively waiting for,
+ * or for tasks that the user expects to be executed immediately.
+ *
+ */
+public final class ForegroundThread extends HandlerThread {
+ private static ForegroundThread sInstance;
+ private static Handler sHandler;
+ private static HandlerExecutor sHandlerExecutor;
+
+ private ForegroundThread() {
+ super("nearbyfg", android.os.Process.THREAD_PRIORITY_FOREGROUND);
+ }
+
+ private static void ensureThreadLocked() {
+ if (sInstance == null) {
+ sInstance = new ForegroundThread();
+ sInstance.start();
+ sHandler = new Handler(sInstance.getLooper());
+ sHandlerExecutor = new HandlerExecutor(sHandler);
+ }
+ }
+
+ /** Get ForegroundThread singleton instance. */
+ public static ForegroundThread get() {
+ synchronized (ForegroundThread.class) {
+ ensureThreadLocked();
+ return sInstance;
+ }
+ }
+
+ /** Get ForegroundThread singleton handler. */
+ public static Handler getHandler() {
+ synchronized (ForegroundThread.class) {
+ ensureThreadLocked();
+ return sHandler;
+ }
+ }
+
+ /** Get ForegroundThread singleton executor. */
+ public static Executor getExecutor() {
+ synchronized (ForegroundThread.class) {
+ ensureThreadLocked();
+ return sHandlerExecutor;
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/servicemonitor/PackageWatcher.java b/nearby/service/java/com/android/server/nearby/common/servicemonitor/PackageWatcher.java
new file mode 100644
index 0000000..7d1db57
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/servicemonitor/PackageWatcher.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.servicemonitor;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+
+import com.android.modules.utils.BackgroundThread;
+
+import java.util.Objects;
+
+/**
+ * This is mostly from frameworks PackageMonitor.
+ * Helper class for watching somePackagesChanged.
+ */
+public abstract class PackageWatcher extends BroadcastReceiver {
+ static final String TAG = "PackageWatcher";
+ static final IntentFilter sPackageFilt = new IntentFilter();
+ static final IntentFilter sNonDataFilt = new IntentFilter();
+ static final IntentFilter sExternalFilt = new IntentFilter();
+
+ static {
+ sPackageFilt.addAction(Intent.ACTION_PACKAGE_ADDED);
+ sPackageFilt.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ sPackageFilt.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ sPackageFilt.addDataScheme("package");
+ sNonDataFilt.addAction(Intent.ACTION_PACKAGES_SUSPENDED);
+ sNonDataFilt.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED);
+ sExternalFilt.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
+ sExternalFilt.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
+ }
+
+ Context mRegisteredContext;
+ Handler mRegisteredHandler;
+ boolean mSomePackagesChanged;
+
+ public PackageWatcher() {
+ }
+
+ void register(Context context, Looper thread, boolean externalStorage) {
+ register(context, externalStorage,
+ (thread == null) ? BackgroundThread.getHandler() : new Handler(thread));
+ }
+
+ void register(Context context, boolean externalStorage, Handler handler) {
+ if (mRegisteredContext != null) {
+ throw new IllegalStateException("Already registered");
+ }
+ mRegisteredContext = context;
+ mRegisteredHandler = Objects.requireNonNull(handler);
+ context.registerReceiverForAllUsers(this, sPackageFilt, null, mRegisteredHandler);
+ context.registerReceiverForAllUsers(this, sNonDataFilt, null, mRegisteredHandler);
+ if (externalStorage) {
+ context.registerReceiverForAllUsers(this, sExternalFilt, null, mRegisteredHandler);
+ }
+ }
+
+ void unregister() {
+ if (mRegisteredContext == null) {
+ throw new IllegalStateException("Not registered");
+ }
+ mRegisteredContext.unregisterReceiver(this);
+ mRegisteredContext = null;
+ }
+
+ // Called when some package has been changed.
+ abstract void onSomePackagesChanged();
+
+ String getPackageName(Intent intent) {
+ Uri uri = intent.getData();
+ String pkg = uri != null ? uri.getSchemeSpecificPart() : null;
+ return pkg;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mSomePackagesChanged = false;
+
+ String action = intent.getAction();
+ if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
+ // We consider something to have changed regardless of whether
+ // this is just an update, because the update is now finished
+ // and the contents of the package may have changed.
+ mSomePackagesChanged = true;
+ } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
+ String pkg = getPackageName(intent);
+ if (pkg != null) {
+ if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+ mSomePackagesChanged = true;
+ }
+ }
+ } else if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+ String pkg = getPackageName(intent);
+ if (pkg != null) {
+ mSomePackagesChanged = true;
+ }
+ } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
+ mSomePackagesChanged = true;
+ } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
+ mSomePackagesChanged = true;
+ } else if (Intent.ACTION_PACKAGES_SUSPENDED.equals(action)) {
+ mSomePackagesChanged = true;
+ } else if (Intent.ACTION_PACKAGES_UNSUSPENDED.equals(action)) {
+ mSomePackagesChanged = true;
+ }
+
+ if (mSomePackagesChanged) {
+ onSomePackagesChanged();
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitor.java b/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitor.java
new file mode 100644
index 0000000..a86af85
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitor.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.servicemonitor;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * This is exported from frameworks ServiceWatcher.
+ * A ServiceMonitor is responsible for continuously maintaining an active binding to a service
+ * selected by it's {@link ServiceProvider}. The {@link ServiceProvider} may change the service it
+ * selects over time, and the currently bound service may crash, restart, have a user change, have
+ * changes made to its package, and so on and so forth. The ServiceMonitor is responsible for
+ * maintaining the binding across all these changes.
+ *
+ * <p>Clients may invoke {@link BinderOperation}s on the ServiceMonitor, and it will make a best
+ * effort to run these on the currently bound service, but individual operations may fail (if there
+ * is no service currently bound for instance). In order to help clients maintain the correct state,
+ * clients may supply a {@link ServiceListener}, which is informed when the ServiceMonitor connects
+ * and disconnects from a service. This allows clients to bring a bound service back into a known
+ * state on connection, and then run binder operations from there. In order to help clients
+ * accomplish this, ServiceMonitor guarantees that {@link BinderOperation}s and the
+ * {@link ServiceListener} will always be run on the same thread, so that strong ordering guarantees
+ * can be established between them.
+ *
+ * There is never any guarantee of whether a ServiceMonitor is currently connected to a service, and
+ * whether any particular {@link BinderOperation} will succeed. Clients must ensure they do not rely
+ * on this, and instead use {@link ServiceListener} notifications as necessary to recover from
+ * failures.
+ */
+public interface ServiceMonitor {
+
+ /**
+ * Operation to run on a binder interface. All operations will be run on the thread used by the
+ * ServiceMonitor this is run with.
+ */
+ interface BinderOperation {
+ /** Invoked to run the operation. Run on the ServiceMonitor thread. */
+ void run(IBinder binder) throws RemoteException;
+
+ /**
+ * Invoked if {@link #run(IBinder)} could not be invoked because there was no current
+ * binding, or if {@link #run(IBinder)} threw an exception ({@link RemoteException} or
+ * {@link RuntimeException}). This callback is only intended for resource deallocation and
+ * cleanup in response to a single binder operation, it should not be used to propagate
+ * errors further. Run on the ServiceMonitor thread.
+ */
+ default void onError() {}
+ }
+
+ /**
+ * Listener for bind and unbind events. All operations will be run on the thread used by the
+ * ServiceMonitor this is run with.
+ *
+ * @param <TBoundServiceInfo> type of bound service
+ */
+ interface ServiceListener<TBoundServiceInfo extends BoundServiceInfo> {
+ /** Invoked when a service is bound. Run on the ServiceMonitor thread. */
+ void onBind(IBinder binder, TBoundServiceInfo service) throws RemoteException;
+
+ /** Invoked when a service is unbound. Run on the ServiceMonitor thread. */
+ void onUnbind();
+ }
+
+ /**
+ * A listener for when a {@link ServiceProvider} decides that the current service has changed.
+ */
+ interface ServiceChangedListener {
+ /**
+ * Should be invoked when the current service may have changed.
+ */
+ void onServiceChanged();
+ }
+
+ /**
+ * This provider encapsulates the logic of deciding what service a {@link ServiceMonitor} should
+ * be bound to at any given moment.
+ *
+ * @param <TBoundServiceInfo> type of bound service
+ */
+ interface ServiceProvider<TBoundServiceInfo extends BoundServiceInfo> {
+ /**
+ * Should return true if there exists at least one service capable of meeting the criteria
+ * of this provider. This does not imply that {@link #getServiceInfo()} will always return a
+ * non-null result, as any service may be disqualified for various reasons at any point in
+ * time. May be invoked at any time from any thread and thus should generally not have any
+ * dependency on the other methods in this interface.
+ */
+ boolean hasMatchingService();
+
+ /**
+ * Invoked when the provider should start monitoring for any changes that could result in a
+ * different service selection, and should invoke
+ * {@link ServiceChangedListener#onServiceChanged()} in that case. {@link #getServiceInfo()}
+ * may be invoked after this method is called.
+ */
+ void register(ServiceChangedListener listener);
+
+ /**
+ * Invoked when the provider should stop monitoring for any changes that could result in a
+ * different service selection, should no longer invoke
+ * {@link ServiceChangedListener#onServiceChanged()}. {@link #getServiceInfo()} will not be
+ * invoked after this method is called.
+ */
+ void unregister();
+
+ /**
+ * Must be implemented to return the current service selected by this provider. May return
+ * null if no service currently meets the criteria. Only invoked while registered.
+ */
+ @Nullable TBoundServiceInfo getServiceInfo();
+ }
+
+ /**
+ * Information on the service selected as the best option for binding.
+ */
+ class BoundServiceInfo {
+
+ protected final @Nullable String mAction;
+ protected final int mUid;
+ protected final ComponentName mComponentName;
+
+ protected BoundServiceInfo(String action, int uid, ComponentName componentName) {
+ mAction = action;
+ mUid = uid;
+ mComponentName = Objects.requireNonNull(componentName);
+ }
+
+ /** Returns the action associated with this bound service. */
+ public @Nullable String getAction() {
+ return mAction;
+ }
+
+ /** Returns the component of this bound service. */
+ public ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof BoundServiceInfo)) {
+ return false;
+ }
+
+ BoundServiceInfo that = (BoundServiceInfo) o;
+ return mUid == that.mUid
+ && Objects.equals(mAction, that.mAction)
+ && mComponentName.equals(that.mComponentName);
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(mAction, mUid, mComponentName);
+ }
+
+ @Override
+ public String toString() {
+ if (mComponentName == null) {
+ return "none";
+ } else {
+ return mUid + "/" + mComponentName.flattenToShortString();
+ }
+ }
+ }
+
+ /**
+ * Creates a new ServiceMonitor instance.
+ */
+ static <TBoundServiceInfo extends BoundServiceInfo> ServiceMonitor create(
+ Context context,
+ String tag,
+ ServiceProvider<TBoundServiceInfo> serviceProvider,
+ @Nullable ServiceListener<? super TBoundServiceInfo> serviceListener) {
+ return create(context, ForegroundThread.getHandler(), ForegroundThread.getExecutor(), tag,
+ serviceProvider, serviceListener);
+ }
+
+ /**
+ * Creates a new ServiceMonitor instance that runs on the given handler.
+ */
+ static <TBoundServiceInfo extends BoundServiceInfo> ServiceMonitor create(
+ Context context,
+ Handler handler,
+ Executor executor,
+ String tag,
+ ServiceProvider<TBoundServiceInfo> serviceProvider,
+ @Nullable ServiceListener<? super TBoundServiceInfo> serviceListener) {
+ return new ServiceMonitorImpl<>(context, handler, executor, tag, serviceProvider,
+ serviceListener);
+ }
+
+ /**
+ * Returns true if there is at least one service that the ServiceMonitor could hypothetically
+ * bind to, as selected by the {@link ServiceProvider}.
+ */
+ boolean checkServiceResolves();
+
+ /**
+ * Registers the ServiceMonitor, so that it will begin maintaining an active binding to the
+ * service selected by {@link ServiceProvider}, until {@link #unregister()} is called.
+ */
+ void register();
+
+ /**
+ * Unregisters the ServiceMonitor, so that it will release any active bindings. If the
+ * ServiceMonitor is currently bound, this will result in one final
+ * {@link ServiceListener#onUnbind()} invocation, which may happen after this method completes
+ * (but which is guaranteed to occur before any further
+ * {@link ServiceListener#onBind(IBinder, BoundServiceInfo)} invocation in response to a later
+ * call to {@link #register()}).
+ */
+ void unregister();
+
+ /**
+ * Runs the given binder operation on the currently bound service (if available). The operation
+ * will always fail if the ServiceMonitor is not currently registered.
+ */
+ void runOnBinder(BinderOperation operation);
+
+ /**
+ * Dumps ServiceMonitor information.
+ */
+ void dump(PrintWriter pw);
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitorImpl.java b/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitorImpl.java
new file mode 100644
index 0000000..d0d6c3b
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitorImpl.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.servicemonitor;
+
+import static android.content.Context.BIND_AUTO_CREATE;
+import static android.content.Context.BIND_NOT_FOREGROUND;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+import com.android.server.nearby.common.servicemonitor.ServiceMonitor.BoundServiceInfo;
+import com.android.server.nearby.common.servicemonitor.ServiceMonitor.ServiceChangedListener;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Implementation of ServiceMonitor. Keeping the implementation separate from the interface allows
+ * us to store the generic relationship between the service provider and the service listener, while
+ * hiding the generics from clients, simplifying the API.
+ */
+class ServiceMonitorImpl<TBoundServiceInfo extends BoundServiceInfo> implements ServiceMonitor,
+ ServiceChangedListener {
+
+ private static final String TAG = "ServiceMonitor";
+ private static final boolean D = Log.isLoggable(TAG, Log.DEBUG);
+ private static final long RETRY_DELAY_MS = 15 * 1000;
+
+ // This is the same as Context.BIND_NOT_VISIBLE.
+ private static final int BIND_NOT_VISIBLE = 0x40000000;
+
+ final Context mContext;
+ final Handler mHandler;
+ final Executor mExecutor;
+ final String mTag;
+ final ServiceProvider<TBoundServiceInfo> mServiceProvider;
+ final @Nullable ServiceListener<? super TBoundServiceInfo> mServiceListener;
+
+ private final PackageWatcher mPackageWatcher = new PackageWatcher() {
+ @Override
+ public void onSomePackagesChanged() {
+ onServiceChanged(false);
+ }
+ };
+
+ @GuardedBy("this")
+ private boolean mRegistered = false;
+ @GuardedBy("this")
+ private MyServiceConnection mServiceConnection = new MyServiceConnection(null);
+
+ ServiceMonitorImpl(Context context, Handler handler, Executor executor, String tag,
+ ServiceProvider<TBoundServiceInfo> serviceProvider,
+ ServiceListener<? super TBoundServiceInfo> serviceListener) {
+ mContext = context;
+ mExecutor = executor;
+ mHandler = handler;
+ mTag = tag;
+ mServiceProvider = serviceProvider;
+ mServiceListener = serviceListener;
+ }
+
+ @Override
+ public boolean checkServiceResolves() {
+ return mServiceProvider.hasMatchingService();
+ }
+
+ @Override
+ public synchronized void register() {
+ Preconditions.checkState(!mRegistered);
+
+ mRegistered = true;
+ mPackageWatcher.register(mContext, /*externalStorage=*/ true, mHandler);
+ mServiceProvider.register(this);
+
+ onServiceChanged(false);
+ }
+
+ @Override
+ public synchronized void unregister() {
+ Preconditions.checkState(mRegistered);
+
+ mServiceProvider.unregister();
+ mPackageWatcher.unregister();
+ mRegistered = false;
+
+ onServiceChanged(false);
+ }
+
+ @Override
+ public synchronized void onServiceChanged() {
+ onServiceChanged(false);
+ }
+
+ @Override
+ public synchronized void runOnBinder(BinderOperation operation) {
+ MyServiceConnection serviceConnection = mServiceConnection;
+ mHandler.post(() -> serviceConnection.runOnBinder(operation));
+ }
+
+ synchronized void onServiceChanged(boolean forceRebind) {
+ TBoundServiceInfo newBoundServiceInfo;
+ if (mRegistered) {
+ newBoundServiceInfo = mServiceProvider.getServiceInfo();
+ } else {
+ newBoundServiceInfo = null;
+ }
+
+ if (forceRebind || !Objects.equals(mServiceConnection.getBoundServiceInfo(),
+ newBoundServiceInfo)) {
+ Log.i(TAG, "[" + mTag + "] chose new implementation " + newBoundServiceInfo);
+ MyServiceConnection oldServiceConnection = mServiceConnection;
+ MyServiceConnection newServiceConnection = new MyServiceConnection(newBoundServiceInfo);
+ mServiceConnection = newServiceConnection;
+ mHandler.post(() -> {
+ oldServiceConnection.unbind();
+ newServiceConnection.bind();
+ });
+ }
+ }
+
+ @Override
+ public String toString() {
+ MyServiceConnection serviceConnection;
+ synchronized (this) {
+ serviceConnection = mServiceConnection;
+ }
+
+ return serviceConnection.getBoundServiceInfo().toString();
+ }
+
+ @Override
+ public void dump(PrintWriter pw) {
+ MyServiceConnection serviceConnection;
+ synchronized (this) {
+ serviceConnection = mServiceConnection;
+ }
+
+ pw.println("target service=" + serviceConnection.getBoundServiceInfo());
+ pw.println("connected=" + serviceConnection.isConnected());
+ }
+
+ // runs on the handler thread, and expects most of its methods to be called from that thread
+ private class MyServiceConnection implements ServiceConnection {
+
+ private final @Nullable TBoundServiceInfo mBoundServiceInfo;
+
+ // volatile so that isConnected can be called from any thread easily
+ private volatile @Nullable IBinder mBinder;
+ private @Nullable Runnable mRebinder;
+
+ MyServiceConnection(@Nullable TBoundServiceInfo boundServiceInfo) {
+ mBoundServiceInfo = boundServiceInfo;
+ }
+
+ // may be called from any thread
+ @Nullable TBoundServiceInfo getBoundServiceInfo() {
+ return mBoundServiceInfo;
+ }
+
+ // may be called from any thread
+ boolean isConnected() {
+ return mBinder != null;
+ }
+
+ void bind() {
+ Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
+
+ if (mBoundServiceInfo == null) {
+ return;
+ }
+
+ if (D) {
+ Log.d(TAG, "[" + mTag + "] binding to " + mBoundServiceInfo);
+ }
+
+ Intent bindIntent = new Intent(mBoundServiceInfo.getAction())
+ .setComponent(mBoundServiceInfo.getComponentName());
+ if (!mContext.bindService(bindIntent,
+ BIND_AUTO_CREATE | BIND_NOT_FOREGROUND | BIND_NOT_VISIBLE,
+ mExecutor, this)) {
+ Log.e(TAG, "[" + mTag + "] unexpected bind failure - retrying later");
+ mRebinder = this::bind;
+ mHandler.postDelayed(mRebinder, RETRY_DELAY_MS);
+ } else {
+ mRebinder = null;
+ }
+ }
+
+ void unbind() {
+ Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
+
+ if (mBoundServiceInfo == null) {
+ return;
+ }
+
+ if (D) {
+ Log.d(TAG, "[" + mTag + "] unbinding from " + mBoundServiceInfo);
+ }
+
+ if (mRebinder != null) {
+ mHandler.removeCallbacks(mRebinder);
+ mRebinder = null;
+ } else {
+ mContext.unbindService(this);
+ }
+
+ onServiceDisconnected(mBoundServiceInfo.getComponentName());
+ }
+
+ void runOnBinder(BinderOperation operation) {
+ Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
+
+ if (mBinder == null) {
+ operation.onError();
+ return;
+ }
+
+ try {
+ operation.run(mBinder);
+ } catch (RuntimeException | RemoteException e) {
+ // binders may propagate some specific non-RemoteExceptions from the other side
+ // through the binder as well - we cannot allow those to crash the system server
+ Log.e(TAG, "[" + mTag + "] error running operation on " + mBoundServiceInfo, e);
+ operation.onError();
+ }
+ }
+
+ @Override
+ public final void onServiceConnected(ComponentName component, IBinder binder) {
+ Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
+ Preconditions.checkState(mBinder == null);
+
+ Log.i(TAG, "[" + mTag + "] connected to " + component.toShortString());
+
+ mBinder = binder;
+
+ if (mServiceListener != null) {
+ try {
+ mServiceListener.onBind(binder, mBoundServiceInfo);
+ } catch (RuntimeException | RemoteException e) {
+ // binders may propagate some specific non-RemoteExceptions from the other side
+ // through the binder as well - we cannot allow those to crash the system server
+ Log.e(TAG, "[" + mTag + "] error running operation on " + mBoundServiceInfo, e);
+ }
+ }
+ }
+
+ @Override
+ public final void onServiceDisconnected(ComponentName component) {
+ Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
+
+ if (mBinder == null) {
+ return;
+ }
+
+ Log.i(TAG, "[" + mTag + "] disconnected from " + mBoundServiceInfo);
+
+ mBinder = null;
+ if (mServiceListener != null) {
+ mServiceListener.onUnbind();
+ }
+ }
+
+ @Override
+ public final void onBindingDied(ComponentName component) {
+ Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
+
+ Log.w(TAG, "[" + mTag + "] " + mBoundServiceInfo + " died");
+
+ // introduce a small delay to prevent spamming binding over and over, since the likely
+ // cause of a binding dying is some package event that may take time to recover from
+ mHandler.postDelayed(() -> onServiceChanged(true), 500);
+ }
+
+ @Override
+ public final void onNullBinding(ComponentName component) {
+ Log.e(TAG, "[" + mTag + "] " + mBoundServiceInfo + " has null binding");
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/Constant.java b/nearby/service/java/com/android/server/nearby/fastpair/Constant.java
new file mode 100644
index 0000000..0695b5f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/Constant.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair;
+
+/**
+ * String constant for half sheet.
+ */
+public class Constant {
+
+ /**
+ * Value represents true for {@link android.provider.Settings.Secure}
+ */
+ public static final int SETTINGS_TRUE_VALUE = 1;
+
+ /**
+ * Tag for Fast Pair service related logs.
+ */
+ public static final String TAG = "FastPairService";
+
+ public static final String EXTRA_BINDER = "com.android.server.nearby.fastpair.BINDER";
+ public static final String EXTRA_BUNDLE = "com.android.server.nearby.fastpair.BUNDLE_EXTRA";
+ public static final String ACTION_FAST_PAIR_HALF_SHEET_CANCEL =
+ "com.android.nearby.ACTION_FAST_PAIR_HALF_SHEET_CANCEL";
+ public static final String EXTRA_HALF_SHEET_INFO =
+ "com.android.nearby.halfsheet.HALF_SHEET";
+ public static final String EXTRA_HALF_SHEET_TYPE =
+ "com.android.nearby.halfsheet.HALF_SHEET_TYPE";
+ public static final String DEVICE_PAIRING_FRAGMENT_TYPE = "DEVICE_PAIRING";
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
new file mode 100644
index 0000000..2ecce47
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
@@ -0,0 +1,253 @@
+/*
+ * 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;
+
+import static com.android.server.nearby.fastpair.Constant.TAG;
+
+import static com.google.common.primitives.Bytes.concat;
+
+import android.accounts.Account;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.nearby.FastPairDevice;
+import android.nearby.NearbyDevice;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.common.ble.decode.FastPairDecoder;
+import com.android.server.nearby.common.ble.util.RangingUtils;
+import com.android.server.nearby.common.bloomfilter.BloomFilter;
+import com.android.server.nearby.common.bloomfilter.FastPairBloomFilterHasher;
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
+import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
+import com.android.server.nearby.provider.FastPairDataProvider;
+import com.android.server.nearby.util.DataUtils;
+import com.android.server.nearby.util.Hex;
+
+import java.util.List;
+
+import service.proto.Cache;
+import service.proto.Data;
+import service.proto.Rpcs;
+
+/**
+ * Handler that handle fast pair related broadcast.
+ */
+public class FastPairAdvHandler {
+ Context mContext;
+ String mBleAddress;
+ // Need to be deleted after notification manager in use.
+ private boolean mIsFirst = false;
+ private FastPairDataProvider mPairDataProvider;
+ private static final double NEARBY_DISTANCE_THRESHOLD = 0.6;
+
+ /** The types about how the bloomfilter is processed. */
+ public enum ProcessBloomFilterType {
+ IGNORE, // The bloomfilter is not handled. e.g. distance is too far away.
+ CACHE, // The bloomfilter is recognized in the local cache.
+ FOOTPRINT, // Need to check the bloomfilter from the footprints.
+ ACCOUNT_KEY_HIT // The specified account key was hit the bloom filter.
+ }
+
+ /**
+ * Constructor function.
+ */
+ public FastPairAdvHandler(Context context) {
+ mContext = context;
+ }
+
+ @VisibleForTesting
+ FastPairAdvHandler(Context context, FastPairDataProvider dataProvider) {
+ mContext = context;
+ mPairDataProvider = dataProvider;
+ }
+
+ /**
+ * Handles all of the scanner result. Fast Pair will handle model id broadcast bloomfilter
+ * broadcast and battery level broadcast.
+ */
+ public void handleBroadcast(NearbyDevice device) {
+ FastPairDevice fastPairDevice = (FastPairDevice) device;
+ mBleAddress = fastPairDevice.getBluetoothAddress();
+ if (mPairDataProvider == null) {
+ mPairDataProvider = FastPairDataProvider.getInstance();
+ }
+ if (mPairDataProvider == null) {
+ return;
+ }
+
+ if (FastPairDecoder.checkModelId(fastPairDevice.getData())) {
+ byte[] model = FastPairDecoder.getModelId(fastPairDevice.getData());
+ Log.d(TAG, "On discovery model id " + Hex.bytesToStringLowercase(model));
+ // Use api to get anti spoofing key from model id.
+ try {
+ List<Account> accountList = mPairDataProvider.loadFastPairEligibleAccounts();
+ Rpcs.GetObservedDeviceResponse response =
+ mPairDataProvider.loadFastPairAntispoofKeyDeviceMetadata(model);
+ if (response == null) {
+ Log.e(TAG, "server does not have model id "
+ + Hex.bytesToStringLowercase(model));
+ return;
+ }
+ // Check the distance of the device if the distance is larger than the threshold
+ // do not show half sheet.
+ if (!isNearby(fastPairDevice.getRssi(),
+ response.getDevice().getBleTxPower() == 0 ? fastPairDevice.getTxPower()
+ : response.getDevice().getBleTxPower())) {
+ return;
+ }
+ Locator.get(mContext, FastPairHalfSheetManager.class).showHalfSheet(
+ DataUtils.toScanFastPairStoreItem(
+ response, mBleAddress,
+ accountList.isEmpty() ? null : accountList.get(0).name));
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "OEM does not construct fast pair data proxy correctly");
+ }
+ } else {
+ // Start to process bloom filter
+ try {
+ List<Account> accountList = mPairDataProvider.loadFastPairEligibleAccounts();
+ byte[] bloomFilterByteArray = FastPairDecoder
+ .getBloomFilter(fastPairDevice.getData());
+ byte[] bloomFilterSalt = FastPairDecoder
+ .getBloomFilterSalt(fastPairDevice.getData());
+ if (bloomFilterByteArray == null || bloomFilterByteArray.length == 0) {
+ return;
+ }
+ for (Account account : accountList) {
+ List<Data.FastPairDeviceWithAccountKey> listDevices =
+ mPairDataProvider.loadFastPairDeviceWithAccountKey(account);
+ Data.FastPairDeviceWithAccountKey recognizedDevice =
+ findRecognizedDevice(listDevices,
+ new BloomFilter(bloomFilterByteArray,
+ new FastPairBloomFilterHasher()), bloomFilterSalt);
+
+ if (recognizedDevice != null) {
+ Log.d(TAG, "find matched device show notification to remind"
+ + " user to pair");
+ // Check the distance of the device if the distance is larger than the
+ // threshold
+ // do not show half sheet.
+ if (!isNearby(fastPairDevice.getRssi(),
+ recognizedDevice.getDiscoveryItem().getTxPower() == 0
+ ? fastPairDevice.getTxPower()
+ : recognizedDevice.getDiscoveryItem().getTxPower())) {
+ return;
+ }
+ // Check if the device is already paired
+ List<Cache.StoredFastPairItem> storedFastPairItemList =
+ Locator.get(mContext, FastPairCacheManager.class)
+ .getAllSavedStoredFastPairItem();
+ Cache.StoredFastPairItem recognizedStoredFastPairItem =
+ findRecognizedDeviceFromCachedItem(storedFastPairItemList,
+ new BloomFilter(bloomFilterByteArray,
+ new FastPairBloomFilterHasher()), bloomFilterSalt);
+ if (recognizedStoredFastPairItem != null) {
+ // The bloomfilter is recognized in the cache so the device is paired
+ // before
+ Log.d(TAG, "bloom filter is recognized in the cache");
+ continue;
+ } else {
+ Log.d(TAG, "bloom filter is recognized not paired before should"
+ + "show subsequent pairing notification");
+ if (mIsFirst) {
+ mIsFirst = false;
+ // Get full info from api the initial request will only return
+ // part of the info due to size limit.
+ List<Data.FastPairDeviceWithAccountKey> resList =
+ mPairDataProvider.loadFastPairDeviceWithAccountKey(account,
+ List.of(recognizedDevice.getAccountKey()
+ .toByteArray()));
+ if (resList != null && resList.size() > 0) {
+ //Saved device from footprint does not have ble address so
+ // fill ble address with current scan result.
+ Cache.StoredDiscoveryItem storedDiscoveryItem =
+ resList.get(0).getDiscoveryItem().toBuilder()
+ .setMacAddress(
+ fastPairDevice.getBluetoothAddress())
+ .build();
+ Locator.get(mContext, FastPairController.class).pair(
+ new DiscoveryItem(mContext, storedDiscoveryItem),
+ resList.get(0).getAccountKey().toByteArray(),
+ /** companionApp=*/null);
+ }
+ }
+ }
+
+ return;
+ }
+ }
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "OEM does not construct fast pair data proxy correctly");
+ }
+
+ }
+ }
+
+ /**
+ * Checks the bloom filter to see if any of the devices are recognized and should have a
+ * notification displayed for them. A device is recognized if the account key + salt combination
+ * is inside the bloom filter.
+ */
+ @Nullable
+ static Data.FastPairDeviceWithAccountKey findRecognizedDevice(
+ List<Data.FastPairDeviceWithAccountKey> devices, BloomFilter bloomFilter, byte[] salt) {
+ Log.d(TAG, "saved devices size in the account is " + devices.size());
+ for (Data.FastPairDeviceWithAccountKey device : devices) {
+ if (device.getAccountKey().toByteArray() == null || salt == null) {
+ return null;
+ }
+ byte[] rotatedKey = concat(device.getAccountKey().toByteArray(), salt);
+ StringBuilder sb = new StringBuilder();
+ for (byte b : rotatedKey) {
+ sb.append(b);
+ }
+ if (bloomFilter.possiblyContains(rotatedKey)) {
+ Log.d(TAG, "match " + sb.toString());
+ return device;
+ } else {
+ Log.d(TAG, "not match " + sb.toString());
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ static Cache.StoredFastPairItem findRecognizedDeviceFromCachedItem(
+ List<Cache.StoredFastPairItem> devices, BloomFilter bloomFilter, byte[] salt) {
+ for (Cache.StoredFastPairItem device : devices) {
+ if (device.getAccountKey().toByteArray() == null || salt == null) {
+ return null;
+ }
+ byte[] rotatedKey = concat(device.getAccountKey().toByteArray(), salt);
+ if (bloomFilter.possiblyContains(rotatedKey)) {
+ return device;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Check the device distance for certain rssi value.
+ */
+ boolean isNearby(int rssi, int txPower) {
+ return RangingUtils.distanceFromRssiAndTxPower(rssi, txPower) < NEARBY_DISTANCE_THRESHOLD;
+ }
+
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java
new file mode 100644
index 0000000..e1db7e5
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair;
+
+import static com.google.common.primitives.Bytes.concat;
+
+import android.accounts.Account;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.nearby.FastPairDevice;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress;
+import com.android.server.nearby.common.eventloop.Annotations;
+import com.android.server.nearby.common.eventloop.EventLoop;
+import com.android.server.nearby.common.eventloop.NamedRunnable;
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
+import com.android.server.nearby.fastpair.footprint.FastPairUploadInfo;
+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.pairinghandler.PairingProgressHandlerBase;
+import com.android.server.nearby.provider.FastPairDataProvider;
+
+import com.google.common.hash.Hashing;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import service.proto.Cache;
+
+/**
+ * FastPair controller after get the info from intent handler Fast Pair controller is responsible
+ * for pairing control.
+ */
+public class FastPairController {
+ private static final String TAG = "FastPairController";
+ private final Context mContext;
+ private final EventLoop mEventLoop;
+ private final FastPairCacheManager mFastPairCacheManager;
+ private final FootprintsDeviceManager mFootprintsDeviceManager;
+ private boolean mIsFastPairing = false;
+ // boolean flag whether upload to footprint or not.
+ private boolean mShouldUpload = false;
+ @Nullable
+ private Callback mCallback;
+
+ public FastPairController(Context context) {
+ mContext = context;
+ mEventLoop = Locator.get(mContext, EventLoop.class);
+ mFastPairCacheManager = Locator.get(mContext, FastPairCacheManager.class);
+ mFootprintsDeviceManager = Locator.get(mContext, FootprintsDeviceManager.class);
+ }
+
+ /**
+ * Should be called on create lifecycle.
+ */
+ @WorkerThread
+ public void onCreate() {
+ mEventLoop.postRunnable(new NamedRunnable("FastPairController::InitializeScanner") {
+ @Override
+ public void run() {
+ // init scanner here and start scan.
+ }
+ });
+ }
+
+ /**
+ * Should be called on destroy lifecycle.
+ */
+ @WorkerThread
+ public void onDestroy() {
+ mEventLoop.postRunnable(new NamedRunnable("FastPairController::DestroyScanner") {
+ @Override
+ public void run() {
+ // Unregister scanner from here
+ }
+ });
+ }
+
+ /**
+ * Pairing function.
+ */
+ public void pair(FastPairDevice fastPairDevice) {
+ byte[] discoveryItem = fastPairDevice.getData();
+ String modelId = fastPairDevice.getModelId();
+
+ Log.v(TAG, "pair: fastPairDevice " + fastPairDevice);
+ mEventLoop.postRunnable(
+ new NamedRunnable("fastPairWith=" + modelId) {
+ @Override
+ public void run() {
+ try {
+ DiscoveryItem item = new DiscoveryItem(mContext,
+ Cache.StoredDiscoveryItem.parseFrom(discoveryItem));
+ if (TextUtils.isEmpty(item.getMacAddress())) {
+ Log.w(TAG, "There is no mac address in the DiscoveryItem,"
+ + " ignore pairing");
+ return;
+ }
+ // Check enabled state to prevent multiple pair attempts if we get the
+ // intent more than once (this can happen due to an Android platform
+ // bug - b/31459521).
+ if (item.getState()
+ != Cache.StoredDiscoveryItem.State.STATE_ENABLED) {
+ Log.d(TAG, "Incorrect state, ignore pairing");
+ return;
+ }
+ boolean useLargeNotifications =
+ item.getAuthenticationPublicKeySecp256R1() != null;
+ FastPairNotificationManager fastPairNotificationManager =
+ new FastPairNotificationManager(mContext, item,
+ useLargeNotifications);
+ FastPairHalfSheetManager fastPairHalfSheetManager =
+ Locator.get(mContext, FastPairHalfSheetManager.class);
+ mFastPairCacheManager.saveDiscoveryItem(item);
+
+ PairingProgressHandlerBase pairingProgressHandlerBase =
+ PairingProgressHandlerBase.create(
+ mContext,
+ item,
+ /* companionApp= */ null,
+ /* accountKey= */ null,
+ mFootprintsDeviceManager,
+ fastPairNotificationManager,
+ fastPairHalfSheetManager,
+ /* isRetroactivePair= */ false);
+
+ pair(item,
+ /* accountKey= */ null,
+ /* companionApp= */ null,
+ pairingProgressHandlerBase);
+ } catch (InvalidProtocolBufferException e) {
+ Log.w(TAG,
+ "Error parsing serialized discovery item with size "
+ + discoveryItem.length);
+ }
+ }
+ });
+ }
+
+ /**
+ * Subsequent pairing entry.
+ */
+ public void pair(DiscoveryItem item,
+ @Nullable byte[] accountKey,
+ @Nullable String companionApp) {
+ FastPairNotificationManager fastPairNotificationManager =
+ new FastPairNotificationManager(mContext, item, false);
+ FastPairHalfSheetManager fastPairHalfSheetManager =
+ Locator.get(mContext, FastPairHalfSheetManager.class);
+ PairingProgressHandlerBase pairingProgressHandlerBase =
+ PairingProgressHandlerBase.create(
+ mContext,
+ item,
+ /* companionApp= */ null,
+ /* accountKey= */ accountKey,
+ mFootprintsDeviceManager,
+ fastPairNotificationManager,
+ fastPairHalfSheetManager,
+ /* isRetroactivePair= */ false);
+ pair(item, accountKey, companionApp, pairingProgressHandlerBase);
+ }
+ /**
+ * Pairing function
+ */
+ @Annotations.EventThread
+ public void pair(
+ DiscoveryItem item,
+ @Nullable byte[] accountKey,
+ @Nullable String companionApp,
+ PairingProgressHandlerBase pairingProgressHandlerBase) {
+ if (mIsFastPairing) {
+ Log.d(TAG, "FastPair: fastpairing, skip pair request");
+ return;
+ }
+ mIsFastPairing = true;
+ Log.d(TAG, "FastPair: start pair");
+
+ // Hide all "tap to pair" notifications until after the flow completes.
+ mEventLoop.removeRunnable(mReEnableAllDeviceItemsRunnable);
+ if (mCallback != null) {
+ mCallback.fastPairUpdateDeviceItemsEnabled(false);
+ }
+
+ Future<Void> task =
+ FastPairManager.pair(
+ Executors.newSingleThreadExecutor(),
+ mContext,
+ item,
+ accountKey,
+ companionApp,
+ mFootprintsDeviceManager,
+ pairingProgressHandlerBase);
+ mIsFastPairing = false;
+ }
+
+ /** Fixes a companion app package name with extra spaces. */
+ private static String trimCompanionApp(String companionApp) {
+ return companionApp == null ? null : companionApp.trim();
+ }
+
+ /**
+ * Function to handle when scanner find bloomfilter.
+ */
+ @Annotations.EventThread
+ public FastPairAdvHandler.ProcessBloomFilterType onBloomFilterDetect(FastPairAdvHandler handler,
+ boolean advertiseInRange) {
+ if (mIsFastPairing) {
+ return FastPairAdvHandler.ProcessBloomFilterType.IGNORE;
+ }
+ // Check if the device is in the cache or footprint.
+ return FastPairAdvHandler.ProcessBloomFilterType.CACHE;
+ }
+
+ /**
+ * Add newly paired device info to footprint
+ */
+ @WorkerThread
+ public void addDeviceToFootprint(String publicAddress, byte[] accountKey,
+ DiscoveryItem discoveryItem) {
+ if (!mShouldUpload) {
+ return;
+ }
+ Log.d(TAG, "upload device to footprint");
+ FastPairManager.processBackgroundTask(() -> {
+ Cache.StoredDiscoveryItem storedDiscoveryItem =
+ prepareStoredDiscoveryItemForFootprints(discoveryItem);
+ byte[] hashValue =
+ Hashing.sha256()
+ .hashBytes(
+ concat(accountKey, BluetoothAddress.decode(publicAddress)))
+ .asBytes();
+ FastPairUploadInfo uploadInfo =
+ new FastPairUploadInfo(storedDiscoveryItem, ByteString.copyFrom(accountKey),
+ ByteString.copyFrom(hashValue));
+ // account data place holder here
+ try {
+ FastPairDataProvider fastPairDataProvider = FastPairDataProvider.getInstance();
+ if (fastPairDataProvider == null) {
+ return;
+ }
+ List<Account> accountList = fastPairDataProvider.loadFastPairEligibleAccounts();
+ if (accountList.size() > 0) {
+ fastPairDataProvider.optIn(accountList.get(0));
+ fastPairDataProvider.upload(accountList.get(0), uploadInfo);
+ }
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "OEM does not construct fast pair data proxy correctly");
+ }
+ });
+ }
+
+ @Nullable
+ private Cache.StoredDiscoveryItem getStoredDiscoveryItemFromAddressForFootprints(
+ String bleAddress) {
+
+ List<DiscoveryItem> discoveryItems = new ArrayList<>();
+ //cacheManager.getAllDiscoveryItems();
+ for (DiscoveryItem discoveryItem : discoveryItems) {
+ if (bleAddress.equals(discoveryItem.getMacAddress())) {
+ return prepareStoredDiscoveryItemForFootprints(discoveryItem);
+ }
+ }
+ return null;
+ }
+
+ static Cache.StoredDiscoveryItem prepareStoredDiscoveryItemForFootprints(
+ DiscoveryItem discoveryItem) {
+ Cache.StoredDiscoveryItem.Builder storedDiscoveryItem =
+ discoveryItem.getCopyOfStoredItem().toBuilder();
+ // Strip the mac address so we aren't storing it in the cloud and ensure the item always
+ // starts as enabled and in a good state.
+ storedDiscoveryItem.clearMacAddress();
+
+ return storedDiscoveryItem.build();
+ }
+
+ /**
+ * FastPairConnection will check whether write account key result if the account key is
+ * generated change the parameter.
+ */
+ public void setShouldUpload(boolean shouldUpload) {
+ mShouldUpload = shouldUpload;
+ }
+
+ private final NamedRunnable mReEnableAllDeviceItemsRunnable =
+ new NamedRunnable("reEnableAllDeviceItems") {
+ @Override
+ public void run() {
+ if (mCallback != null) {
+ mCallback.fastPairUpdateDeviceItemsEnabled(true);
+ }
+ }
+ };
+
+ interface Callback {
+ void fastPairUpdateDeviceItemsEnabled(boolean enabled);
+ }
+}
\ No newline at end of file
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
new file mode 100644
index 0000000..f368080
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
@@ -0,0 +1,464 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair;
+
+import static com.android.server.nearby.fastpair.Constant.TAG;
+
+import android.annotation.Nullable;
+import android.annotation.WorkerThread;
+import android.app.KeyguardManager;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.nearby.FastPairDevice;
+import android.nearby.NearbyDevice;
+import android.nearby.NearbyManager;
+import android.nearby.ScanCallback;
+import android.nearby.ScanRequest;
+import android.net.Uri;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.nearby.common.ble.decode.FastPairDecoder;
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
+import com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection;
+import com.android.server.nearby.common.bluetooth.fastpair.PairingException;
+import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
+import com.android.server.nearby.common.bluetooth.fastpair.ReflectionException;
+import com.android.server.nearby.common.bluetooth.fastpair.SimpleBroadcastReceiver;
+import com.android.server.nearby.common.eventloop.Annotations;
+import com.android.server.nearby.common.eventloop.EventLoop;
+import com.android.server.nearby.common.eventloop.NamedRunnable;
+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.pairinghandler.PairingProgressHandlerBase;
+import com.android.server.nearby.util.ForegroundThread;
+import com.android.server.nearby.util.Hex;
+
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.ByteString;
+
+import java.security.GeneralSecurityException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import service.proto.Cache;
+import service.proto.Rpcs;
+
+/**
+ * FastPairManager is the class initiated in nearby service to handle Fast Pair related
+ * work.
+ */
+
+public class FastPairManager {
+
+ private static final String ACTION_PREFIX = UserActionHandler.PREFIX;
+ private static final int WAIT_FOR_UNLOCK_MILLIS = 5000;
+
+ /** 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";
+
+ private static Executor sFastPairExecutor;
+
+ private ContentObserver mFastPairScanChangeContentObserver = null;
+
+ final LocatorContextWrapper mLocatorContextWrapper;
+ final IntentFilter mIntentFilter;
+ final Locator mLocator;
+ private boolean mScanEnabled;
+
+ private final BroadcastReceiver mScreenBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)
+ || intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
+ Log.d(TAG, "onReceive: ACTION_SCREEN_ON or boot complete.");
+ invalidateScan();
+ } else if (intent.getAction().equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) {
+ processBluetoothConnectionEvent(intent);
+ }
+ }
+ };
+
+ public FastPairManager(LocatorContextWrapper contextWrapper) {
+ mLocatorContextWrapper = contextWrapper;
+ mIntentFilter = new IntentFilter();
+ mLocator = mLocatorContextWrapper.getLocator();
+ mLocator.bind(new FastPairModule());
+ Rpcs.GetObservedDeviceResponse getObservedDeviceResponse =
+ Rpcs.GetObservedDeviceResponse.newBuilder().build();
+ }
+
+ final ScanCallback mScanCallback = new ScanCallback() {
+ @Override
+ public void onDiscovered(@NonNull NearbyDevice device) {
+ Locator.get(mLocatorContextWrapper, FastPairAdvHandler.class).handleBroadcast(device);
+ }
+
+ @Override
+ public void onUpdated(@NonNull NearbyDevice device) {
+ FastPairDevice fastPairDevice = (FastPairDevice) device;
+ byte[] modelArray = FastPairDecoder.getModelId(fastPairDevice.getData());
+ Log.d(TAG, "update model id" + Hex.bytesToStringLowercase(modelArray));
+ }
+
+ @Override
+ public void onLost(@NonNull NearbyDevice device) {
+ FastPairDevice fastPairDevice = (FastPairDevice) device;
+ byte[] modelArray = FastPairDecoder.getModelId(fastPairDevice.getData());
+ Log.d(TAG, "lost model id" + Hex.bytesToStringLowercase(modelArray));
+ }
+ };
+
+ /**
+ * Function called when nearby service start.
+ */
+ public void initiate() {
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+ mIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ mIntentFilter.addAction(Intent.ACTION_BOOT_COMPLETED);
+
+ mLocatorContextWrapper.getContext()
+ .registerReceiver(mScreenBroadcastReceiver, mIntentFilter);
+
+ Locator.getFromContextWrapper(mLocatorContextWrapper, FastPairCacheManager.class);
+ // Default false for now.
+ mScanEnabled = NearbyManager.getFastPairScanEnabled(mLocatorContextWrapper.getContext());
+ registerFastPairScanChangeContentObserver(mLocatorContextWrapper.getContentResolver());
+ }
+
+ /**
+ * Function to free up fast pair resource.
+ */
+ public void cleanUp() {
+ mLocatorContextWrapper.getContext().unregisterReceiver(mScreenBroadcastReceiver);
+ if (mFastPairScanChangeContentObserver != null) {
+ mLocatorContextWrapper.getContentResolver().unregisterContentObserver(
+ mFastPairScanChangeContentObserver);
+ }
+ }
+
+ /**
+ * Starts fast pair process.
+ */
+ @Annotations.EventThread
+ public static Future<Void> pair(
+ ExecutorService executor,
+ Context context,
+ DiscoveryItem item,
+ @Nullable byte[] accountKey,
+ @Nullable String companionApp,
+ FootprintsDeviceManager footprints,
+ PairingProgressHandlerBase pairingProgressHandlerBase) {
+ return executor.submit(
+ () -> pairInternal(context, item, companionApp, accountKey, footprints,
+ pairingProgressHandlerBase), /* result= */ null);
+ }
+
+ /**
+ * Starts fast pair
+ */
+ @WorkerThread
+ public static void pairInternal(
+ Context context,
+ DiscoveryItem item,
+ @Nullable String companionApp,
+ @Nullable byte[] accountKey,
+ FootprintsDeviceManager footprints,
+ PairingProgressHandlerBase pairingProgressHandlerBase) {
+ FastPairHalfSheetManager fastPairHalfSheetManager =
+ Locator.get(context, FastPairHalfSheetManager.class);
+ try {
+ pairingProgressHandlerBase.onPairingStarted();
+ if (pairingProgressHandlerBase.skipWaitingScreenUnlock()) {
+ // Do nothing due to we are not showing the status notification in some pairing
+ // types, e.g. the retroactive pairing.
+ } else {
+ // If the screen is locked when the user taps to pair, the screen will unlock. We
+ // must wait for the unlock to complete before showing the status notification, or
+ // it won't be heads-up.
+ pairingProgressHandlerBase.onWaitForScreenUnlock();
+ waitUntilScreenIsUnlocked(context);
+ pairingProgressHandlerBase.onScreenUnlocked();
+ }
+ BluetoothAdapter bluetoothAdapter = getBluetoothAdapter(context);
+
+ boolean isBluetoothEnabled = bluetoothAdapter != null && bluetoothAdapter.isEnabled();
+ if (!isBluetoothEnabled) {
+ if (bluetoothAdapter == null || !bluetoothAdapter.enable()) {
+ Log.d(TAG, "FastPair: Failed to enable bluetooth");
+ return;
+ }
+ Log.v(TAG, "FastPair: Enabling bluetooth for fast pair");
+
+ Locator.get(context, EventLoop.class)
+ .postRunnable(
+ new NamedRunnable("enableBluetoothToast") {
+ @Override
+ public void run() {
+ Log.d(TAG, "Enable bluetooth toast test");
+ }
+ });
+ // Set up call back to call this function again once bluetooth has been
+ // enabled; this does not seem to be a problem as the device connects without a
+ // problem, but in theory the timeout also includes turning on bluetooth now.
+ }
+
+ pairingProgressHandlerBase.onReadyToPair();
+
+ String modelId = item.getTriggerId();
+ Preferences.Builder prefsBuilder =
+ Preferences.builderFromGmsLog()
+ .setEnableBrEdrHandover(false)
+ .setIgnoreDiscoveryError(true);
+ pairingProgressHandlerBase.onSetupPreferencesBuilder(prefsBuilder);
+ if (item.getFastPairInformation() != null) {
+ prefsBuilder.setSkipConnectingProfiles(
+ item.getFastPairInformation().getDataOnlyConnection());
+ }
+ // When add watch and auto device needs to change the config
+ prefsBuilder.setRejectMessageAccess(true);
+ prefsBuilder.setRejectPhonebookAccess(true);
+ prefsBuilder.setHandlePasskeyConfirmationByUi(false);
+
+ FastPairConnection connection = new FastPairDualConnection(
+ context, item.getMacAddress(),
+ prefsBuilder.build(),
+ null);
+ pairingProgressHandlerBase.onPairingSetupCompleted();
+
+ FastPairConnection.SharedSecret sharedSecret;
+ if ((accountKey != null || item.getAuthenticationPublicKeySecp256R1() != null)) {
+ sharedSecret =
+ connection.pair(
+ accountKey != null ? accountKey
+ : item.getAuthenticationPublicKeySecp256R1());
+ if (accountKey == null) {
+ // Account key is null so it is initial pairing
+ if (sharedSecret != null) {
+ Locator.get(context, FastPairController.class).addDeviceToFootprint(
+ connection.getPublicAddress(), sharedSecret.getKey(), item);
+ cacheFastPairDevice(context, connection.getPublicAddress(),
+ sharedSecret.getKey(), item);
+ }
+ }
+ } else {
+ // Fast Pair one
+ connection.pair();
+ }
+ // TODO(b/213373051): Merge logic with pairingProgressHandlerBase or delete the
+ // pairingProgressHandlerBase class.
+ fastPairHalfSheetManager.showPairingSuccessHalfSheet(connection.getPublicAddress());
+ pairingProgressHandlerBase.onPairingSuccess(connection.getPublicAddress());
+ } catch (BluetoothException
+ | InterruptedException
+ | ReflectionException
+ | TimeoutException
+ | ExecutionException
+ | PairingException
+ | GeneralSecurityException e) {
+ Log.e(TAG, "Failed to pair.", e);
+
+ // TODO(b/213373051): Merge logic with pairingProgressHandlerBase or delete the
+ // pairingProgressHandlerBase class.
+ fastPairHalfSheetManager.showPairingFailed();
+ pairingProgressHandlerBase.onPairingFailed(e);
+ }
+ }
+
+ private static void cacheFastPairDevice(Context context, String publicAddress, byte[] key,
+ DiscoveryItem item) {
+ try {
+ Locator.get(context, EventLoop.class).postAndWait(
+ new NamedRunnable("FastPairCacheDevice") {
+ @Override
+ public void run() {
+ Cache.StoredFastPairItem storedFastPairItem =
+ Cache.StoredFastPairItem.newBuilder()
+ .setMacAddress(publicAddress)
+ .setAccountKey(ByteString.copyFrom(key))
+ .setModelId(item.getTriggerId())
+ .addAllFeatures(item.getFastPairInformation() == null
+ ? ImmutableList.of() :
+ item.getFastPairInformation().getFeaturesList())
+ .setDiscoveryItem(item.getCopyOfStoredItem())
+ .build();
+ Locator.get(context, FastPairCacheManager.class)
+ .putStoredFastPairItem(storedFastPairItem);
+ }
+ }
+ );
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Fail to insert paired device into cache");
+ }
+ }
+
+ /** Checks if the pairing is initial pairing with fast pair 2.0 design. */
+ public static boolean isThroughFastPair2InitialPairing(
+ DiscoveryItem item, @Nullable byte[] accountKey) {
+ return accountKey == null && item.getAuthenticationPublicKeySecp256R1() != null;
+ }
+
+ private static void waitUntilScreenIsUnlocked(Context context)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
+
+ // KeyguardManager's isScreenLocked() counterintuitively returns false when the lock screen
+ // is showing if the user has set "swipe to unlock" (i.e. no required password, PIN, or
+ // pattern) So we use this method instead, which returns true when on the lock screen
+ // regardless.
+ if (keyguardManager.isKeyguardLocked()) {
+ Log.v(TAG, "FastPair: Screen is locked, waiting until unlocked "
+ + "to show status notifications.");
+ try (SimpleBroadcastReceiver isUnlockedReceiver =
+ SimpleBroadcastReceiver.oneShotReceiver(
+ context, FlagUtils.getPreferencesBuilder().build(),
+ Intent.ACTION_USER_PRESENT)) {
+ isUnlockedReceiver.await(WAIT_FOR_UNLOCK_MILLIS, TimeUnit.MILLISECONDS);
+ }
+ }
+ }
+
+ private void registerFastPairScanChangeContentObserver(ContentResolver resolver) {
+ mFastPairScanChangeContentObserver = new ContentObserver(ForegroundThread.getHandler()) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ super.onChange(selfChange, uri);
+ setScanEnabled(
+ NearbyManager.getFastPairScanEnabled(mLocatorContextWrapper.getContext()));
+ }
+ };
+ try {
+ resolver.registerContentObserver(
+ Settings.Secure.getUriFor(NearbyManager.FAST_PAIR_SCAN_ENABLED),
+ /* notifyForDescendants= */ false,
+ mFastPairScanChangeContentObserver);
+ } catch (SecurityException e) {
+ Log.e(TAG, "Failed to register content observer for fast pair scan.", e);
+ }
+ }
+
+ /**
+ * Processed task in a background thread
+ */
+ @Annotations.EventThread
+ public static void processBackgroundTask(Runnable runnable) {
+ getExecutor().execute(runnable);
+ }
+
+ /**
+ * This function should only be called on main thread since there is no lock
+ */
+ private static Executor getExecutor() {
+ if (sFastPairExecutor != null) {
+ return sFastPairExecutor;
+ }
+ sFastPairExecutor = Executors.newSingleThreadExecutor();
+ return sFastPairExecutor;
+ }
+
+ /**
+ * Null when the Nearby Service is not available.
+ */
+ @Nullable
+ private NearbyManager getNearbyManager() {
+ return (NearbyManager) mLocatorContextWrapper
+ .getApplicationContext().getSystemService(Context.NEARBY_SERVICE);
+ }
+ private void setScanEnabled(boolean scanEnabled) {
+ if (mScanEnabled == scanEnabled) {
+ return;
+ }
+ mScanEnabled = scanEnabled;
+ invalidateScan();
+ }
+
+ /**
+ * Starts or stops scanning according to mAllowScan value.
+ */
+ private void invalidateScan() {
+ NearbyManager nearbyManager = getNearbyManager();
+ if (nearbyManager == null) {
+ Log.w(TAG, "invalidateScan: "
+ + "failed to start or stop scannning because NearbyManager is null.");
+ return;
+ }
+ if (mScanEnabled) {
+ Log.v(TAG, "invalidateScan: scan is enabled");
+ nearbyManager.startScan(new ScanRequest.Builder()
+ .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR).build(),
+ ForegroundThread.getExecutor(),
+ mScanCallback);
+ } else {
+ Log.v(TAG, "invalidateScan: scan is disabled");
+ nearbyManager.stopScan(mScanCallback);
+ }
+ }
+
+ /**
+ * When certain device is forgotten we need to remove the info from database because the info
+ * is no longer useful.
+ */
+ private void processBluetoothConnectionEvent(Intent intent) {
+ int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
+ BluetoothDevice.ERROR);
+ if (bondState == BluetoothDevice.BOND_NONE) {
+ BluetoothDevice device =
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ if (device != null) {
+ Log.d("FastPairService", "Forget device detect");
+ processBackgroundTask(new Runnable() {
+ @Override
+ public void run() {
+ mLocatorContextWrapper.getLocator().get(FastPairCacheManager.class)
+ .removeStoredFastPairItem(device.getAddress());
+ }
+ });
+ }
+
+ }
+ }
+
+ /**
+ * Helper function to get bluetooth adapter.
+ */
+ @Nullable
+ public static BluetoothAdapter getBluetoothAdapter(Context context) {
+ BluetoothManager manager = context.getSystemService(BluetoothManager.class);
+ return manager == null ? null : manager.getAdapter();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairModule.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairModule.java
new file mode 100644
index 0000000..d7946d1
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairModule.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair;
+
+import android.content.Context;
+
+import com.android.server.nearby.common.eventloop.EventLoop;
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.common.locator.Module;
+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 java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneId;
+
+/**
+ * Module that associates all of the fast pair related singleton class
+ */
+public class FastPairModule extends Module {
+ /**
+ * Initiate the class that needs to be singleton.
+ */
+ @Override
+ public void configure(Context context, Class<?> type, Locator locator) {
+ if (type.equals(FastPairCacheManager.class)) {
+ locator.bind(FastPairCacheManager.class, new FastPairCacheManager(context));
+ } else if (type.equals(FootprintsDeviceManager.class)) {
+ locator.bind(FootprintsDeviceManager.class, new FootprintsDeviceManager());
+ } else if (type.equals(EventLoop.class)) {
+ locator.bind(EventLoop.class, EventLoop.newInstance("NearbyFastPair"));
+ } else if (type.equals(FastPairController.class)) {
+ locator.bind(FastPairController.class, new FastPairController(context));
+ } else if (type.equals(FastPairCacheManager.class)) {
+ locator.bind(FastPairCacheManager.class, new FastPairCacheManager(context));
+ } else if (type.equals(FastPairHalfSheetManager.class)) {
+ locator.bind(FastPairHalfSheetManager.class, new FastPairHalfSheetManager(context));
+ } else if (type.equals(FastPairAdvHandler.class)) {
+ locator.bind(FastPairAdvHandler.class, new FastPairAdvHandler(context));
+ } else if (type.equals(Clock.class)) {
+ locator.bind(Clock.class, new Clock() {
+ @Override
+ public ZoneId getZone() {
+ return null;
+ }
+
+ @Override
+ public Clock withZone(ZoneId zone) {
+ return null;
+ }
+
+ @Override
+ public Instant instant() {
+ return null;
+ }
+ });
+ }
+
+ }
+
+ /**
+ * Clean up the singleton classes.
+ */
+ @Override
+ public void destroy(Context context, Class<?> type, Object instance) {
+ super.destroy(context, type, instance);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FlagUtils.java b/nearby/service/java/com/android/server/nearby/fastpair/FlagUtils.java
new file mode 100644
index 0000000..883a1f8
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FlagUtils.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair;
+
+import android.text.TextUtils;
+
+import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * This is fast pair connection preference
+ */
+public class FlagUtils {
+ private static final int GATT_OPERATION_TIME_OUT_SECOND = 10;
+ private static final int GATT_CONNECTION_TIME_OUT_SECOND = 15;
+ private static final int BLUETOOTH_TOGGLE_TIME_OUT_SECOND = 10;
+ private static final int BLUETOOTH_TOGGLE_SLEEP_TIME_OUT_SECOND = 2;
+ private static final int CLASSIC_DISCOVERY_TIME_OUT_SECOND = 13;
+ private static final int NUM_DISCOVER_ATTEMPTS = 3;
+ private static final int DISCOVERY_RETRY_SLEEP_SECONDS = 1;
+ private static final int SDP_TIME_OUT_SECONDS = 10;
+ private static final int NUM_SDP_ATTEMPTS = 0;
+ private static final int NUM_CREATED_BOND_ATTEMPTS = 3;
+ private static final int NUM_CONNECT_ATTEMPT = 2;
+ private static final int NUM_WRITE_ACCOUNT_KEY_ATTEMPT = 3;
+ private static final boolean TOGGLE_BLUETOOTH_ON_FAILURE = false;
+ private static final boolean BLUETOOTH_STATE_POOLING = true;
+ private static final int BLUETOOTH_STATE_POOLING_MILLIS = 1000;
+ private static final int NUM_ATTEMPTS = 2;
+ private static final short BREDR_HANDOVER_DATA_CHARACTERISTIC_ID = 11265; // 0x2c01
+ private static final short BLUETOOTH_SIG_DATA_CHARACTERISTIC_ID = 11266; // 0x2c02
+ private static final short TRANSPORT_BLOCK_DATA_CHARACTERISTIC_ID = 11267; // 0x2c03
+ private static final boolean WAIT_FOR_UUID_AFTER_BONDING = true;
+ private static final boolean RECEIVE_UUID_AND_BONDED_EVENT_BEFORE_CLOSE = true;
+ private static final int REMOVE_BOND_TIME_OUT_SECONDS = 5;
+ private static final int REMOVE_BOND_SLEEP_MILLIS = 1000;
+ private static final int CREATE_BOND_TIME_OUT_SECONDS = 15;
+ private static final int HIDE_CREATED_BOND_TIME_OUT_SECONDS = 40;
+ private static final int PROXY_TIME_OUT_SECONDS = 2;
+ private static final boolean REJECT_ACCESS = false;
+ private static final boolean ACCEPT_PASSKEY = true;
+ private static final int WRITE_ACCOUNT_KEY_SLEEP_MILLIS = 2000;
+ private static final boolean PROVIDER_INITIATE_BONDING = false;
+ private static final boolean SPECIFY_CREATE_BOND_TRANSPORT_TYPE = false;
+ private static final int CREATE_BOND_TRANSPORT_TYPE = 0;
+ private static final boolean KEEP_SAME_ACCOUNT_KEY_WRITE = true;
+ private static final boolean ENABLE_NAMING_CHARACTERISTIC = true;
+ private static final boolean CHECK_FIRMWARE_VERSION = true;
+ private static final int SDP_ATTEMPTS_AFTER_BONDED = 1;
+ private static final boolean SUPPORT_HID = false;
+ private static final boolean ENABLE_PAIRING_WHILE_DIRECTLY_CONNECTING = true;
+ private static final boolean ACCEPT_CONSENT_FOR_FP_ONE = true;
+ private static final int GATT_CONNECT_RETRY_TIMEOUT_MILLIS = 18000;
+ private static final boolean ENABLE_128BIT_CUSTOM_GATT_CHARACTERISTIC = true;
+ private static final boolean ENABLE_SEND_EXCEPTION_STEP_TO_VALIDATOR = true;
+ private static final boolean ENABLE_ADDITIONAL_DATA_TYPE_WHEN_ACTION_OVER_BLE = true;
+ private static final boolean CHECK_BOND_STATE_WHEN_SKIP_CONNECTING_PROFILE = true;
+ private static final boolean MORE_LOG_FOR_QUALITY = true;
+ private static final boolean RETRY_GATT_CONNECTION_AND_SECRET_HANDSHAKE = true;
+ private static final int GATT_CONNECT_SHORT_TIMEOUT_MS = 7000;
+ private static final int GATT_CONNECTION_LONG_TIME_OUT_MS = 15000;
+ private static final int GATT_CONNECT_SHORT_TIMEOUT_RETRY_MAX_SPENT_TIME_MS = 1000;
+ private static final int ADDRESS_ROTATE_RETRY_MAX_SPENT_TIME_MS = 15000;
+ private static final int PAIRING_RETRY_DELAY_MS = 100;
+ private static final int HANDSHAKE_SHORT_TIMEOUT_MS = 3000;
+ private static final int HANDSHAKE_LONG_TIMEOUT_MS = 1000;
+ private static final int SECRET_HANDSHAKE_SHORT_TIMEOUT_RETRY_MAX_SPENT_TIME_MS = 5000;
+ private static final int SECRET_HANDSHAKE_LONG_TIMEOUT_RETRY_MAX_SPENT_TIME_MS = 7000;
+ private static final int SECRET_HANDSHAKE_RETRY_ATTEMPTS = 3;
+ private static final int SECRET_HANDSHAKE_RETRY_GATT_CONNECTION_MAX_SPENT_TIME_MS = 15000;
+ private static final int SIGNAL_LOST_RETRY_MAX_SPENT_TIME_MS = 15000;
+ private static final boolean RETRY_SECRET_HANDSHAKE_TIMEOUT = false;
+ private static final boolean LOG_USER_MANUAL_RETRY = true;
+ private static final boolean ENABLE_PAIR_FLOW_SHOW_UI_WITHOUT_PROFILE_CONNECTION = false;
+ private static final boolean LOG_USER_MANUAL_CITY = true;
+ private static final boolean LOG_PAIR_WITH_CACHED_MODEL_ID = true;
+ private static final boolean DIRECT_CONNECT_PROFILE_IF_MODEL_ID_IN_CACHE = false;
+
+ public static Preferences.Builder getPreferencesBuilder() {
+ return Preferences.builder()
+ .setGattOperationTimeoutSeconds(GATT_OPERATION_TIME_OUT_SECOND)
+ .setGattConnectionTimeoutSeconds(GATT_CONNECTION_TIME_OUT_SECOND)
+ .setBluetoothToggleTimeoutSeconds(BLUETOOTH_TOGGLE_TIME_OUT_SECOND)
+ .setBluetoothToggleSleepSeconds(BLUETOOTH_TOGGLE_SLEEP_TIME_OUT_SECOND)
+ .setClassicDiscoveryTimeoutSeconds(CLASSIC_DISCOVERY_TIME_OUT_SECOND)
+ .setNumDiscoverAttempts(NUM_DISCOVER_ATTEMPTS)
+ .setDiscoveryRetrySleepSeconds(DISCOVERY_RETRY_SLEEP_SECONDS)
+ .setSdpTimeoutSeconds(SDP_TIME_OUT_SECONDS)
+ .setNumSdpAttempts(NUM_SDP_ATTEMPTS)
+ .setNumCreateBondAttempts(NUM_CREATED_BOND_ATTEMPTS)
+ .setNumConnectAttempts(NUM_CONNECT_ATTEMPT)
+ .setNumWriteAccountKeyAttempts(NUM_WRITE_ACCOUNT_KEY_ATTEMPT)
+ .setToggleBluetoothOnFailure(TOGGLE_BLUETOOTH_ON_FAILURE)
+ .setBluetoothStateUsesPolling(BLUETOOTH_STATE_POOLING)
+ .setBluetoothStatePollingMillis(BLUETOOTH_STATE_POOLING_MILLIS)
+ .setNumAttempts(NUM_ATTEMPTS)
+ .setBrHandoverDataCharacteristicId(BREDR_HANDOVER_DATA_CHARACTERISTIC_ID)
+ .setBluetoothSigDataCharacteristicId(BLUETOOTH_SIG_DATA_CHARACTERISTIC_ID)
+ .setBrTransportBlockDataDescriptorId(TRANSPORT_BLOCK_DATA_CHARACTERISTIC_ID)
+ .setWaitForUuidsAfterBonding(WAIT_FOR_UUID_AFTER_BONDING)
+ .setReceiveUuidsAndBondedEventBeforeClose(
+ RECEIVE_UUID_AND_BONDED_EVENT_BEFORE_CLOSE)
+ .setRemoveBondTimeoutSeconds(REMOVE_BOND_TIME_OUT_SECONDS)
+ .setRemoveBondSleepMillis(REMOVE_BOND_SLEEP_MILLIS)
+ .setCreateBondTimeoutSeconds(CREATE_BOND_TIME_OUT_SECONDS)
+ .setHidCreateBondTimeoutSeconds(HIDE_CREATED_BOND_TIME_OUT_SECONDS)
+ .setProxyTimeoutSeconds(PROXY_TIME_OUT_SECONDS)
+ .setRejectPhonebookAccess(REJECT_ACCESS)
+ .setRejectMessageAccess(REJECT_ACCESS)
+ .setRejectSimAccess(REJECT_ACCESS)
+ .setAcceptPasskey(ACCEPT_PASSKEY)
+ .setWriteAccountKeySleepMillis(WRITE_ACCOUNT_KEY_SLEEP_MILLIS)
+ .setProviderInitiatesBondingIfSupported(PROVIDER_INITIATE_BONDING)
+ .setAttemptDirectConnectionWhenPreviouslyBonded(true)
+ .setAutomaticallyReconnectGattWhenNeeded(true)
+ .setSkipDisconnectingGattBeforeWritingAccountKey(true)
+ .setIgnoreUuidTimeoutAfterBonded(true)
+ .setSpecifyCreateBondTransportType(SPECIFY_CREATE_BOND_TRANSPORT_TYPE)
+ .setCreateBondTransportType(CREATE_BOND_TRANSPORT_TYPE)
+ .setIncreaseIntentFilterPriority(true)
+ .setEvaluatePerformance(false)
+ .setKeepSameAccountKeyWrite(KEEP_SAME_ACCOUNT_KEY_WRITE)
+ .setEnableNamingCharacteristic(ENABLE_NAMING_CHARACTERISTIC)
+ .setEnableFirmwareVersionCharacteristic(CHECK_FIRMWARE_VERSION)
+ .setNumSdpAttemptsAfterBonded(SDP_ATTEMPTS_AFTER_BONDED)
+ .setSupportHidDevice(SUPPORT_HID)
+ .setEnablePairingWhileDirectlyConnecting(
+ ENABLE_PAIRING_WHILE_DIRECTLY_CONNECTING)
+ .setAcceptConsentForFastPairOne(ACCEPT_CONSENT_FOR_FP_ONE)
+ .setGattConnectRetryTimeoutMillis(GATT_CONNECT_RETRY_TIMEOUT_MILLIS)
+ .setEnable128BitCustomGattCharacteristicsId(
+ ENABLE_128BIT_CUSTOM_GATT_CHARACTERISTIC)
+ .setEnableSendExceptionStepToValidator(ENABLE_SEND_EXCEPTION_STEP_TO_VALIDATOR)
+ .setEnableAdditionalDataTypeWhenActionOverBle(
+ ENABLE_ADDITIONAL_DATA_TYPE_WHEN_ACTION_OVER_BLE)
+ .setCheckBondStateWhenSkipConnectingProfiles(
+ CHECK_BOND_STATE_WHEN_SKIP_CONNECTING_PROFILE)
+ .setMoreEventLogForQuality(MORE_LOG_FOR_QUALITY)
+ .setRetryGattConnectionAndSecretHandshake(
+ RETRY_GATT_CONNECTION_AND_SECRET_HANDSHAKE)
+ .setGattConnectShortTimeoutMs(GATT_CONNECT_SHORT_TIMEOUT_MS)
+ .setGattConnectLongTimeoutMs(GATT_CONNECTION_LONG_TIME_OUT_MS)
+ .setGattConnectShortTimeoutRetryMaxSpentTimeMs(
+ GATT_CONNECT_SHORT_TIMEOUT_RETRY_MAX_SPENT_TIME_MS)
+ .setAddressRotateRetryMaxSpentTimeMs(ADDRESS_ROTATE_RETRY_MAX_SPENT_TIME_MS)
+ .setPairingRetryDelayMs(PAIRING_RETRY_DELAY_MS)
+ .setSecretHandshakeShortTimeoutMs(HANDSHAKE_SHORT_TIMEOUT_MS)
+ .setSecretHandshakeLongTimeoutMs(HANDSHAKE_LONG_TIMEOUT_MS)
+ .setSecretHandshakeShortTimeoutRetryMaxSpentTimeMs(
+ SECRET_HANDSHAKE_SHORT_TIMEOUT_RETRY_MAX_SPENT_TIME_MS)
+ .setSecretHandshakeLongTimeoutRetryMaxSpentTimeMs(
+ SECRET_HANDSHAKE_LONG_TIMEOUT_RETRY_MAX_SPENT_TIME_MS)
+ .setSecretHandshakeRetryAttempts(SECRET_HANDSHAKE_RETRY_ATTEMPTS)
+ .setSecretHandshakeRetryGattConnectionMaxSpentTimeMs(
+ SECRET_HANDSHAKE_RETRY_GATT_CONNECTION_MAX_SPENT_TIME_MS)
+ .setSignalLostRetryMaxSpentTimeMs(SIGNAL_LOST_RETRY_MAX_SPENT_TIME_MS)
+ .setGattConnectionAndSecretHandshakeNoRetryGattError(
+ getGattConnectionAndSecretHandshakeNoRetryGattError())
+ .setRetrySecretHandshakeTimeout(RETRY_SECRET_HANDSHAKE_TIMEOUT)
+ .setLogUserManualRetry(LOG_USER_MANUAL_RETRY)
+ .setEnablePairFlowShowUiWithoutProfileConnection(
+ ENABLE_PAIR_FLOW_SHOW_UI_WITHOUT_PROFILE_CONNECTION)
+ .setLogUserManualRetry(LOG_USER_MANUAL_CITY)
+ .setLogPairWithCachedModelId(LOG_PAIR_WITH_CACHED_MODEL_ID)
+ .setDirectConnectProfileIfModelIdInCache(
+ DIRECT_CONNECT_PROFILE_IF_MODEL_ID_IN_CACHE);
+ }
+
+ private static ImmutableSet<Integer> getGattConnectionAndSecretHandshakeNoRetryGattError() {
+ ImmutableSet.Builder<Integer> noRetryGattErrorsBuilder = ImmutableSet.builder();
+ // When GATT connection fail we will not retry on error code 257
+ for (String errorCode :
+ Splitter.on(",").split("257,")) {
+ if (!TextUtils.isDigitsOnly(errorCode)) {
+ continue;
+ }
+
+ try {
+ noRetryGattErrorsBuilder.add(Integer.parseInt(errorCode));
+ } catch (NumberFormatException e) {
+ // Ignore
+ }
+ }
+ return noRetryGattErrorsBuilder.build();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/UserActionHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/UserActionHandler.java
new file mode 100644
index 0000000..674633d
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/UserActionHandler.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair;
+
+import com.android.server.nearby.common.fastpair.service.UserActionHandlerBase;
+
+/**
+ * User action handler class.
+ */
+public class UserActionHandler extends UserActionHandlerBase {
+
+ public static final String EXTRA_DISCOVERY_ITEM = PREFIX + "EXTRA_DISCOVERY_ITEM";
+ public static final String EXTRA_FAST_PAIR_SECRET = PREFIX + "EXTRA_FAST_PAIR_SECRET";
+ public static final String ACTION_FAST_PAIR = ACTION_PREFIX + "ACTION_FAST_PAIR";
+ public static final String EXTRA_PRIVATE_BLE_ADDRESS =
+ ACTION_PREFIX + "EXTRA_PRIVATE_BLE_ADDRESS";
+}
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
new file mode 100644
index 0000000..6065f99
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java
@@ -0,0 +1,470 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.cache;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
+import static com.android.server.nearby.fastpair.UserActionHandler.EXTRA_FAST_PAIR_SECRET;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.text.TextUtils;
+import android.util.Log;
+
+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;
+import java.net.URISyntaxException;
+import java.time.Clock;
+import java.util.Objects;
+
+import service.proto.Cache;
+
+/**
+ * Wrapper class around StoredDiscoveryItem. A centralized place for methods related to
+ * updating/parsing StoredDiscoveryItem.
+ */
+public class DiscoveryItem implements Comparable<DiscoveryItem> {
+
+ private static final String ACTION_FAST_PAIR =
+ "com.android.server.nearby:ACTION_FAST_PAIR";
+ private static final int BEACON_STALENESS_MILLIS = 120000;
+ private static final int ITEM_EXPIRATION_MILLIS = 20000;
+ private static final int APP_INSTALL_EXPIRATION_MILLIS = 600000;
+ private static final int ITEM_DELETABLE_MILLIS = 15000;
+
+ private final FastPairCacheManager mFastPairCacheManager;
+ private final Clock mClock;
+
+ private Cache.StoredDiscoveryItem mStoredDiscoveryItem;
+
+ /** IntDef for StoredDiscoveryItem.State */
+ @IntDef({
+ Cache.StoredDiscoveryItem.State.STATE_ENABLED_VALUE,
+ Cache.StoredDiscoveryItem.State.STATE_MUTED_VALUE,
+ Cache.StoredDiscoveryItem.State.STATE_DISABLED_BY_SYSTEM_VALUE
+ })
+ @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);
+ this.mStoredDiscoveryItem = mStoredDiscoveryItem;
+ }
+
+ /** @return A new StoredDiscoveryItem with state fields set to their defaults. */
+ public static Cache.StoredDiscoveryItem newStoredDiscoveryItem() {
+ Cache.StoredDiscoveryItem.Builder storedDiscoveryItem =
+ Cache.StoredDiscoveryItem.newBuilder();
+ storedDiscoveryItem.setState(Cache.StoredDiscoveryItem.State.STATE_ENABLED);
+ return storedDiscoveryItem.build();
+ }
+
+ /**
+ * Checks if store discovery item support fast pair or not.
+ */
+ public boolean isFastPair() {
+ Intent intent = parseIntentScheme(mStoredDiscoveryItem.getActionUrl());
+ if (intent == null) {
+ Log.w("FastPairDiscovery", "FastPair: fail to parse action url"
+ + mStoredDiscoveryItem.getActionUrl());
+ return false;
+ }
+ return ACTION_FAST_PAIR.equals(intent.getAction());
+ }
+
+ /**
+ * Sets the store discovery item mac address.
+ */
+ public void setMacAddress(String address) {
+ mStoredDiscoveryItem = mStoredDiscoveryItem.toBuilder().setMacAddress(address).build();
+
+ mFastPairCacheManager.saveDiscoveryItem(this);
+ }
+
+ /**
+ * Checks if the item is expired. Expired items are those over getItemExpirationMillis() eg. 2
+ * minutes
+ */
+ public static boolean isExpired(
+ long currentTimestampMillis, @Nullable Long lastObservationTimestampMillis) {
+ if (lastObservationTimestampMillis == null) {
+ return true;
+ }
+ return (currentTimestampMillis - lastObservationTimestampMillis)
+ >= ITEM_EXPIRATION_MILLIS;
+ }
+
+ /**
+ * Checks if the item is deletable for saving disk space. Deletable items are those over
+ * getItemDeletableMillis eg. over 25 hrs.
+ */
+ public static boolean isDeletable(
+ long currentTimestampMillis, @Nullable Long lastObservationTimestampMillis) {
+ if (lastObservationTimestampMillis == null) {
+ return true;
+ }
+ return currentTimestampMillis - lastObservationTimestampMillis
+ >= ITEM_DELETABLE_MILLIS;
+ }
+
+ /** Checks if the item has a pending app install */
+ public boolean isPendingAppInstallValid() {
+ return isPendingAppInstallValid(mClock.millis());
+ }
+
+ /**
+ * Checks if pending app valid.
+ */
+ public boolean isPendingAppInstallValid(long appInstallMillis) {
+ return isPendingAppInstallValid(appInstallMillis, mStoredDiscoveryItem);
+ }
+
+ /**
+ * Checks if the app install time expired.
+ */
+ public static boolean isPendingAppInstallValid(
+ long currentMillis, Cache.StoredDiscoveryItem storedItem) {
+ return currentMillis - storedItem.getPendingAppInstallTimestampMillis()
+ < APP_INSTALL_EXPIRATION_MILLIS;
+ }
+
+
+ /** Checks if the item has enough data to be shown */
+ public boolean isReadyForDisplay() {
+ boolean hasUrlOrPopularApp = !mStoredDiscoveryItem.getActionUrl().isEmpty();
+
+ return !TextUtils.isEmpty(mStoredDiscoveryItem.getTitle()) && hasUrlOrPopularApp;
+ }
+
+ /** Checks if the action url is app install */
+ public boolean isApp() {
+ return mStoredDiscoveryItem.getActionUrlType() == Cache.ResolvedUrlType.APP;
+ }
+
+ /** Returns true if an item is muted, or if state is unavailable. */
+ public boolean isMuted() {
+ return mStoredDiscoveryItem.getState() != Cache.StoredDiscoveryItem.State.STATE_ENABLED;
+ }
+
+ /**
+ * Returns the state of store discovery item.
+ */
+ public Cache.StoredDiscoveryItem.State getState() {
+ return mStoredDiscoveryItem.getState();
+ }
+
+ /** Checks if it's device item. e.g. Chromecast / Wear */
+ public static boolean isDeviceType(Cache.NearbyType type) {
+ return type == Cache.NearbyType.NEARBY_CHROMECAST
+ || type == Cache.NearbyType.NEARBY_WEAR
+ || type == Cache.NearbyType.NEARBY_DEVICE;
+ }
+
+ /**
+ * Check if the type is supported.
+ */
+ public static boolean isTypeEnabled(Cache.NearbyType type) {
+ switch (type) {
+ case NEARBY_WEAR:
+ case NEARBY_CHROMECAST:
+ case NEARBY_DEVICE:
+ return true;
+ default:
+ Log.e("FastPairDiscoveryItem", "Invalid item type " + type.name());
+ return false;
+ }
+ }
+
+ /** Gets hash code of UI related data so we can collapse identical items. */
+ public int getUiHashCode() {
+ return Objects.hash(
+ mStoredDiscoveryItem.getTitle(),
+ mStoredDiscoveryItem.getDescription(),
+ mStoredDiscoveryItem.getAppName(),
+ mStoredDiscoveryItem.getDisplayUrl(),
+ mStoredDiscoveryItem.getMacAddress());
+ }
+
+ // Getters below
+
+ /**
+ * Returns the id of store discovery item.
+ */
+ @Nullable
+ public String getId() {
+ return mStoredDiscoveryItem.getId();
+ }
+
+ /**
+ * Returns the title of discovery item.
+ */
+ @Nullable
+ public String getTitle() {
+ return mStoredDiscoveryItem.getTitle();
+ }
+
+ /**
+ * Returns the description of discovery item.
+ */
+ @Nullable
+ public String getDescription() {
+ return mStoredDiscoveryItem.getDescription();
+ }
+
+ /**
+ * Returns the mac address of discovery item.
+ */
+ @Nullable
+ public String getMacAddress() {
+ return mStoredDiscoveryItem.getMacAddress();
+ }
+
+ /**
+ * Returns the display url of discovery item.
+ */
+ @Nullable
+ public String getDisplayUrl() {
+ return mStoredDiscoveryItem.getDisplayUrl();
+ }
+
+ /**
+ * Returns the public key of discovery item.
+ */
+ @Nullable
+ public byte[] getAuthenticationPublicKeySecp256R1() {
+ return mStoredDiscoveryItem.getAuthenticationPublicKeySecp256R1().toByteArray();
+ }
+
+ /**
+ * Returns the pairing secret.
+ */
+ @Nullable
+ public String getFastPairSecretKey() {
+ Intent intent = parseIntentScheme(mStoredDiscoveryItem.getActionUrl());
+ if (intent == null) {
+ Log.d("FastPairDiscoveryItem", "FastPair: fail to parse action url "
+ + mStoredDiscoveryItem.getActionUrl());
+ return null;
+ }
+ return intent.getStringExtra(EXTRA_FAST_PAIR_SECRET);
+ }
+
+ /**
+ * Returns the fast pair info of discovery item.
+ */
+ @Nullable
+ public Cache.FastPairInformation getFastPairInformation() {
+ return mStoredDiscoveryItem.hasFastPairInformation()
+ ? mStoredDiscoveryItem.getFastPairInformation() : null;
+ }
+
+ /**
+ * Returns the app name of discovery item.
+ */
+ @Nullable
+ private String getAppName() {
+ return mStoredDiscoveryItem.getAppName();
+ }
+
+ /**
+ * Returns the package name of discovery item.
+ */
+ @Nullable
+ public String getAppPackageName() {
+ return mStoredDiscoveryItem.getPackageName();
+ }
+
+ /**
+ * Returns the action url of discovery item.
+ */
+ @Nullable
+ public String getActionUrl() {
+ return mStoredDiscoveryItem.getActionUrl();
+ }
+
+ /**
+ * Returns the rssi value of discovery item.
+ */
+ @Nullable
+ public Integer getRssi() {
+ return mStoredDiscoveryItem.getRssi();
+ }
+
+ /**
+ * Returns the TX power of discovery item.
+ */
+ @Nullable
+ public Integer getTxPower() {
+ return mStoredDiscoveryItem.getTxPower();
+ }
+
+ /**
+ * Returns the first observed time stamp of discovery item.
+ */
+ @Nullable
+ public Long getFirstObservationTimestampMillis() {
+ return mStoredDiscoveryItem.getFirstObservationTimestampMillis();
+ }
+
+ /**
+ * Returns the last observed time stamp of discovery item.
+ */
+ @Nullable
+ public Long getLastObservationTimestampMillis() {
+ return mStoredDiscoveryItem.getLastObservationTimestampMillis();
+ }
+
+ /**
+ * Calculates an estimated distance for the item, computed from the TX power (at 1m) and RSSI.
+ *
+ * @return estimated distance, or null if there is no RSSI or no TX power.
+ */
+ @Nullable
+ public Double getEstimatedDistance() {
+ // In the future, we may want to do a foreground subscription to leverage onDistanceChanged.
+ return RangingUtils.distanceFromRssiAndTxPower(mStoredDiscoveryItem.getRssi(),
+ mStoredDiscoveryItem.getTxPower());
+ }
+
+ /**
+ * Gets icon Bitmap from icon store.
+ *
+ * @return null if no icon or icon size is incorrect.
+ */
+ @Nullable
+ public Bitmap getIcon() {
+ Bitmap icon =
+ BitmapFactory.decodeByteArray(
+ mStoredDiscoveryItem.getIconPng().toByteArray(),
+ 0 /* offset */, mStoredDiscoveryItem.getIconPng().size());
+ if (IconUtils.isIconSizeCorrect(icon)) {
+ return icon;
+ } else {
+ return null;
+ }
+ }
+
+ /** Gets a FIFE URL of the icon. */
+ @Nullable
+ public String getIconFifeUrl() {
+ return mStoredDiscoveryItem.getIconFifeUrl();
+ }
+
+ /**
+ * Compares this object to the specified object: 1. By device type. Device setups are 'greater
+ * than' beacons. 2. By relevance. More relevant items are 'greater than' less relevant items.
+ * 3.By distance. Nearer items are 'greater than' further items.
+ *
+ * <p>In the list view, we sort in descending order, i.e. we put the most relevant items first.
+ */
+ @Override
+ public int compareTo(DiscoveryItem another) {
+ // For items of the same relevance, compare distance.
+ Double distance1 = getEstimatedDistance();
+ Double distance2 = another.getEstimatedDistance();
+ distance1 = distance1 != null ? distance1 : Double.MAX_VALUE;
+ distance2 = distance2 != null ? distance2 : Double.MAX_VALUE;
+ // Negate because closer items are better ("greater than") further items.
+ return -distance1.compareTo(distance2);
+ }
+
+ @Nullable
+ public String getTriggerId() {
+ return mStoredDiscoveryItem.getTriggerId();
+ }
+
+ @Override
+ public boolean equals(Object another) {
+ if (another instanceof DiscoveryItem) {
+ return ((DiscoveryItem) another).mStoredDiscoveryItem.equals(mStoredDiscoveryItem);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mStoredDiscoveryItem.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "[triggerId=%s], [id=%s], [title=%s], [url=%s], [ready=%s], [macAddress=%s]",
+ getTriggerId(),
+ getId(),
+ getTitle(),
+ getActionUrl(),
+ isReadyForDisplay(),
+ maskBluetoothAddress(getMacAddress()));
+ }
+
+ /**
+ * Gets a copy of the StoredDiscoveryItem proto backing this DiscoveryItem. Currently needed for
+ * Fast Pair 2.0: We store the item in the cloud associated with a user's account, to enable
+ * pairing with other devices owned by the user.
+ */
+ public Cache.StoredDiscoveryItem getCopyOfStoredItem() {
+ return mStoredDiscoveryItem;
+ }
+
+ /**
+ * Gets the StoredDiscoveryItem represented by this DiscoveryItem. This lets tests manipulate
+ * values that production code should not manipulate.
+ */
+
+ public Cache.StoredDiscoveryItem getStoredItemForTest() {
+ return mStoredDiscoveryItem;
+ }
+
+ /**
+ * Sets the StoredDiscoveryItem represented by this DiscoveryItem. This lets tests manipulate
+ * values that production code should not manipulate.
+ */
+ public void setStoredItemForTest(Cache.StoredDiscoveryItem s) {
+ mStoredDiscoveryItem = s;
+ }
+
+ /**
+ * Parse the intent from item url.
+ */
+ public static Intent parseIntentScheme(String uri) {
+ try {
+ return Intent.parseUri(uri, Intent.URI_INTENT_SCHEME);
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItemContract.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItemContract.java
new file mode 100644
index 0000000..61ca3fd
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItemContract.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.cache;
+
+import android.provider.BaseColumns;
+
+/**
+ * Defines DiscoveryItem database schema.
+ */
+public class DiscoveryItemContract {
+ private DiscoveryItemContract() {}
+
+ /**
+ * Discovery item entry related info.
+ */
+ public static class DiscoveryItemEntry implements BaseColumns {
+ public static final String TABLE_NAME = "SCAN_RESULT";
+ public static final String COLUMN_MODEL_ID = "MODEL_ID";
+ public static final String COLUMN_SCAN_BYTE = "SCAN_RESULT_BYTE";
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java
new file mode 100644
index 0000000..b840091
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.cache;
+
+import android.bluetooth.le.ScanResult;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+import com.android.server.nearby.common.eventloop.Annotations;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import service.proto.Cache;
+import service.proto.Rpcs;
+
+
+/**
+ * Save FastPair device info to database to avoid multiple requesting.
+ */
+public class FastPairCacheManager {
+ private final Context mContext;
+ private final FastPairDbHelper mFastPairDbHelper;
+
+ public FastPairCacheManager(Context context) {
+ mContext = context;
+ mFastPairDbHelper = new FastPairDbHelper(context);
+ }
+
+ /**
+ * Clean up function to release db
+ */
+ public void cleanUp() {
+ mFastPairDbHelper.close();
+ }
+
+ /**
+ * Saves the response to the db
+ */
+ private void saveDevice() {
+ }
+
+ Cache.ServerResponseDbItem getDeviceFromScanResult(ScanResult scanResult) {
+ return Cache.ServerResponseDbItem.newBuilder().build();
+ }
+
+ /**
+ * Checks if the entry can be auto deleted from the cache
+ */
+ public boolean isDeletable(Cache.ServerResponseDbItem entry) {
+ if (!entry.getExpirable()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Save discovery item into database. Discovery item is item that discovered through Ble before
+ * pairing success.
+ */
+ public boolean saveDiscoveryItem(DiscoveryItem item) {
+
+ SQLiteDatabase db = mFastPairDbHelper.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(DiscoveryItemContract.DiscoveryItemEntry.COLUMN_MODEL_ID, item.getTriggerId());
+ values.put(DiscoveryItemContract.DiscoveryItemEntry.COLUMN_SCAN_BYTE,
+ item.getCopyOfStoredItem().toByteArray());
+ db.insert(DiscoveryItemContract.DiscoveryItemEntry.TABLE_NAME, null, values);
+ return true;
+ }
+
+
+ @Annotations.EventThread
+ private Rpcs.GetObservedDeviceResponse getObservedDeviceInfo(ScanResult scanResult) {
+ return Rpcs.GetObservedDeviceResponse.getDefaultInstance();
+ }
+
+ /**
+ * Get discovery item from item id.
+ */
+ public DiscoveryItem getDiscoveryItem(String itemId) {
+ return new DiscoveryItem(mContext, getStoredDiscoveryItem(itemId));
+ }
+
+ /**
+ * Get discovery item from item id.
+ */
+ public Cache.StoredDiscoveryItem getStoredDiscoveryItem(String itemId) {
+ SQLiteDatabase db = mFastPairDbHelper.getReadableDatabase();
+ String[] projection = {
+ DiscoveryItemContract.DiscoveryItemEntry.COLUMN_MODEL_ID,
+ DiscoveryItemContract.DiscoveryItemEntry.COLUMN_SCAN_BYTE
+ };
+ String selection = DiscoveryItemContract.DiscoveryItemEntry.COLUMN_MODEL_ID + " =? ";
+ String[] selectionArgs = {itemId};
+ Cursor cursor = db.query(
+ DiscoveryItemContract.DiscoveryItemEntry.TABLE_NAME,
+ projection,
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null
+ );
+
+ if (cursor.moveToNext()) {
+ byte[] res = cursor.getBlob(cursor.getColumnIndexOrThrow(
+ DiscoveryItemContract.DiscoveryItemEntry.COLUMN_SCAN_BYTE));
+ try {
+ Cache.StoredDiscoveryItem item = Cache.StoredDiscoveryItem.parseFrom(res);
+ return item;
+ } catch (InvalidProtocolBufferException e) {
+ Log.e("FastPairCacheManager", "storediscovery has error");
+ }
+ }
+ cursor.close();
+ return Cache.StoredDiscoveryItem.getDefaultInstance();
+ }
+
+ /**
+ * Get all of the discovery item related info in the cache.
+ */
+ public List<Cache.StoredDiscoveryItem> getAllSavedStoreDiscoveryItem() {
+ List<Cache.StoredDiscoveryItem> storedDiscoveryItemList = new ArrayList<>();
+ SQLiteDatabase db = mFastPairDbHelper.getReadableDatabase();
+ String[] projection = {
+ DiscoveryItemContract.DiscoveryItemEntry.COLUMN_MODEL_ID,
+ DiscoveryItemContract.DiscoveryItemEntry.COLUMN_SCAN_BYTE
+ };
+ Cursor cursor = db.query(
+ DiscoveryItemContract.DiscoveryItemEntry.TABLE_NAME,
+ projection,
+ null,
+ null,
+ null,
+ null,
+ null
+ );
+
+ while (cursor.moveToNext()) {
+ byte[] res = cursor.getBlob(cursor.getColumnIndexOrThrow(
+ DiscoveryItemContract.DiscoveryItemEntry.COLUMN_SCAN_BYTE));
+ try {
+ Cache.StoredDiscoveryItem item = Cache.StoredDiscoveryItem.parseFrom(res);
+ storedDiscoveryItemList.add(item);
+ } catch (InvalidProtocolBufferException e) {
+ Log.e("FastPairCacheManager", "storediscovery has error");
+ }
+
+ }
+ cursor.close();
+ return storedDiscoveryItemList;
+ }
+
+ /**
+ * Get scan result from local database use model id
+ */
+ public Cache.StoredScanResult getStoredScanResult(String modelId) {
+ return Cache.StoredScanResult.getDefaultInstance();
+ }
+
+ /**
+ * Gets the paired Fast Pair item that paired to the phone through mac address.
+ */
+ public Cache.StoredFastPairItem getStoredFastPairItemFromMacAddress(String macAddress) {
+ SQLiteDatabase db = mFastPairDbHelper.getReadableDatabase();
+ String[] projection = {
+ StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_ACCOUNT_KEY,
+ StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_MAC_ADDRESS,
+ StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_STORED_FAST_PAIR_BYTE
+ };
+ String selection =
+ StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_MAC_ADDRESS + " =? ";
+ String[] selectionArgs = {macAddress};
+ Cursor cursor = db.query(
+ StoredFastPairItemContract.StoredFastPairItemEntry.TABLE_NAME,
+ projection,
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null
+ );
+
+ if (cursor.moveToNext()) {
+ byte[] res = cursor.getBlob(cursor.getColumnIndexOrThrow(
+ StoredFastPairItemContract.StoredFastPairItemEntry
+ .COLUMN_STORED_FAST_PAIR_BYTE));
+ try {
+ Cache.StoredFastPairItem item = Cache.StoredFastPairItem.parseFrom(res);
+ return item;
+ } catch (InvalidProtocolBufferException e) {
+ Log.e("FastPairCacheManager", "storediscovery has error");
+ }
+ }
+ cursor.close();
+ return Cache.StoredFastPairItem.getDefaultInstance();
+ }
+
+ /**
+ * Save paired fast pair item into the database.
+ */
+ public boolean putStoredFastPairItem(Cache.StoredFastPairItem storedFastPairItem) {
+ SQLiteDatabase db = mFastPairDbHelper.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_MAC_ADDRESS,
+ storedFastPairItem.getMacAddress());
+ values.put(StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_ACCOUNT_KEY,
+ storedFastPairItem.getAccountKey().toString());
+ values.put(StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_STORED_FAST_PAIR_BYTE,
+ storedFastPairItem.toByteArray());
+ db.insert(StoredFastPairItemContract.StoredFastPairItemEntry.TABLE_NAME, null, values);
+ return true;
+
+ }
+
+ /**
+ * Removes certain storedFastPairItem so that it can update timely.
+ */
+ public void removeStoredFastPairItem(String macAddress) {
+ SQLiteDatabase db = mFastPairDbHelper.getWritableDatabase();
+ int res = db.delete(StoredFastPairItemContract.StoredFastPairItemEntry.TABLE_NAME,
+ StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_MAC_ADDRESS + "=?",
+ new String[]{macAddress});
+
+ }
+
+ /**
+ * Get all of the store fast pair item related info in the cache.
+ */
+ public List<Cache.StoredFastPairItem> getAllSavedStoredFastPairItem() {
+ List<Cache.StoredFastPairItem> storedFastPairItemList = new ArrayList<>();
+ SQLiteDatabase db = mFastPairDbHelper.getReadableDatabase();
+ String[] projection = {
+ StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_MAC_ADDRESS,
+ StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_ACCOUNT_KEY,
+ StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_STORED_FAST_PAIR_BYTE
+ };
+ Cursor cursor = db.query(
+ StoredFastPairItemContract.StoredFastPairItemEntry.TABLE_NAME,
+ projection,
+ null,
+ null,
+ null,
+ null,
+ null
+ );
+
+ while (cursor.moveToNext()) {
+ byte[] res = cursor.getBlob(cursor.getColumnIndexOrThrow(StoredFastPairItemContract
+ .StoredFastPairItemEntry.COLUMN_STORED_FAST_PAIR_BYTE));
+ try {
+ Cache.StoredFastPairItem item = Cache.StoredFastPairItem.parseFrom(res);
+ storedFastPairItemList.add(item);
+ } catch (InvalidProtocolBufferException e) {
+ Log.e("FastPairCacheManager", "storediscovery has error");
+ }
+
+ }
+ cursor.close();
+ return storedFastPairItemList;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairDbHelper.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairDbHelper.java
new file mode 100644
index 0000000..d950d8d
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairDbHelper.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.cache;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+/**
+ * Fast Pair db helper handle all of the db actions related Fast Pair.
+ */
+public class FastPairDbHelper extends SQLiteOpenHelper {
+
+ public static final int DATABASE_VERSION = 1;
+ public static final String DATABASE_NAME = "FastPair.db";
+ private static final String SQL_CREATE_DISCOVERY_ITEM_DB =
+ "CREATE TABLE IF NOT EXISTS " + DiscoveryItemContract.DiscoveryItemEntry.TABLE_NAME
+ + " (" + DiscoveryItemContract.DiscoveryItemEntry._ID
+ + "INTEGER PRIMARY KEY,"
+ + DiscoveryItemContract.DiscoveryItemEntry.COLUMN_MODEL_ID
+ + " TEXT," + DiscoveryItemContract.DiscoveryItemEntry.COLUMN_SCAN_BYTE
+ + " BLOB)";
+ private static final String SQL_DELETE_DISCOVERY_ITEM_DB =
+ "DROP TABLE IF EXISTS " + DiscoveryItemContract.DiscoveryItemEntry.TABLE_NAME;
+ private static final String SQL_CREATE_FAST_PAIR_ITEM_DB =
+ "CREATE TABLE IF NOT EXISTS "
+ + StoredFastPairItemContract.StoredFastPairItemEntry.TABLE_NAME
+ + " (" + StoredFastPairItemContract.StoredFastPairItemEntry._ID
+ + "INTEGER PRIMARY KEY,"
+ + StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_MAC_ADDRESS
+ + " TEXT,"
+ + StoredFastPairItemContract.StoredFastPairItemEntry.COLUMN_ACCOUNT_KEY
+ + " TEXT,"
+ + StoredFastPairItemContract
+ .StoredFastPairItemEntry.COLUMN_STORED_FAST_PAIR_BYTE
+ + " BLOB)";
+ private static final String SQL_DELETE_FAST_PAIR_ITEM_DB =
+ "DROP TABLE IF EXISTS " + StoredFastPairItemContract.StoredFastPairItemEntry.TABLE_NAME;
+
+ public FastPairDbHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(SQL_CREATE_DISCOVERY_ITEM_DB);
+ db.execSQL(SQL_CREATE_FAST_PAIR_ITEM_DB);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // Since the outdated data has no value so just remove the data.
+ db.execSQL(SQL_DELETE_DISCOVERY_ITEM_DB);
+ db.execSQL(SQL_DELETE_FAST_PAIR_ITEM_DB);
+ onCreate(db);
+ }
+
+ @Override
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ super.onDowngrade(db, oldVersion, newVersion);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/StoredFastPairItemContract.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/StoredFastPairItemContract.java
new file mode 100644
index 0000000..9980565
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/cache/StoredFastPairItemContract.java
@@ -0,0 +1,37 @@
+/*
+ * 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.cache;
+
+import android.provider.BaseColumns;
+
+/**
+ * Defines fast pair item database schema.
+ */
+public class StoredFastPairItemContract {
+ private StoredFastPairItemContract() {}
+
+ /**
+ * StoredFastPairItem entry related info.
+ */
+ public static class StoredFastPairItemEntry implements BaseColumns {
+ public static final String TABLE_NAME = "STORED_FAST_PAIR_ITEM";
+ public static final String COLUMN_MAC_ADDRESS = "MAC_ADDRESS";
+ public static final String COLUMN_ACCOUNT_KEY = "ACCOUNT_KEY";
+
+ public static final String COLUMN_STORED_FAST_PAIR_BYTE = "STORED_FAST_PAIR_BYTE";
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/footprint/FastPairUploadInfo.java b/nearby/service/java/com/android/server/nearby/fastpair/footprint/FastPairUploadInfo.java
new file mode 100644
index 0000000..6c9aff0
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/footprint/FastPairUploadInfo.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.footprint;
+
+
+import com.google.protobuf.ByteString;
+
+import service.proto.Cache;
+
+/**
+ * Wrapper class that upload the pair info to the footprint.
+ */
+public class FastPairUploadInfo {
+
+ private Cache.StoredDiscoveryItem mStoredDiscoveryItem;
+
+ private ByteString mAccountKey;
+
+ private ByteString mSha256AccountKeyPublicAddress;
+
+
+ public FastPairUploadInfo(Cache.StoredDiscoveryItem storedDiscoveryItem, ByteString accountKey,
+ ByteString sha256AccountKeyPublicAddress) {
+ mStoredDiscoveryItem = storedDiscoveryItem;
+ mAccountKey = accountKey;
+ mSha256AccountKeyPublicAddress = sha256AccountKeyPublicAddress;
+ }
+
+ public Cache.StoredDiscoveryItem getStoredDiscoveryItem() {
+ return mStoredDiscoveryItem;
+ }
+
+ public ByteString getAccountKey() {
+ return mAccountKey;
+ }
+
+
+ public ByteString getSha256AccountKeyPublicAddress() {
+ return mSha256AccountKeyPublicAddress;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/footprint/FootprintsDeviceManager.java b/nearby/service/java/com/android/server/nearby/fastpair/footprint/FootprintsDeviceManager.java
new file mode 100644
index 0000000..68217c1
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/footprint/FootprintsDeviceManager.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.footprint;
+
+/**
+ * FootprintDeviceManager is responsible for all of the foot print operation. Footprint will
+ * store all of device info that already paired with certain account. This class will call AOSP
+ * api to let OEM save certain device.
+ */
+public class FootprintsDeviceManager {
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManager.java b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManager.java
new file mode 100644
index 0000000..553d5ce
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManager.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.halfsheet;
+
+import static com.android.server.nearby.fastpair.Constant.DEVICE_PAIRING_FRAGMENT_TYPE;
+import static com.android.server.nearby.fastpair.Constant.EXTRA_BINDER;
+import static com.android.server.nearby.fastpair.Constant.EXTRA_BUNDLE;
+import static com.android.server.nearby.fastpair.Constant.EXTRA_HALF_SHEET_INFO;
+import static com.android.server.nearby.fastpair.Constant.EXTRA_HALF_SHEET_TYPE;
+import static com.android.server.nearby.fastpair.FastPairManager.ACTION_RESOURCES_APK;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.nearby.FastPairDevice;
+import android.nearby.FastPairStatusCallback;
+import android.nearby.PairStatusMetadata;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.FastPairController;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+import com.android.server.nearby.util.Environment;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import service.proto.Cache;
+
+/**
+ * Fast Pair ux manager for half sheet.
+ */
+public class FastPairHalfSheetManager {
+ private static final String ACTIVITY_INTENT_ACTION = "android.nearby.SHOW_HALFSHEET";
+ private static final String HALF_SHEET_CLASS_NAME =
+ "com.android.nearby.halfsheet.HalfSheetActivity";
+ private static final String TAG = "FPHalfSheetManager";
+
+ private String mHalfSheetApkPkgName;
+ private final LocatorContextWrapper mLocatorContextWrapper;
+
+ FastPairUiServiceImpl mFastPairUiService;
+
+ public FastPairHalfSheetManager(Context context) {
+ this(new LocatorContextWrapper(context));
+ }
+
+ @VisibleForTesting
+ FastPairHalfSheetManager(LocatorContextWrapper locatorContextWrapper) {
+ mLocatorContextWrapper = locatorContextWrapper;
+ mFastPairUiService = new FastPairUiServiceImpl();
+ }
+
+ /**
+ * Invokes half sheet in the other apk. This function can only be called in Nearby because other
+ * app can't get the correct component name.
+ */
+ public void showHalfSheet(Cache.ScanFastPairStoreItem scanFastPairStoreItem) {
+ try {
+ if (mLocatorContextWrapper != null) {
+ String packageName = getHalfSheetApkPkgName();
+ if (packageName == null) {
+ Log.e(TAG, "package name is null");
+ return;
+ }
+ mFastPairUiService.setFastPairController(
+ mLocatorContextWrapper.getLocator().get(FastPairController.class));
+ Bundle bundle = new Bundle();
+ bundle.putBinder(EXTRA_BINDER, mFastPairUiService);
+ mLocatorContextWrapper
+ .startActivityAsUser(new Intent(ACTIVITY_INTENT_ACTION)
+ .putExtra(EXTRA_HALF_SHEET_INFO,
+ scanFastPairStoreItem.toByteArray())
+ .putExtra(EXTRA_HALF_SHEET_TYPE,
+ DEVICE_PAIRING_FRAGMENT_TYPE)
+ .putExtra(EXTRA_BUNDLE, bundle)
+ .setComponent(new ComponentName(packageName,
+ HALF_SHEET_CLASS_NAME)),
+ UserHandle.CURRENT);
+ }
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Can't resolve package that contains half sheet");
+ }
+ }
+
+ /**
+ * Shows pairing fail half sheet.
+ */
+ public void showPairingFailed() {
+ FastPairStatusCallback pairStatusCallback = mFastPairUiService.getPairStatusCallback();
+ if (pairStatusCallback != null) {
+ Log.v(TAG, "showPairingFailed: pairStatusCallback not NULL");
+ pairStatusCallback.onPairUpdate(new FastPairDevice.Builder().build(),
+ new PairStatusMetadata(PairStatusMetadata.Status.FAIL));
+ } else {
+ Log.w(TAG, "FastPairHalfSheetManager failed to show success half sheet because "
+ + "the pairStatusCallback is null");
+ }
+ }
+
+ /**
+ * Get the half sheet status whether it is foreground or dismissed
+ */
+ public boolean getHalfSheetForegroundState() {
+ return true;
+ }
+
+ /**
+ * Show passkey confirmation info on half sheet
+ */
+ public void showPasskeyConfirmation(BluetoothDevice device, int passkey) {
+ }
+
+ /**
+ * This function will handle pairing steps for half sheet.
+ */
+ public void showPairingHalfSheet(DiscoveryItem item) {
+ Log.d(TAG, "show pairing half sheet");
+ }
+
+ /**
+ * Shows pairing success info.
+ */
+ public void showPairingSuccessHalfSheet(String address) {
+ FastPairStatusCallback pairStatusCallback = mFastPairUiService.getPairStatusCallback();
+ if (pairStatusCallback != null) {
+ pairStatusCallback.onPairUpdate(
+ new FastPairDevice.Builder().setBluetoothAddress(address).build(),
+ new PairStatusMetadata(PairStatusMetadata.Status.SUCCESS));
+ } else {
+ Log.w(TAG, "FastPairHalfSheetManager failed to show success half sheet because "
+ + "the pairStatusCallback is null");
+ }
+ }
+
+ /**
+ * Removes dismiss runnable.
+ */
+ public void disableDismissRunnable() {
+ }
+
+ /**
+ * Destroys the bluetooth pairing controller.
+ */
+ public void destroyBluetoothPairController() {
+ }
+
+ /**
+ * Notify manager the pairing has finished.
+ */
+ public void notifyPairingProcessDone(boolean success, String address, DiscoveryItem item) {
+ }
+
+ /**
+ * Gets the package name of HalfSheet.apk
+ * getHalfSheetApkPkgName may invoke PackageManager multiple times and it does not have
+ * race condition check. Since there is no lock for mHalfSheetApkPkgName.
+ */
+ String getHalfSheetApkPkgName() {
+ if (mHalfSheetApkPkgName != null) {
+ return mHalfSheetApkPkgName;
+ }
+ List<ResolveInfo> resolveInfos = mLocatorContextWrapper
+ .getPackageManager().queryIntentActivities(
+ new Intent(ACTION_RESOURCES_APK),
+ PackageManager.MATCH_SYSTEM_ONLY);
+
+ // remove apps that don't live in the nearby apex
+ resolveInfos.removeIf(info ->
+ !Environment.isAppInNearbyApex(info.activityInfo.applicationInfo));
+
+ if (resolveInfos.isEmpty()) {
+ // Resource APK not loaded yet, print a stack trace to see where this is called from
+ Log.e("FastPairManager", "Attempted to fetch resources before halfsheet "
+ + " APK is installed or package manager can't resolve correctly!",
+ new IllegalStateException());
+ return null;
+ }
+
+ if (resolveInfos.size() > 1) {
+ // multiple apps found, log a warning, but continue
+ Log.w("FastPairManager", "Found > 1 APK that can resolve halfsheet APK intent: "
+ + resolveInfos.stream()
+ .map(info -> info.activityInfo.applicationInfo.packageName)
+ .collect(Collectors.joining(", ")));
+ }
+
+ // Assume the first ResolveInfo is the one we're looking for
+ ResolveInfo info = resolveInfos.get(0);
+ mHalfSheetApkPkgName = info.activityInfo.applicationInfo.packageName;
+ Log.i("FastPairManager", "Found halfsheet APK at: " + mHalfSheetApkPkgName);
+ return mHalfSheetApkPkgName;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairUiServiceImpl.java b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairUiServiceImpl.java
new file mode 100644
index 0000000..3bd273e
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairUiServiceImpl.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.halfsheet;
+
+import static com.android.server.nearby.fastpair.Constant.TAG;
+
+import android.nearby.FastPairDevice;
+import android.nearby.FastPairStatusCallback;
+import android.nearby.PairStatusMetadata;
+import android.nearby.aidl.IFastPairStatusCallback;
+import android.nearby.aidl.IFastPairUiService;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.server.nearby.fastpair.FastPairController;
+
+/**
+ * Service implementing Fast Pair functionality.
+ *
+ * @hide
+ */
+public class FastPairUiServiceImpl extends IFastPairUiService.Stub {
+
+ private IBinder mStatusCallbackProxy;
+ private FastPairController mFastPairController;
+ private FastPairStatusCallback mFastPairStatusCallback;
+
+ /**
+ * Registers the Binder call back in the server notifies the proxy when there is an update
+ * in the server.
+ */
+ @Override
+ public void registerCallback(IFastPairStatusCallback iFastPairStatusCallback) {
+ mStatusCallbackProxy = iFastPairStatusCallback.asBinder();
+ mFastPairStatusCallback = new FastPairStatusCallback() {
+ @Override
+ public void onPairUpdate(FastPairDevice fastPairDevice,
+ PairStatusMetadata pairStatusMetadata) {
+ try {
+ iFastPairStatusCallback.onPairUpdate(fastPairDevice, pairStatusMetadata);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to update pair status.", e);
+ }
+ }
+ };
+ }
+
+ /**
+ * Unregisters the Binder call back in the server.
+ */
+ @Override
+ public void unregisterCallback(IFastPairStatusCallback iFastPairStatusCallback) {
+ mStatusCallbackProxy = null;
+ mFastPairStatusCallback = null;
+ }
+
+ /**
+ * Asks the Fast Pair service to pair the device. initial pairing.
+ */
+ @Override
+ public void connect(FastPairDevice fastPairDevice) {
+ if (mFastPairController != null) {
+ mFastPairController.pair(fastPairDevice);
+ } else {
+ Log.w(TAG, "Failed to connect because there is no FastPairController.");
+ }
+ }
+
+ /**
+ * Cancels Fast Pair connection and dismisses half sheet.
+ */
+ @Override
+ public void cancel(FastPairDevice fastPairDevice) {
+ }
+
+ public FastPairStatusCallback getPairStatusCallback() {
+ return mFastPairStatusCallback;
+ }
+
+ /**
+ * Sets function for Fast Pair controller.
+ */
+ public void setFastPairController(FastPairController fastPairController) {
+ mFastPairController = fastPairController;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationManager.java b/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationManager.java
new file mode 100644
index 0000000..b1ae573
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationManager.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.notification;
+
+
+import android.annotation.Nullable;
+import android.content.Context;
+
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+
+/**
+ * Responsible for show notification logic.
+ */
+public class FastPairNotificationManager {
+
+ /**
+ * FastPair notification manager that handle notification ui for fast pair.
+ */
+ public FastPairNotificationManager(Context context, DiscoveryItem item, boolean useLargeIcon,
+ int notificationId) {
+ }
+ /**
+ * FastPair notification manager that handle notification ui for fast pair.
+ */
+ public FastPairNotificationManager(Context context, DiscoveryItem item, boolean useLargeIcon) {
+
+ }
+
+ /**
+ * Shows pairing in progress notification.
+ */
+ public void showConnectingNotification() {}
+
+ /**
+ * Shows success notification
+ */
+ public void showPairingSucceededNotification(
+ @Nullable String companionApp,
+ int batteryLevel,
+ @Nullable String deviceName,
+ String address) {
+
+ }
+
+ /**
+ * Shows failed notification.
+ */
+ public void showPairingFailedNotification(byte[] accountKey) {
+
+ }
+
+ /**
+ * Notify the pairing process is done.
+ */
+ public void notifyPairingProcessDone(boolean success, boolean forceNotify,
+ String privateAddress, String publicAddress) {}
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandler.java
new file mode 100644
index 0000000..c95f74f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandler.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.pairinghandler;
+
+
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+
+import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
+import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
+import com.android.server.nearby.intdefs.NearbyEventIntDefs;
+
+/** Pairing progress handler that handle pairing come from half sheet. */
+public final class HalfSheetPairingProgressHandler extends PairingProgressHandlerBase {
+
+ private final FastPairHalfSheetManager mFastPairHalfSheetManager;
+ private final boolean mIsSubsequentPair;
+ private final DiscoveryItem mItemResurface;
+
+ HalfSheetPairingProgressHandler(
+ Context context,
+ DiscoveryItem item,
+ @Nullable String companionApp,
+ @Nullable byte[] accountKey) {
+ super(context, item);
+ this.mFastPairHalfSheetManager = Locator.get(context, FastPairHalfSheetManager.class);
+ this.mIsSubsequentPair =
+ item.getAuthenticationPublicKeySecp256R1() != null && accountKey != null;
+ this.mItemResurface = item;
+ }
+
+ @Override
+ protected int getPairStartEventCode() {
+ return mIsSubsequentPair ? NearbyEventIntDefs.EventCode.SUBSEQUENT_PAIR_START
+ : NearbyEventIntDefs.EventCode.MAGIC_PAIR_START;
+ }
+
+ @Override
+ protected int getPairEndEventCode() {
+ return mIsSubsequentPair ? NearbyEventIntDefs.EventCode.SUBSEQUENT_PAIR_END
+ : NearbyEventIntDefs.EventCode.MAGIC_PAIR_END;
+ }
+
+ @Override
+ public void onPairingStarted() {
+ super.onPairingStarted();
+ // Half sheet is not in the foreground reshow half sheet, also avoid showing HalfSheet on TV
+ if (!mFastPairHalfSheetManager.getHalfSheetForegroundState()) {
+ mFastPairHalfSheetManager.showPairingHalfSheet(mItemResurface);
+ }
+ mFastPairHalfSheetManager.disableDismissRunnable();
+ }
+
+ @Override
+ public void onHandlePasskeyConfirmation(BluetoothDevice device, int passkey) {
+ super.onHandlePasskeyConfirmation(device, passkey);
+ mFastPairHalfSheetManager.showPasskeyConfirmation(device, passkey);
+ }
+
+ @Nullable
+ @Override
+ public String onPairedCallbackCalled(
+ FastPairConnection connection,
+ byte[] accountKey,
+ FootprintsDeviceManager footprints,
+ String address) {
+ String deviceName = super.onPairedCallbackCalled(connection, accountKey,
+ footprints, address);
+ mFastPairHalfSheetManager.showPairingSuccessHalfSheet(address);
+ mFastPairHalfSheetManager.disableDismissRunnable();
+ return deviceName;
+ }
+
+ @Override
+ public void onPairingFailed(Throwable throwable) {
+ super.onPairingFailed(throwable);
+ mFastPairHalfSheetManager.disableDismissRunnable();
+ mFastPairHalfSheetManager.showPairingFailed();
+ mFastPairHalfSheetManager.notifyPairingProcessDone(
+ /* success= */ false, /* publicAddress= */ null, mItem);
+ // fix auto rebond issue
+ mFastPairHalfSheetManager.destroyBluetoothPairController();
+ }
+
+ @Override
+ public void onPairingSuccess(String address) {
+ super.onPairingSuccess(address);
+ mFastPairHalfSheetManager.disableDismissRunnable();
+ mFastPairHalfSheetManager
+ .notifyPairingProcessDone(/* success= */ true, address, mItem);
+ mFastPairHalfSheetManager.destroyBluetoothPairController();
+ }
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandler.java
new file mode 100644
index 0000000..d469c45
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandler.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.pairinghandler;
+
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
+import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
+import com.android.server.nearby.intdefs.NearbyEventIntDefs;
+
+/** Pairing progress handler for pairing coming from notifications. */
+@SuppressWarnings("nullness")
+public class NotificationPairingProgressHandler extends PairingProgressHandlerBase {
+ private final FastPairNotificationManager mFastPairNotificationManager;
+ @Nullable
+ private final String mCompanionApp;
+ @Nullable
+ private final byte[] mAccountKey;
+ private final boolean mIsSubsequentPair;
+
+ NotificationPairingProgressHandler(
+ Context context,
+ DiscoveryItem item,
+ @Nullable String companionApp,
+ @Nullable byte[] accountKey,
+ FastPairNotificationManager mFastPairNotificationManager) {
+ super(context, item);
+ this.mFastPairNotificationManager = mFastPairNotificationManager;
+ this.mCompanionApp = companionApp;
+ this.mAccountKey = accountKey;
+ this.mIsSubsequentPair =
+ item.getAuthenticationPublicKeySecp256R1() != null && accountKey != null;
+ }
+
+ @Override
+ public int getPairStartEventCode() {
+ return mIsSubsequentPair ? NearbyEventIntDefs.EventCode.SUBSEQUENT_PAIR_START
+ : NearbyEventIntDefs.EventCode.MAGIC_PAIR_START;
+ }
+
+ @Override
+ public int getPairEndEventCode() {
+ return mIsSubsequentPair ? NearbyEventIntDefs.EventCode.SUBSEQUENT_PAIR_END
+ : NearbyEventIntDefs.EventCode.MAGIC_PAIR_END;
+ }
+
+ @Override
+ public void onReadyToPair() {
+ super.onReadyToPair();
+ mFastPairNotificationManager.showConnectingNotification();
+ }
+
+ @Override
+ public String onPairedCallbackCalled(
+ FastPairConnection connection,
+ byte[] accountKey,
+ FootprintsDeviceManager footprints,
+ String address) {
+ String deviceName = super.onPairedCallbackCalled(connection, accountKey, footprints,
+ address);
+
+ int batteryLevel = -1;
+
+ BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class);
+ BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
+ if (bluetoothAdapter != null) {
+ // Need to check battery level here set that to -1 for now
+ batteryLevel = -1;
+ } else {
+ Log.v(
+ "NotificationPairingProgressHandler",
+ "onPairedCallbackCalled getBatteryLevel failed,"
+ + " adapter is null");
+ }
+ mFastPairNotificationManager.showPairingSucceededNotification(
+ !TextUtils.isEmpty(mCompanionApp) ? mCompanionApp : null,
+ batteryLevel,
+ deviceName,
+ address);
+ return deviceName;
+ }
+
+ @Override
+ public void onPairingFailed(Throwable throwable) {
+ super.onPairingFailed(throwable);
+ mFastPairNotificationManager.showPairingFailedNotification(mAccountKey);
+ mFastPairNotificationManager.notifyPairingProcessDone(
+ /* success= */ false,
+ /* forceNotify= */ false,
+ /* privateAddress= */ mItem.getMacAddress(),
+ /* publicAddress= */ null);
+ }
+
+ @Override
+ public void onPairingSuccess(String address) {
+ super.onPairingSuccess(address);
+ mFastPairNotificationManager.notifyPairingProcessDone(
+ /* success= */ true,
+ /* forceNotify= */ false,
+ /* privateAddress= */ mItem.getMacAddress(),
+ /* publicAddress= */ address);
+ }
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java
new file mode 100644
index 0000000..ccd7e5e
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.pairinghandler;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
+import static com.android.server.nearby.fastpair.FastPairManager.isThroughFastPair2InitialPairing;
+
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.util.Log;
+
+import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
+import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+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.intdefs.FastPairEventIntDefs;
+
+/** Base class for pairing progress handler. */
+public abstract class PairingProgressHandlerBase {
+ protected final Context mContext;
+ protected final DiscoveryItem mItem;
+ @Nullable
+ private FastPairEventIntDefs.ErrorCode mRescueFromError;
+
+ protected abstract int getPairStartEventCode();
+
+ protected abstract int getPairEndEventCode();
+
+ protected PairingProgressHandlerBase(Context context, DiscoveryItem item) {
+ this.mContext = context;
+ this.mItem = item;
+ }
+
+
+ /**
+ * Pairing progress init function.
+ */
+ public static PairingProgressHandlerBase create(
+ Context context,
+ DiscoveryItem item,
+ @Nullable String companionApp,
+ @Nullable byte[] accountKey,
+ FootprintsDeviceManager footprints,
+ FastPairNotificationManager notificationManager,
+ FastPairHalfSheetManager fastPairHalfSheetManager,
+ boolean isRetroactivePair) {
+ PairingProgressHandlerBase pairingProgressHandlerBase;
+ // Disable half sheet on subsequent pairing
+ if (item.getAuthenticationPublicKeySecp256R1() != null
+ && accountKey != null) {
+ // Subsequent pairing
+ pairingProgressHandlerBase =
+ new NotificationPairingProgressHandler(
+ context, item, companionApp, accountKey, notificationManager);
+ } else {
+ pairingProgressHandlerBase =
+ new HalfSheetPairingProgressHandler(context, item, companionApp, accountKey);
+ }
+
+
+ Log.v("PairingHandler",
+ "PairingProgressHandler:Create "
+ + item.getMacAddress() + " for pairing");
+ return pairingProgressHandlerBase;
+ }
+
+
+ /**
+ * Function calls when pairing start.
+ */
+ public void onPairingStarted() {
+ Log.v("PairingHandler", "PairingProgressHandler:onPairingStarted");
+ }
+
+ /**
+ * Waits for screen to unlock.
+ */
+ public void onWaitForScreenUnlock() {
+ Log.v("PairingHandler", "PairingProgressHandler:onWaitForScreenUnlock");
+ }
+
+ /**
+ * Function calls when screen unlock.
+ */
+ public void onScreenUnlocked() {
+ Log.v("PairingHandler", "PairingProgressHandler:onScreenUnlocked");
+ }
+
+ /**
+ * Calls when the handler is ready to pair.
+ */
+ public void onReadyToPair() {
+ Log.v("PairingHandler", "PairingProgressHandler:onReadyToPair");
+ }
+
+ /**
+ * Helps to set up pairing preference.
+ */
+ public void onSetupPreferencesBuilder(Preferences.Builder builder) {
+ Log.v("PairingHandler", "PairingProgressHandler:onSetupPreferencesBuilder");
+ }
+
+ /**
+ * Calls when pairing setup complete.
+ */
+ public void onPairingSetupCompleted() {
+ Log.v("PairingHandler", "PairingProgressHandler:onPairingSetupCompleted");
+ }
+
+ /** Called while pairing if needs to handle the passkey confirmation by Ui. */
+ public void onHandlePasskeyConfirmation(BluetoothDevice device, int passkey) {
+ Log.v("PairingHandler", "PairingProgressHandler:onHandlePasskeyConfirmation");
+ }
+
+ /**
+ * In this callback, we know if it is a real initial pairing by existing account key, and do
+ * following things:
+ * <li>1, optIn footprint for initial pairing.
+ * <li>2, write the device name to provider
+ * <li>2.1, generate default personalized name for initial pairing or get the personalized name
+ * from footprint for subsequent pairing.
+ * <li>2.2, set alias name for the bluetooth device.
+ * <li>2.3, update the device name for connection to write into provider for initial pair.
+ * <li>3, suppress battery notifications until oobe finishes.
+ *
+ * @return display name of the pairing device
+ */
+ @Nullable
+ public String onPairedCallbackCalled(
+ FastPairConnection connection,
+ byte[] accountKey,
+ FootprintsDeviceManager footprints,
+ String address) {
+ Log.v("PairingHandler",
+ "PairingProgressHandler:onPairedCallbackCalled with address: "
+ + address);
+
+ byte[] existingAccountKey = connection.getExistingAccountKey();
+ optInFootprintsForInitialPairing(footprints, mItem, accountKey, existingAccountKey);
+ // Add support for naming the device
+ return null;
+ }
+
+ /**
+ * Gets the related info from db use account key.
+ */
+ @Nullable
+ public byte[] getKeyForLocalCache(
+ byte[] accountKey, FastPairConnection connection,
+ FastPairConnection.SharedSecret sharedSecret) {
+ Log.v("PairingHandler", "PairingProgressHandler:getKeyForLocalCache");
+ return accountKey != null ? accountKey : connection.getExistingAccountKey();
+ }
+
+ /**
+ * Function handles pairing fail.
+ */
+ public void onPairingFailed(Throwable throwable) {
+ Log.w("PairingHandler", "PairingProgressHandler:onPairingFailed");
+ }
+
+ /**
+ * Function handles pairing success.
+ */
+ public void onPairingSuccess(String address) {
+ Log.v("PairingHandler", "PairingProgressHandler:onPairingSuccess with address: "
+ + maskBluetoothAddress(address));
+ }
+
+ private static void optInFootprintsForInitialPairing(
+ FootprintsDeviceManager footprints,
+ DiscoveryItem item,
+ byte[] accountKey,
+ @Nullable byte[] existingAccountKey) {
+ if (isThroughFastPair2InitialPairing(item, accountKey) && existingAccountKey == null) {
+ // enable the save to footprint
+ Log.v("PairingHandler", "footprint should call opt in here");
+ }
+ }
+
+ /**
+ * Returns {@code true} if the PairingProgressHandler is running at the background.
+ *
+ * <p>In order to keep the following status notification shows as a heads up, we must wait for
+ * the screen unlocked to continue.
+ */
+ public boolean skipWaitingScreenUnlock() {
+ return false;
+ }
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/injector/ContextHubManagerAdapter.java b/nearby/service/java/com/android/server/nearby/injector/ContextHubManagerAdapter.java
new file mode 100644
index 0000000..9af0227
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/injector/ContextHubManagerAdapter.java
@@ -0,0 +1,77 @@
+/*
+ * 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.injector;
+
+import android.hardware.location.ContextHubClient;
+import android.hardware.location.ContextHubClientCallback;
+import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubManager;
+import android.hardware.location.ContextHubTransaction;
+import android.hardware.location.NanoAppState;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/** Wrap {@link ContextHubManager} for dependence injection. */
+public class ContextHubManagerAdapter {
+ private final ContextHubManager mManager;
+
+ public ContextHubManagerAdapter(ContextHubManager manager) {
+ mManager = manager;
+ }
+
+ /**
+ * Returns the list of ContextHubInfo objects describing the available Context Hubs.
+ *
+ * @return the list of ContextHubInfo objects
+ * @see ContextHubInfo
+ */
+ public List<ContextHubInfo> getContextHubs() {
+ return mManager.getContextHubs();
+ }
+
+ /**
+ * Requests a query for nanoapps loaded at the specified Context Hub.
+ *
+ * @param hubInfo the hub to query a list of nanoapps from
+ * @return the ContextHubTransaction of the request
+ * @throws NullPointerException if hubInfo is null
+ */
+ public ContextHubTransaction<List<NanoAppState>> queryNanoApps(ContextHubInfo hubInfo) {
+ return mManager.queryNanoApps(hubInfo);
+ }
+
+ /**
+ * Creates and registers a client and its callback with the Context Hub Service.
+ *
+ * <p>A client is registered with the Context Hub Service for a specified Context Hub. When the
+ * registration succeeds, the client can send messages to nanoapps through the returned {@link
+ * ContextHubClient} object, and receive notifications through the provided callback.
+ *
+ * @param hubInfo the hub to attach this client to
+ * @param executor the executor to invoke the callback
+ * @param callback the notification callback to register
+ * @return the registered client object
+ * @throws IllegalArgumentException if hubInfo does not represent a valid hub
+ * @throws IllegalStateException if there were too many registered clients at the service
+ * @throws NullPointerException if callback, hubInfo, or executor is null
+ */
+ public ContextHubClient createClient(
+ ContextHubInfo hubInfo, ContextHubClientCallback callback, Executor executor) {
+ return mManager.createClient(hubInfo, callback, executor);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/injector/Injector.java b/nearby/service/java/com/android/server/nearby/injector/Injector.java
new file mode 100644
index 0000000..f990dc9
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/injector/Injector.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.injector;
+
+import android.bluetooth.BluetoothAdapter;
+
+/**
+ * Nearby dependency injector. To be used for accessing various Nearby class instances and as a
+ * handle for mock injection.
+ */
+public interface Injector {
+
+ /** Get the BluetoothAdapter for BleDiscoveryProvider to scan. */
+ BluetoothAdapter getBluetoothAdapter();
+
+ /** Get the ContextHubManagerAdapter for ChreDiscoveryProvider to scan. */
+ ContextHubManagerAdapter getContextHubManagerAdapter();
+}
diff --git a/nearby/service/java/com/android/server/nearby/intdefs/FastPairEventIntDefs.java b/nearby/service/java/com/android/server/nearby/intdefs/FastPairEventIntDefs.java
new file mode 100644
index 0000000..8bb7980
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/intdefs/FastPairEventIntDefs.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.intdefs;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Holds integer definitions for FastPair. */
+public class FastPairEventIntDefs {
+
+ /** Fast Pair Bond State. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ BondState.UNKNOWN_BOND_STATE,
+ BondState.NONE,
+ BondState.BONDING,
+ BondState.BONDED,
+ })
+ public @interface BondState {
+ int UNKNOWN_BOND_STATE = 0;
+ int NONE = 10;
+ int BONDING = 11;
+ int BONDED = 12;
+ }
+
+ /** Fast Pair error code. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ ErrorCode.UNKNOWN_ERROR_CODE,
+ ErrorCode.OTHER_ERROR,
+ ErrorCode.TIMEOUT,
+ ErrorCode.INTERRUPTED,
+ ErrorCode.REFLECTIVE_OPERATION_EXCEPTION,
+ ErrorCode.EXECUTION_EXCEPTION,
+ ErrorCode.PARSE_EXCEPTION,
+ ErrorCode.MDH_REMOTE_EXCEPTION,
+ ErrorCode.SUCCESS_RETRY_GATT_ERROR,
+ ErrorCode.SUCCESS_RETRY_GATT_TIMEOUT,
+ ErrorCode.SUCCESS_RETRY_SECRET_HANDSHAKE_ERROR,
+ ErrorCode.SUCCESS_RETRY_SECRET_HANDSHAKE_TIMEOUT,
+ ErrorCode.SUCCESS_SECRET_HANDSHAKE_RECONNECT,
+ ErrorCode.SUCCESS_ADDRESS_ROTATE,
+ ErrorCode.SUCCESS_SIGNAL_LOST,
+ })
+ public @interface ErrorCode {
+ int UNKNOWN_ERROR_CODE = 0;
+
+ // Check the other fields for a more specific error code.
+ int OTHER_ERROR = 1;
+
+ // The operation timed out.
+ int TIMEOUT = 2;
+
+ // The thread was interrupted.
+ int INTERRUPTED = 3;
+
+ // Some reflective call failed (should never happen).
+ int REFLECTIVE_OPERATION_EXCEPTION = 4;
+
+ // A Future threw an exception (should never happen).
+ int EXECUTION_EXCEPTION = 5;
+
+ // Parsing something (e.g. BR/EDR Handover data) failed.
+ int PARSE_EXCEPTION = 6;
+
+ // A failure at MDH.
+ int MDH_REMOTE_EXCEPTION = 7;
+
+ // For errors on GATT connection and retry success
+ int SUCCESS_RETRY_GATT_ERROR = 8;
+
+ // For timeout on GATT connection and retry success
+ int SUCCESS_RETRY_GATT_TIMEOUT = 9;
+
+ // For errors on secret handshake and retry success
+ int SUCCESS_RETRY_SECRET_HANDSHAKE_ERROR = 10;
+
+ // For timeout on secret handshake and retry success
+ int SUCCESS_RETRY_SECRET_HANDSHAKE_TIMEOUT = 11;
+
+ // For secret handshake fail and restart GATT connection success
+ int SUCCESS_SECRET_HANDSHAKE_RECONNECT = 12;
+
+ // For address rotate and retry with new address success
+ int SUCCESS_ADDRESS_ROTATE = 13;
+
+ // For signal lost and retry with old address still success
+ int SUCCESS_SIGNAL_LOST = 14;
+ }
+
+ /** Fast Pair BrEdrHandover Error Code. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ BrEdrHandoverErrorCode.UNKNOWN_BR_EDR_HANDOVER_ERROR_CODE,
+ BrEdrHandoverErrorCode.CONTROL_POINT_RESULT_CODE_NOT_SUCCESS,
+ BrEdrHandoverErrorCode.BLUETOOTH_MAC_INVALID,
+ BrEdrHandoverErrorCode.TRANSPORT_BLOCK_INVALID,
+ })
+ public @interface BrEdrHandoverErrorCode {
+ int UNKNOWN_BR_EDR_HANDOVER_ERROR_CODE = 0;
+ int CONTROL_POINT_RESULT_CODE_NOT_SUCCESS = 1;
+ int BLUETOOTH_MAC_INVALID = 2;
+ int TRANSPORT_BLOCK_INVALID = 3;
+ }
+
+ /** Fast Pair CreateBound Error Code. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ CreateBondErrorCode.UNKNOWN_BOND_ERROR_CODE,
+ CreateBondErrorCode.BOND_BROKEN,
+ CreateBondErrorCode.POSSIBLE_MITM,
+ CreateBondErrorCode.NO_PERMISSION,
+ CreateBondErrorCode.INCORRECT_VARIANT,
+ CreateBondErrorCode.FAILED_BUT_ALREADY_RECEIVE_PASS_KEY,
+ })
+ public @interface CreateBondErrorCode {
+ int UNKNOWN_BOND_ERROR_CODE = 0;
+ int BOND_BROKEN = 1;
+ int POSSIBLE_MITM = 2;
+ int NO_PERMISSION = 3;
+ int INCORRECT_VARIANT = 4;
+ int FAILED_BUT_ALREADY_RECEIVE_PASS_KEY = 5;
+ }
+
+ /** Fast Pair Connect Error Code. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ ConnectErrorCode.UNKNOWN_CONNECT_ERROR_CODE,
+ ConnectErrorCode.UNSUPPORTED_PROFILE,
+ ConnectErrorCode.GET_PROFILE_PROXY_FAILED,
+ ConnectErrorCode.DISCONNECTED,
+ ConnectErrorCode.LINK_KEY_CLEARED,
+ ConnectErrorCode.FAIL_TO_DISCOVERY,
+ ConnectErrorCode.DISCOVERY_NOT_FINISHED,
+ })
+ public @interface ConnectErrorCode {
+ int UNKNOWN_CONNECT_ERROR_CODE = 0;
+ int UNSUPPORTED_PROFILE = 1;
+ int GET_PROFILE_PROXY_FAILED = 2;
+ int DISCONNECTED = 3;
+ int LINK_KEY_CLEARED = 4;
+ int FAIL_TO_DISCOVERY = 5;
+ int DISCOVERY_NOT_FINISHED = 6;
+ }
+
+ private FastPairEventIntDefs() {}
+}
diff --git a/nearby/service/java/com/android/server/nearby/intdefs/NearbyEventIntDefs.java b/nearby/service/java/com/android/server/nearby/intdefs/NearbyEventIntDefs.java
new file mode 100644
index 0000000..91bf49a
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/intdefs/NearbyEventIntDefs.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.intdefs;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Holds integer definitions for NearbyEvent. */
+public class NearbyEventIntDefs {
+
+ /** NearbyEvent Code. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ EventCode.UNKNOWN_EVENT_TYPE,
+ EventCode.MAGIC_PAIR_START,
+ EventCode.WAIT_FOR_SCREEN_UNLOCK,
+ EventCode.GATT_CONNECT,
+ EventCode.BR_EDR_HANDOVER_WRITE_CONTROL_POINT_REQUEST,
+ EventCode.BR_EDR_HANDOVER_READ_BLUETOOTH_MAC,
+ EventCode.BR_EDR_HANDOVER_READ_TRANSPORT_BLOCK,
+ EventCode.GET_PROFILES_VIA_SDP,
+ EventCode.DISCOVER_DEVICE,
+ EventCode.CANCEL_DISCOVERY,
+ EventCode.REMOVE_BOND,
+ EventCode.CANCEL_BOND,
+ EventCode.CREATE_BOND,
+ EventCode.CONNECT_PROFILE,
+ EventCode.DISABLE_BLUETOOTH,
+ EventCode.ENABLE_BLUETOOTH,
+ EventCode.MAGIC_PAIR_END,
+ EventCode.SECRET_HANDSHAKE,
+ EventCode.WRITE_ACCOUNT_KEY,
+ EventCode.WRITE_TO_FOOTPRINTS,
+ EventCode.PASSKEY_EXCHANGE,
+ EventCode.DEVICE_RECOGNIZED,
+ EventCode.GET_LOCAL_PUBLIC_ADDRESS,
+ EventCode.DIRECTLY_CONNECTED_TO_PROFILE,
+ EventCode.DEVICE_ALIAS_CHANGED,
+ EventCode.WRITE_DEVICE_NAME,
+ EventCode.UPDATE_PROVIDER_NAME_START,
+ EventCode.UPDATE_PROVIDER_NAME_END,
+ EventCode.READ_FIRMWARE_VERSION,
+ EventCode.RETROACTIVE_PAIR_START,
+ EventCode.RETROACTIVE_PAIR_END,
+ EventCode.SUBSEQUENT_PAIR_START,
+ EventCode.SUBSEQUENT_PAIR_END,
+ EventCode.BISTO_PAIR_START,
+ EventCode.BISTO_PAIR_END,
+ EventCode.REMOTE_PAIR_START,
+ EventCode.REMOTE_PAIR_END,
+ EventCode.BEFORE_CREATE_BOND,
+ EventCode.BEFORE_CREATE_BOND_BONDING,
+ EventCode.BEFORE_CREATE_BOND_BONDED,
+ EventCode.BEFORE_CONNECT_PROFILE,
+ EventCode.HANDLE_PAIRING_REQUEST,
+ EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION,
+ EventCode.GATT_CONNECTION_AND_SECRET_HANDSHAKE,
+ EventCode.CHECK_SIGNAL_AFTER_HANDSHAKE,
+ EventCode.RECOVER_BY_RETRY_GATT,
+ EventCode.RECOVER_BY_RETRY_HANDSHAKE,
+ EventCode.RECOVER_BY_RETRY_HANDSHAKE_RECONNECT,
+ EventCode.GATT_HANDSHAKE_MANUAL_RETRY_ATTEMPTS,
+ EventCode.PAIR_WITH_CACHED_MODEL_ID,
+ EventCode.DIRECTLY_CONNECT_PROFILE_WITH_CACHED_ADDRESS,
+ EventCode.PAIR_WITH_NEW_MODEL,
+ })
+ public @interface EventCode {
+ int UNKNOWN_EVENT_TYPE = 0;
+
+ // Codes for Magic Pair.
+ // Starting at 1000 to not conflict with other existing codes (e.g.
+ // DiscoveryEvent) that may be migrated to become official Event Codes.
+ int MAGIC_PAIR_START = 1010;
+ int WAIT_FOR_SCREEN_UNLOCK = 1020;
+ int GATT_CONNECT = 1030;
+ int BR_EDR_HANDOVER_WRITE_CONTROL_POINT_REQUEST = 1040;
+ int BR_EDR_HANDOVER_READ_BLUETOOTH_MAC = 1050;
+ int BR_EDR_HANDOVER_READ_TRANSPORT_BLOCK = 1060;
+ int GET_PROFILES_VIA_SDP = 1070;
+ int DISCOVER_DEVICE = 1080;
+ int CANCEL_DISCOVERY = 1090;
+ int REMOVE_BOND = 1100;
+ int CANCEL_BOND = 1110;
+ int CREATE_BOND = 1120;
+ int CONNECT_PROFILE = 1130;
+ int DISABLE_BLUETOOTH = 1140;
+ int ENABLE_BLUETOOTH = 1150;
+ int MAGIC_PAIR_END = 1160;
+ int SECRET_HANDSHAKE = 1170;
+ int WRITE_ACCOUNT_KEY = 1180;
+ int WRITE_TO_FOOTPRINTS = 1190;
+ int PASSKEY_EXCHANGE = 1200;
+ int DEVICE_RECOGNIZED = 1210;
+ int GET_LOCAL_PUBLIC_ADDRESS = 1220;
+ int DIRECTLY_CONNECTED_TO_PROFILE = 1230;
+ int DEVICE_ALIAS_CHANGED = 1240;
+ int WRITE_DEVICE_NAME = 1250;
+ int UPDATE_PROVIDER_NAME_START = 1260;
+ int UPDATE_PROVIDER_NAME_END = 1270;
+ int READ_FIRMWARE_VERSION = 1280;
+ int RETROACTIVE_PAIR_START = 1290;
+ int RETROACTIVE_PAIR_END = 1300;
+ int SUBSEQUENT_PAIR_START = 1310;
+ int SUBSEQUENT_PAIR_END = 1320;
+ int BISTO_PAIR_START = 1330;
+ int BISTO_PAIR_END = 1340;
+ int REMOTE_PAIR_START = 1350;
+ int REMOTE_PAIR_END = 1360;
+ int BEFORE_CREATE_BOND = 1370;
+ int BEFORE_CREATE_BOND_BONDING = 1380;
+ int BEFORE_CREATE_BOND_BONDED = 1390;
+ int BEFORE_CONNECT_PROFILE = 1400;
+ int HANDLE_PAIRING_REQUEST = 1410;
+ int SECRET_HANDSHAKE_GATT_COMMUNICATION = 1420;
+ int GATT_CONNECTION_AND_SECRET_HANDSHAKE = 1430;
+ int CHECK_SIGNAL_AFTER_HANDSHAKE = 1440;
+ int RECOVER_BY_RETRY_GATT = 1450;
+ int RECOVER_BY_RETRY_HANDSHAKE = 1460;
+ int RECOVER_BY_RETRY_HANDSHAKE_RECONNECT = 1470;
+ int GATT_HANDSHAKE_MANUAL_RETRY_ATTEMPTS = 1480;
+ int PAIR_WITH_CACHED_MODEL_ID = 1490;
+ int DIRECTLY_CONNECT_PROFILE_WITH_CACHED_ADDRESS = 1500;
+ int PAIR_WITH_NEW_MODEL = 1510;
+ }
+
+ private NearbyEventIntDefs() {}
+}
diff --git a/nearby/service/java/com/android/server/nearby/metrics/NearbyMetrics.java b/nearby/service/java/com/android/server/nearby/metrics/NearbyMetrics.java
new file mode 100644
index 0000000..75815f1
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/metrics/NearbyMetrics.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.metrics;
+
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.ScanRequest;
+import android.os.WorkSource;
+
+import com.android.server.nearby.proto.NearbyStatsLog;
+
+/**
+ * A class to collect and report Nearby metrics.
+ */
+public class NearbyMetrics {
+ /**
+ * Logs a scan started event.
+ */
+ public static void logScanStarted(int scanSessionId, ScanRequest scanRequest) {
+ NearbyStatsLog.write(
+ NearbyStatsLog.NEARBY_DEVICE_SCAN_STATE_CHANGED,
+ getUid(scanRequest),
+ scanSessionId,
+ NearbyStatsLog
+ .NEARBY_DEVICE_SCAN_STATE_CHANGED__SCAN_STATE__NEARBY_SCAN_STATE_STARTED,
+ scanRequest.getScanType(),
+ 0,
+ 0,
+ "",
+ "");
+ }
+
+ /**
+ * Logs a scan stopped event.
+ */
+ public static void logScanStopped(int scanSessionId, ScanRequest scanRequest) {
+ NearbyStatsLog.write(
+ NearbyStatsLog.NEARBY_DEVICE_SCAN_STATE_CHANGED,
+ getUid(scanRequest),
+ scanSessionId,
+ NearbyStatsLog
+ .NEARBY_DEVICE_SCAN_STATE_CHANGED__SCAN_STATE__NEARBY_SCAN_STATE_STOPPED,
+ scanRequest.getScanType(),
+ 0,
+ 0,
+ "",
+ "");
+ }
+
+ /**
+ * Logs a scan device discovered event.
+ */
+ public static void logScanDeviceDiscovered(int scanSessionId, ScanRequest scanRequest,
+ NearbyDeviceParcelable nearbyDevice) {
+ NearbyStatsLog.write(
+ NearbyStatsLog.NEARBY_DEVICE_SCAN_STATE_CHANGED,
+ getUid(scanRequest),
+ scanSessionId,
+ NearbyStatsLog
+ .NEARBY_DEVICE_SCAN_STATE_CHANGED__SCAN_STATE__NEARBY_SCAN_STATE_DISCOVERED,
+ scanRequest.getScanType(),
+ nearbyDevice.getMedium(),
+ nearbyDevice.getRssi(),
+ nearbyDevice.getFastPairModelId(),
+ "");
+ }
+
+ private static int getUid(ScanRequest scanRequest) {
+ WorkSource workSource = scanRequest.getWorkSource();
+ return workSource.isEmpty() ? -1 : workSource.getUid(0);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java b/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java
new file mode 100644
index 0000000..e4df673
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import android.annotation.Nullable;
+import android.nearby.BroadcastRequest;
+import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PresenceCredential;
+
+import com.android.internal.util.Preconditions;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+
+/**
+ * A Nearby Presence advertisement to be advertised on BT4.2 devices.
+ *
+ * <p>Serializable between Java object and bytes formats. Java object is used at the upper scanning
+ * and advertising interface as an abstraction of the actual bytes. Bytes format is used at the
+ * underlying BLE and mDNS stacks, which do necessary slicing and merging based on advertising
+ * capacities.
+ */
+// The fast advertisement is defined in the format below:
+// Header (1 byte) | salt (2 bytes) | identity (14 bytes) | tx_power (1 byte) | actions (1~ bytes)
+// The header contains:
+// version (3 bits) | provision_mode_flag (1 bit) | identity_type (3 bits) |
+// extended_advertisement_mode (1 bit)
+public class FastAdvertisement {
+
+ private static final int FAST_ADVERTISEMENT_MAX_LENGTH = 24;
+
+ static final byte INVALID_TX_POWER = (byte) 0xFF;
+
+ static final int HEADER_LENGTH = 1;
+
+ static final int SALT_LENGTH = 2;
+
+ static final int IDENTITY_LENGTH = 14;
+
+ static final int TX_POWER_LENGTH = 1;
+
+ private static final int MAX_ACTION_COUNT = 6;
+
+ /**
+ * Creates a {@link FastAdvertisement} from a Presence Broadcast Request.
+ */
+ public static FastAdvertisement createFromRequest(PresenceBroadcastRequest request) {
+ byte[] salt = request.getSalt();
+ byte[] identity = request.getCredential().getMetadataEncryptionKey();
+ List<Integer> actions = request.getActions();
+ Preconditions.checkArgument(
+ salt.length == SALT_LENGTH,
+ "FastAdvertisement's salt does not match correct length");
+ Preconditions.checkArgument(
+ identity.length == IDENTITY_LENGTH,
+ "FastAdvertisement's identity does not match correct length");
+ Preconditions.checkArgument(
+ !actions.isEmpty(), "FastAdvertisement must contain at least one action");
+ Preconditions.checkArgument(
+ actions.size() <= MAX_ACTION_COUNT,
+ "FastAdvertisement advertised actions cannot exceed max count " + MAX_ACTION_COUNT);
+
+ return new FastAdvertisement(
+ request.getCredential().getIdentityType(),
+ identity,
+ salt,
+ actions,
+ (byte) request.getTxPower());
+ }
+
+ /** Serialize an {@link FastAdvertisement} object into bytes. */
+ public byte[] toBytes() {
+ ByteBuffer buffer = ByteBuffer.allocate(getLength());
+
+ buffer.put(FastAdvertisementUtils.constructHeader(getVersion(), mIdentityType));
+ buffer.put(mSalt);
+ buffer.put(getIdentity());
+
+ buffer.put(mTxPower == null ? INVALID_TX_POWER : mTxPower);
+ for (int action : mActions) {
+ buffer.put((byte) action);
+ }
+ return buffer.array();
+ }
+
+ private final int mLength;
+
+ private final int mLtvFieldCount;
+
+ @PresenceCredential.IdentityType private final int mIdentityType;
+
+ private final byte[] mIdentity;
+
+ private final byte[] mSalt;
+
+ private final List<Integer> mActions;
+
+ @Nullable
+ private final Byte mTxPower;
+
+ FastAdvertisement(
+ @PresenceCredential.IdentityType int identityType,
+ byte[] identity,
+ byte[] salt,
+ List<Integer> actions,
+ @Nullable Byte txPower) {
+ this.mIdentityType = identityType;
+ this.mIdentity = identity;
+ this.mSalt = salt;
+ this.mActions = actions;
+ this.mTxPower = txPower;
+ int ltvFieldCount = 3;
+ int length =
+ HEADER_LENGTH // header
+ + identity.length
+ + salt.length
+ + actions.size();
+ length += TX_POWER_LENGTH;
+ if (txPower != null) { // TX power
+ ltvFieldCount += 1;
+ }
+ this.mLength = length;
+ this.mLtvFieldCount = ltvFieldCount;
+ Preconditions.checkArgument(
+ length <= FAST_ADVERTISEMENT_MAX_LENGTH,
+ "FastAdvertisement exceeds maximum length");
+ }
+
+ /** Returns the version in the advertisement. */
+ @BroadcastRequest.BroadcastVersion
+ public int getVersion() {
+ return BroadcastRequest.PRESENCE_VERSION_V0;
+ }
+
+ /** Returns the identity type in the advertisement. */
+ @PresenceCredential.IdentityType
+ public int getIdentityType() {
+ return mIdentityType;
+ }
+
+ /** Returns the identity bytes in the advertisement. */
+ public byte[] getIdentity() {
+ return mIdentity.clone();
+ }
+
+ /** Returns the salt of the advertisement. */
+ public byte[] getSalt() {
+ return mSalt.clone();
+ }
+
+ /** Returns the actions in the advertisement. */
+ public List<Integer> getActions() {
+ return new ArrayList<>(mActions);
+ }
+
+ /** Returns the adjusted TX Power in the advertisement. Null if not available. */
+ @Nullable
+ public Byte getTxPower() {
+ return mTxPower;
+ }
+
+ /** Returns the length of the advertisement. */
+ public int getLength() {
+ return mLength;
+ }
+
+ /** Returns the count of LTV fields in the advertisement. */
+ public int getLtvFieldCount() {
+ return mLtvFieldCount;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "FastAdvertisement:<VERSION: %s, length: %s, ltvFieldCount: %s, identityType: %s,"
+ + " identity: %s, salt: %s, actions: %s, txPower: %s",
+ getVersion(),
+ getLength(),
+ getLtvFieldCount(),
+ getIdentityType(),
+ Arrays.toString(getIdentity()),
+ Arrays.toString(getSalt()),
+ getActions(),
+ getTxPower());
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/FastAdvertisementUtils.java b/nearby/service/java/com/android/server/nearby/presence/FastAdvertisementUtils.java
new file mode 100644
index 0000000..ab0a246
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/FastAdvertisementUtils.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import android.nearby.BroadcastRequest;
+
+/**
+ * Provides serialization and deserialization util methods for {@link FastAdvertisement}.
+ */
+public final class FastAdvertisementUtils {
+
+ private static final int VERSION_MASK = 0b11100000;
+
+ private static final int IDENTITY_TYPE_MASK = 0b00001110;
+
+ /**
+ * Constructs the header of a {@link FastAdvertisement}.
+ */
+ public static byte constructHeader(@BroadcastRequest.BroadcastVersion int version,
+ int identityType) {
+ return (byte) (((version << 5) & VERSION_MASK) | ((identityType << 1)
+ & IDENTITY_TYPE_MASK));
+ }
+
+ private FastAdvertisementUtils() {}
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java b/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
new file mode 100644
index 0000000..c355df2
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.to128BitUuid;
+
+import java.util.UUID;
+
+/**
+ * Constants for Nearby Presence operations.
+ */
+public class PresenceConstants {
+
+ /** Presence advertisement service data uuid. */
+ public static final UUID PRESENCE_UUID = to128BitUuid((short) 0xFCF1);
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java b/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
new file mode 100644
index 0000000..80ad88d
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import android.nearby.NearbyDevice;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceDevice;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Represents a Presence discovery result. */
+public class PresenceDiscoveryResult {
+
+ /** Creates a {@link PresenceDiscoveryResult} from the scan data. */
+ public static PresenceDiscoveryResult fromDevice(NearbyDeviceParcelable device) {
+ return new PresenceDiscoveryResult.Builder()
+ .setTxPower(device.getTxPower())
+ .setRssi(device.getRssi())
+ .addPresenceAction(device.getAction())
+ .setPublicCredential(device.getPublicCredential())
+ .build();
+ }
+
+ private final int mTxPower;
+ private final int mRssi;
+ private final byte[] mSalt;
+ private final List<Integer> mPresenceActions;
+ private final PublicCredential mPublicCredential;
+
+ private PresenceDiscoveryResult(
+ int txPower,
+ int rssi,
+ byte[] salt,
+ List<Integer> presenceActions,
+ PublicCredential publicCredential) {
+ mTxPower = txPower;
+ mRssi = rssi;
+ mSalt = salt;
+ mPresenceActions = presenceActions;
+ mPublicCredential = publicCredential;
+ }
+
+ /** Returns whether the discovery result matches the scan filter. */
+ public boolean matches(PresenceScanFilter scanFilter) {
+ return pathLossMatches(scanFilter.getMaxPathLoss())
+ && actionMatches(scanFilter.getPresenceActions())
+ && credentialMatches(scanFilter.getCredentials());
+ }
+
+ private boolean pathLossMatches(int maxPathLoss) {
+ return (mTxPower - mRssi) <= maxPathLoss;
+ }
+
+ private boolean actionMatches(List<Integer> filterActions) {
+ if (filterActions.isEmpty()) {
+ return true;
+ }
+ return filterActions.stream().anyMatch(mPresenceActions::contains);
+ }
+
+ private boolean credentialMatches(List<PublicCredential> credentials) {
+ return credentials.contains(mPublicCredential);
+ }
+
+ /** Converts a presence device from the discovery result. */
+ public PresenceDevice toPresenceDevice() {
+ return new PresenceDevice.Builder(
+ // Use the public credential hash as the device Id.
+ String.valueOf(mPublicCredential.hashCode()),
+ mSalt,
+ mPublicCredential.getSecretId(),
+ mPublicCredential.getEncryptedMetadata())
+ .setRssi(mRssi)
+ .addMedium(NearbyDevice.Medium.BLE)
+ .build();
+ }
+
+ /** Builder for {@link PresenceDiscoveryResult}. */
+ public static class Builder {
+ private int mTxPower;
+ private int mRssi;
+ private byte[] mSalt;
+
+ private PublicCredential mPublicCredential;
+ private final List<Integer> mPresenceActions;
+
+ public Builder() {
+ mPresenceActions = new ArrayList<>();
+ }
+
+ /** Sets the calibrated tx power for the discovery result. */
+ public Builder setTxPower(int txPower) {
+ mTxPower = txPower;
+ return this;
+ }
+
+ /** Sets the rssi for the discovery result. */
+ public Builder setRssi(int rssi) {
+ mRssi = rssi;
+ return this;
+ }
+
+ /** Sets the salt for the discovery result. */
+ public Builder setSalt(byte[] salt) {
+ mSalt = salt;
+ return this;
+ }
+
+ /** Sets the public credential for the discovery result. */
+ public Builder setPublicCredential(PublicCredential publicCredential) {
+ mPublicCredential = publicCredential;
+ return this;
+ }
+
+ /** Adds presence action of the discovery result. */
+ public Builder addPresenceAction(int presenceAction) {
+ mPresenceActions.add(presenceAction);
+ return this;
+ }
+
+ /** Builds a {@link PresenceDiscoveryResult}. */
+ public PresenceDiscoveryResult build() {
+ return new PresenceDiscoveryResult(
+ mTxPower, mRssi, mSalt, mPresenceActions, mPublicCredential);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
new file mode 100644
index 0000000..382c47a
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.nearby.NearbyDevice;
+import android.nearby.NearbyManager;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanCallback;
+import android.nearby.ScanRequest;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+
+import java.util.Locale;
+import java.util.concurrent.Executors;
+
+/** PresenceManager is the class initiated in nearby service to handle presence related work. */
+public class PresenceManager {
+
+ final LocatorContextWrapper mLocatorContextWrapper;
+ final Locator mLocator;
+ private final IntentFilter mIntentFilter;
+
+ private final ScanCallback mScanCallback =
+ new ScanCallback() {
+ @Override
+ public void onDiscovered(@NonNull NearbyDevice device) {
+ Log.i(TAG, "[PresenceManager] discovered Device.");
+ }
+
+ @Override
+ public void onUpdated(@NonNull NearbyDevice device) {}
+
+ @Override
+ public void onLost(@NonNull NearbyDevice device) {}
+ };
+
+ private final BroadcastReceiver mScreenBroadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ NearbyManager manager = getNearbyManager();
+ if (manager == null) {
+ Log.e(TAG, "Nearby Manager is null");
+ return;
+ }
+ if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {
+ Log.d(TAG, "Start CHRE scan.");
+ byte[] secreteId = {1, 0, 0, 0};
+ byte[] authenticityKey = {2, 0, 0, 0};
+ byte[] publicKey = {3, 0, 0, 0};
+ byte[] encryptedMetaData = {4, 0, 0, 0};
+ byte[] encryptedMetaDataTag = {5, 0, 0, 0};
+ PublicCredential publicCredential =
+ new PublicCredential.Builder(
+ secreteId,
+ authenticityKey,
+ publicKey,
+ encryptedMetaData,
+ encryptedMetaDataTag)
+ .build();
+ PresenceScanFilter presenceScanFilter =
+ new PresenceScanFilter.Builder()
+ .setMaxPathLoss(3)
+ .addCredential(publicCredential)
+ .addPresenceAction(1)
+ .build();
+ ScanRequest scanRequest =
+ new ScanRequest.Builder()
+ .setScanType(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(presenceScanFilter)
+ .build();
+ Log.i(
+ TAG,
+ String.format(
+ Locale.getDefault(),
+ "[PresenceManager] Start Presence scan with request: %s",
+ scanRequest.toString()));
+ manager.startScan(
+ scanRequest, Executors.newSingleThreadExecutor(), mScanCallback);
+ } else if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
+ Log.d(TAG, "Stop CHRE scan.");
+ manager.stopScan(mScanCallback);
+ }
+ }
+ };
+
+ public PresenceManager(LocatorContextWrapper contextWrapper) {
+ mLocatorContextWrapper = contextWrapper;
+ mLocator = mLocatorContextWrapper.getLocator();
+ mIntentFilter = new IntentFilter();
+ }
+
+ /** Null when the Nearby Service is not available. */
+ @Nullable
+ private NearbyManager getNearbyManager() {
+ return (NearbyManager)
+ mLocatorContextWrapper
+ .getApplicationContext()
+ .getSystemService(Context.NEARBY_SERVICE);
+ }
+
+ /** Function called when nearby service start. */
+ public void initiate() {
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+ mLocatorContextWrapper
+ .getContext()
+ .registerReceiver(mScreenBroadcastReceiver, mIntentFilter);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
new file mode 100644
index 0000000..f136695
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.content.Context;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Base class for all discovery providers.
+ *
+ * @hide
+ */
+public abstract class AbstractDiscoveryProvider {
+
+ protected final Context mContext;
+ protected final DiscoveryProviderController mController;
+ protected final Executor mExecutor;
+ protected Listener mListener;
+ protected List<ScanFilter> mScanFilters;
+
+ /** Interface for listening to discovery providers. */
+ public interface Listener {
+ /**
+ * Called when a provider has a new nearby device available. May be invoked from any thread.
+ */
+ void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice);
+ }
+
+ protected AbstractDiscoveryProvider(Context context, Executor executor) {
+ mContext = context;
+ mExecutor = executor;
+ mController = new Controller();
+ }
+
+ /**
+ * Callback invoked when the provider is started, and signals that other callback invocations
+ * can now be expected. Always implies that the provider request is set to the empty request.
+ * Always invoked on the provider executor.
+ */
+ protected void onStart() {}
+
+ /**
+ * Callback invoked when the provider is stopped, and signals that no further callback
+ * invocations will occur (until a further call to {@link #onStart()}. Always invoked on the
+ * provider executor.
+ */
+ protected void onStop() {}
+
+ /**
+ * Callback invoked to inform the provider of a new provider request which replaces any prior
+ * provider request. Always invoked on the provider executor.
+ */
+ protected void invalidateScanMode() {}
+
+ /**
+ * Retrieves the controller for this discovery provider. Should never be invoked by subclasses,
+ * as a discovery provider should not be controlling itself. Using this method from subclasses
+ * could also result in deadlock.
+ */
+ protected DiscoveryProviderController getController() {
+ return mController;
+ }
+
+ private class Controller implements DiscoveryProviderController {
+
+ private boolean mStarted = false;
+ private @ScanRequest.ScanMode int mScanMode;
+
+ @Override
+ public void setListener(@Nullable Listener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public boolean isStarted() {
+ return mStarted;
+ }
+
+ @Override
+ public void start() {
+ if (mStarted) {
+ Log.d(TAG, "Provider already started.");
+ return;
+ }
+ mStarted = true;
+ mExecutor.execute(AbstractDiscoveryProvider.this::onStart);
+ }
+
+ @Override
+ public void stop() {
+ if (!mStarted) {
+ Log.d(TAG, "Provider already stopped.");
+ return;
+ }
+ mStarted = false;
+ mExecutor.execute(AbstractDiscoveryProvider.this::onStop);
+ }
+
+ @Override
+ public void setProviderScanMode(@ScanRequest.ScanMode int scanMode) {
+ if (mScanMode == scanMode) {
+ Log.d(TAG, "Provider already in desired scan mode.");
+ return;
+ }
+ mScanMode = scanMode;
+ mExecutor.execute(AbstractDiscoveryProvider.this::invalidateScanMode);
+ }
+
+ @ScanRequest.ScanMode
+ @Override
+ public int getProviderScanMode() {
+ return mScanMode;
+ }
+
+ @Override
+ public void setProviderScanFilters(List<ScanFilter> filters) {
+ mScanFilters = filters;
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
new file mode 100644
index 0000000..3602787
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.BluetoothLeAdvertiser;
+import android.nearby.BroadcastCallback;
+import android.os.ParcelUuid;
+
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.presence.PresenceConstants;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A provider for Bluetooth Low Energy advertisement.
+ */
+public class BleBroadcastProvider extends AdvertiseCallback {
+
+ /**
+ * Listener for Broadcast status changes.
+ */
+ interface BroadcastListener {
+ void onStatusChanged(int status);
+ }
+
+ private final Injector mInjector;
+ private final Executor mExecutor;
+
+ private BroadcastListener mBroadcastListener;
+ private boolean mIsAdvertising;
+
+ BleBroadcastProvider(Injector injector, Executor executor) {
+ mInjector = injector;
+ mExecutor = executor;
+ }
+
+ void start(byte[] advertisementPackets, BroadcastListener listener) {
+ if (mIsAdvertising) {
+ stop();
+ }
+ boolean advertiseStarted = false;
+ BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+ if (adapter != null) {
+ BluetoothLeAdvertiser bluetoothLeAdvertiser =
+ mInjector.getBluetoothAdapter().getBluetoothLeAdvertiser();
+ if (bluetoothLeAdvertiser != null) {
+ advertiseStarted = true;
+ AdvertiseSettings settings =
+ new AdvertiseSettings.Builder()
+ .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
+ .setConnectable(true)
+ .build();
+ AdvertiseData advertiseData =
+ new AdvertiseData.Builder()
+ .addServiceData(new ParcelUuid(PresenceConstants.PRESENCE_UUID),
+ advertisementPackets).build();
+
+ try {
+ mBroadcastListener = listener;
+ bluetoothLeAdvertiser.startAdvertising(settings, advertiseData, this);
+ } catch (NullPointerException | IllegalStateException | SecurityException e) {
+ advertiseStarted = false;
+ }
+ }
+ }
+ if (!advertiseStarted) {
+ listener.onStatusChanged(BroadcastCallback.STATUS_FAILURE);
+ }
+ }
+
+ void stop() {
+ if (mIsAdvertising) {
+ BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+ if (adapter != null) {
+ BluetoothLeAdvertiser bluetoothLeAdvertiser =
+ mInjector.getBluetoothAdapter().getBluetoothLeAdvertiser();
+ if (bluetoothLeAdvertiser != null) {
+ bluetoothLeAdvertiser.stopAdvertising(this);
+ }
+ }
+ mBroadcastListener = null;
+ mIsAdvertising = false;
+ }
+ }
+
+ @Override
+ public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+ mExecutor.execute(() -> {
+ if (mBroadcastListener != null) {
+ mBroadcastListener.onStatusChanged(BroadcastCallback.STATUS_OK);
+ }
+ mIsAdvertising = true;
+ });
+ }
+
+ @Override
+ public void onStartFailure(int errorCode) {
+ if (mBroadcastListener != null) {
+ mBroadcastListener.onStatusChanged(BroadcastCallback.STATUS_FAILURE);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
new file mode 100644
index 0000000..4cb6d8d
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.content.Context;
+import android.nearby.NearbyDevice;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.ScanRequest;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import com.android.server.nearby.common.bluetooth.fastpair.Constants;
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.presence.PresenceConstants;
+import com.android.server.nearby.util.ForegroundThread;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Discovery provider that uses Bluetooth Low Energy to do scanning.
+ */
+public class BleDiscoveryProvider extends AbstractDiscoveryProvider {
+
+ @VisibleForTesting
+ static final ParcelUuid FAST_PAIR_UUID = new ParcelUuid(Constants.FastPairService.ID);
+ private static final ParcelUuid PRESENCE_UUID = new ParcelUuid(PresenceConstants.PRESENCE_UUID);
+
+ // Don't block the thread as it may be used by other services.
+ private static final Executor NEARBY_EXECUTOR = ForegroundThread.getExecutor();
+ private final Injector mInjector;
+ private android.bluetooth.le.ScanCallback mScanCallback =
+ new android.bluetooth.le.ScanCallback() {
+ @Override
+ public void onScanResult(int callbackType, ScanResult scanResult) {
+ NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder();
+ builder.setMedium(NearbyDevice.Medium.BLE)
+ .setRssi(scanResult.getRssi())
+ .setTxPower(scanResult.getTxPower())
+ .setBluetoothAddress(scanResult.getDevice().getAddress());
+
+ ScanRecord record = scanResult.getScanRecord();
+ if (record != null) {
+ String deviceName = record.getDeviceName();
+ if (deviceName != null) {
+ builder.setName(record.getDeviceName());
+ }
+ Map<ParcelUuid, byte[]> serviceDataMap = record.getServiceData();
+ byte[] fastPairData = serviceDataMap.get(FAST_PAIR_UUID);
+ if (fastPairData != null) {
+ builder.setData(serviceDataMap.get(FAST_PAIR_UUID));
+ } else {
+ byte [] presenceData = serviceDataMap.get(PRESENCE_UUID);
+ if (presenceData != null) {
+ builder.setData(serviceDataMap.get(PRESENCE_UUID));
+ }
+ }
+ }
+ mExecutor.execute(() -> mListener.onNearbyDeviceDiscovered(builder.build()));
+ }
+
+ @Override
+ public void onScanFailed(int errorCode) {
+ Log.w(TAG, "BLE Scan failed with error code " + errorCode);
+ }
+ };
+
+ public BleDiscoveryProvider(Context context, Injector injector) {
+ super(context, NEARBY_EXECUTOR);
+ mInjector = injector;
+ }
+
+ private static List<ScanFilter> getScanFilters() {
+ List<ScanFilter> scanFilterList = new ArrayList<>();
+ scanFilterList.add(
+ new ScanFilter.Builder()
+ .setServiceData(FAST_PAIR_UUID, new byte[]{0}, new byte[]{0})
+ .build());
+ return scanFilterList;
+ }
+
+ private boolean isBleAvailable() {
+ BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+ if (adapter == null) {
+ return false;
+ }
+
+ return adapter.getBluetoothLeScanner() != null;
+ }
+
+ @Nullable
+ private BluetoothLeScanner getBleScanner() {
+ BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+ if (adapter == null) {
+ return null;
+ }
+ return adapter.getBluetoothLeScanner();
+ }
+
+ @Override
+ protected void onStart() {
+ if (isBleAvailable()) {
+ Log.d(TAG, "BleDiscoveryProvider started.");
+ startScan(getScanFilters(), getScanSettings(), mScanCallback);
+ return;
+ }
+ Log.w(TAG, "Cannot start BleDiscoveryProvider because Ble is not available.");
+ mController.stop();
+ }
+
+ @Override
+ protected void onStop() {
+ BluetoothLeScanner bluetoothLeScanner = getBleScanner();
+ if (bluetoothLeScanner == null) {
+ Log.w(TAG, "BleDiscoveryProvider failed to stop BLE scanning "
+ + "because BluetoothLeScanner is null.");
+ return;
+ }
+ Log.v(TAG, "Ble scan stopped.");
+ bluetoothLeScanner.stopScan(mScanCallback);
+ }
+
+ @Override
+ protected void invalidateScanMode() {
+ onStop();
+ onStart();
+ }
+
+ private void startScan(
+ List<ScanFilter> scanFilters, ScanSettings scanSettings,
+ android.bluetooth.le.ScanCallback scanCallback) {
+ try {
+ BluetoothLeScanner bluetoothLeScanner = getBleScanner();
+ if (bluetoothLeScanner == null) {
+ Log.w(TAG, "BleDiscoveryProvider failed to start BLE scanning "
+ + "because BluetoothLeScanner is null.");
+ return;
+ }
+ bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallback);
+ } catch (NullPointerException | IllegalStateException | SecurityException e) {
+ // NullPointerException:
+ // - Commonly, on Blackberry devices. b/73299795
+ // - Rarely, on other devices. b/75285249
+ // IllegalStateException:
+ // Caused if we call BluetoothLeScanner.startScan() after Bluetooth has turned off.
+ // SecurityException:
+ // refer to b/177380884
+ Log.w(TAG, "BleDiscoveryProvider failed to start BLE scanning.", e);
+ }
+ }
+
+ private ScanSettings getScanSettings() {
+ int bleScanMode = ScanSettings.SCAN_MODE_LOW_POWER;
+ switch (mController.getProviderScanMode()) {
+ case ScanRequest.SCAN_MODE_LOW_LATENCY:
+ bleScanMode = ScanSettings.SCAN_MODE_LOW_LATENCY;
+ break;
+ case ScanRequest.SCAN_MODE_BALANCED:
+ bleScanMode = ScanSettings.SCAN_MODE_BALANCED;
+ break;
+ case ScanRequest.SCAN_MODE_LOW_POWER:
+ bleScanMode = ScanSettings.SCAN_MODE_LOW_POWER;
+ break;
+ case ScanRequest.SCAN_MODE_NO_POWER:
+ bleScanMode = ScanSettings.SCAN_MODE_OPPORTUNISTIC;
+ break;
+ }
+ return new ScanSettings.Builder().setScanMode(bleScanMode).build();
+ }
+
+ @VisibleForTesting
+ ScanCallback getScanCallback() {
+ return mScanCallback;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java b/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java
new file mode 100644
index 0000000..72fe29a
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import android.content.Context;
+import android.nearby.BroadcastCallback;
+import android.nearby.BroadcastRequest;
+import android.nearby.IBroadcastListener;
+import android.nearby.PresenceBroadcastRequest;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.presence.FastAdvertisement;
+import com.android.server.nearby.util.ForegroundThread;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A manager for nearby broadcasts.
+ */
+public class BroadcastProviderManager implements BleBroadcastProvider.BroadcastListener {
+
+ private static final String TAG = "BroadcastProvider";
+
+ private final Object mLock;
+ private final BleBroadcastProvider mBleBroadcastProvider;
+ private final Executor mExecutor;
+ private final NearbyConfiguration mNearbyConfiguration;
+
+ private IBroadcastListener mBroadcastListener;
+
+ public BroadcastProviderManager(Context context, Injector injector) {
+ this(ForegroundThread.getExecutor(),
+ new BleBroadcastProvider(injector, ForegroundThread.getExecutor()));
+ }
+
+ @VisibleForTesting
+ BroadcastProviderManager(Executor executor, BleBroadcastProvider bleBroadcastProvider) {
+ mExecutor = executor;
+ mBleBroadcastProvider = bleBroadcastProvider;
+ mLock = new Object();
+ mNearbyConfiguration = new NearbyConfiguration();
+ mBroadcastListener = null;
+ }
+
+ /**
+ * Starts a nearby broadcast, the callback is sent through the given listener.
+ */
+ public void startBroadcast(BroadcastRequest broadcastRequest, IBroadcastListener listener) {
+ synchronized (mLock) {
+ NearbyConfiguration configuration = new NearbyConfiguration();
+ if (!configuration.isPresenceBroadcastLegacyEnabled()) {
+ reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+ return;
+ }
+ if (broadcastRequest.getType() != BroadcastRequest.BROADCAST_TYPE_NEARBY_PRESENCE) {
+ reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+ return;
+ }
+ PresenceBroadcastRequest presenceBroadcastRequest =
+ (PresenceBroadcastRequest) broadcastRequest;
+ if (presenceBroadcastRequest.getVersion() != BroadcastRequest.PRESENCE_VERSION_V0) {
+ reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+ return;
+ }
+ FastAdvertisement fastAdvertisement = FastAdvertisement.createFromRequest(
+ presenceBroadcastRequest);
+ byte[] advertisementPackets = fastAdvertisement.toBytes();
+ mBroadcastListener = listener;
+ mExecutor.execute(() -> {
+ mBleBroadcastProvider.start(advertisementPackets, this);
+ });
+ }
+ }
+
+ /**
+ * Stops the nearby broadcast.
+ */
+ public void stopBroadcast(IBroadcastListener listener) {
+ synchronized (mLock) {
+ if (!mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
+ reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+ return;
+ }
+ mBroadcastListener = null;
+ mExecutor.execute(() -> mBleBroadcastProvider.stop());
+ }
+ }
+
+ @Override
+ public void onStatusChanged(int status) {
+ IBroadcastListener listener = null;
+ synchronized (mLock) {
+ listener = mBroadcastListener;
+ }
+ // Don't invoke callback while holding the local lock, as this could cause deadlock.
+ if (listener != null) {
+ reportBroadcastStatus(listener, status);
+ }
+ }
+
+ private void reportBroadcastStatus(IBroadcastListener listener, int status) {
+ try {
+ listener.onStatusChanged(status);
+ } catch (RemoteException exception) {
+ Log.e(TAG, "remote exception when reporting status");
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java b/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
new file mode 100644
index 0000000..5077ffe
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.hardware.location.ContextHubClient;
+import android.hardware.location.ContextHubClientCallback;
+import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubTransaction;
+import android.hardware.location.NanoAppMessage;
+import android.hardware.location.NanoAppState;
+import android.util.Log;
+
+import com.android.server.nearby.injector.ContextHubManagerAdapter;
+import com.android.server.nearby.injector.Injector;
+
+import com.google.common.base.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/**
+ * Responsible for setting up communication with the appropriate contexthub on the device and
+ * handling nanoapp messages to / from it.
+ */
+public class ChreCommunication extends ContextHubClientCallback {
+
+ /** Callback that receives messages forwarded from the context hub. */
+ public interface ContextHubCommsCallback {
+ /** Indicates whether {@link ChreCommunication} was started successfully. */
+ void started(boolean success);
+
+ /** Indicates the ContextHub has been restarted. */
+ void onHubReset();
+
+ /**
+ * Indicates the given {@code nanoAppId} has been restarted. Either via code download or by
+ * being enabled by CHRE.
+ */
+ void onNanoAppRestart(long nanoAppId);
+
+ /** Indicates a new {@link NanoAppMessage} has been received. */
+ void onMessageFromNanoApp(NanoAppMessage message);
+ }
+
+ private final Injector mInjector;
+ private final Executor mExecutor;
+
+ private boolean mStarted = false;
+ @Nullable private ContextHubCommsCallback mCallback;
+ @Nullable private ContextHubClient mContextHubClient;
+
+ public ChreCommunication(Injector injector, Executor executor) {
+ mInjector = injector;
+ mExecutor = executor;
+ }
+
+ public boolean available() {
+ return mContextHubClient != null;
+ }
+
+ /**
+ * Starts communication with the contexthub. This will invoke {@link
+ * ContextHubCommsCallback#start(boolean)} on completion.
+ *
+ * @param nanoAppIds - List of IDs that must have at least one match inside the chosen
+ * contexthub.
+ */
+ public synchronized void start(ContextHubCommsCallback callback, Set<Long> nanoAppIds) {
+ ContextHubManagerAdapter manager = mInjector.getContextHubManagerAdapter();
+ if (manager == null) {
+ Log.e(TAG, "ContexHub not available in this device");
+ return;
+ } else {
+ Log.i(TAG, "Start ChreCommunication");
+ }
+ Preconditions.checkNotNull(callback);
+ Preconditions.checkArgument(!nanoAppIds.isEmpty());
+ if (mStarted) {
+ Log.i(TAG, "ChreCommunication already started");
+ this.mCallback.started(true);
+ return;
+ }
+
+ // Use this to indicate whether stop was called before the transaction below
+ // completes.
+ mStarted = true;
+ this.mCallback = callback;
+
+ List<ContextHubInfo> contextHubs = manager.getContextHubs();
+
+ // Make a copy of the list so we can modify it during our async callbacks (in case the code
+ // is still iterating)
+ List<ContextHubInfo> validContextHubs = new ArrayList<>(contextHubs);
+
+ for (ContextHubInfo info : contextHubs) {
+ ContextHubTransaction<List<NanoAppState>> transaction = manager.queryNanoApps(info);
+ Log.i(TAG, "After query Nano Apps ");
+ transaction.setOnCompleteListener(
+ new OnQueryCompleteListener(info, validContextHubs, nanoAppIds, manager),
+ mExecutor);
+ }
+ }
+
+ /**
+ * Closes the connection to the {@link ContextHub} chosen during start.
+ *
+ * <p>NOTE: Do not invoke any other methods on this class after this returns.
+ */
+ public synchronized void stop() {
+ if (!mStarted) {
+ return;
+ }
+ mStarted = false;
+ if (mContextHubClient != null) {
+ mContextHubClient.close();
+ mContextHubClient = null;
+ }
+ }
+
+ /** Sends a {@link NanoAppMessage} to Context Hub Nearby nanoapp. */
+ public synchronized boolean sendMessageToNanoApp(NanoAppMessage message) {
+ if (mContextHubClient == null) {
+ Log.i(TAG, "Error sending message to nanoapp, contextHubClient is null");
+ return false;
+ }
+ int result = mContextHubClient.sendMessageToNanoApp(message);
+ if (result != ContextHubTransaction.RESULT_SUCCESS) {
+ Log.i(
+ TAG,
+ String.format(
+ Locale.getDefault(),
+ "Error sending message to nanoapp: %s",
+ contextHubTransactionResultToString(result)));
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public synchronized void onMessageFromNanoApp(ContextHubClient client, NanoAppMessage message) {
+ mCallback.onMessageFromNanoApp(message);
+ }
+
+ @Override
+ public synchronized void onHubReset(ContextHubClient client) {
+ mCallback.onHubReset();
+ }
+
+ @Override
+ public synchronized void onNanoAppLoaded(ContextHubClient client, long nanoAppId) {
+ Log.i(TAG, String.format("Nanoapp ID loaded: %s", nanoAppId));
+ mCallback.onNanoAppRestart(nanoAppId);
+ }
+
+ private static String contextHubTransactionResultToString(int result) {
+ switch (result) {
+ case ContextHubTransaction.RESULT_SUCCESS:
+ return "RESULT_SUCCESS";
+ case ContextHubTransaction.RESULT_FAILED_UNKNOWN:
+ return "RESULT_FAILED_UNKNOWN";
+ case ContextHubTransaction.RESULT_FAILED_BAD_PARAMS:
+ return "RESULT_FAILED_BAD_PARAMS";
+ case ContextHubTransaction.RESULT_FAILED_UNINITIALIZED:
+ return "RESULT_FAILED_UNINITIALIZED";
+ case ContextHubTransaction.RESULT_FAILED_BUSY:
+ return "RESULT_FAILED_BUSY";
+ case ContextHubTransaction.RESULT_FAILED_AT_HUB:
+ return "RESULT_FAILED_AT_HUB";
+ case ContextHubTransaction.RESULT_FAILED_TIMEOUT:
+ return "RESULT_FAILED_TIMEOUT";
+ case ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE:
+ return "RESULT_FAILED_SERVICE_INTERNAL_FAILURE";
+ case ContextHubTransaction.RESULT_FAILED_HAL_UNAVAILABLE:
+ return "RESULT_FAILED_HAL_UNAVAILABLE";
+ default:
+ return String.format(Locale.getDefault(), "UNKNOWN_RESULT value=%d", result);
+ }
+ }
+
+ /**
+ * Used when initializing the class to identify the appropriate {@link ContextHubInfo} to listen
+ * to.
+ */
+ class OnQueryCompleteListener
+ implements ContextHubTransaction.OnCompleteListener<List<NanoAppState>> {
+
+ private final ContextHubInfo mQueriedContextHub;
+ private final List<ContextHubInfo> mContextHubs;
+ private final Set<Long> mNanoAppIds;
+ private final ContextHubManagerAdapter mManager;
+
+ OnQueryCompleteListener(
+ ContextHubInfo queriedContextHub,
+ List<ContextHubInfo> contextHubs,
+ Set<Long> nanoAppIds,
+ ContextHubManagerAdapter manager) {
+ this.mQueriedContextHub = queriedContextHub;
+ this.mContextHubs = contextHubs;
+ this.mNanoAppIds = nanoAppIds;
+ this.mManager = manager;
+ }
+
+ @Override
+ public void onComplete(
+ ContextHubTransaction<List<NanoAppState>> transaction,
+ ContextHubTransaction.Response<List<NanoAppState>> response) {
+ Log.i(TAG, "query nano app onComplete");
+ // Ensure the class hasn't found a client already or stop hasn't been called before
+ // the transaction completed to avoid messing with state.
+ if (mContextHubClient != null || !mStarted) {
+ return;
+ }
+
+ if (response.getResult() == ContextHubTransaction.RESULT_SUCCESS) {
+ for (NanoAppState state : response.getContents()) {
+ if (mNanoAppIds.contains(state.getNanoAppId())) {
+ Log.i(
+ TAG,
+ String.format(
+ "Found valid contexthub: %s", mQueriedContextHub.getId()));
+ mContextHubClient =
+ mManager.createClient(
+ mQueriedContextHub, ChreCommunication.this, mExecutor);
+ mCallback.started(true);
+ return;
+ }
+ }
+ Log.e(
+ TAG,
+ String.format(
+ "Didn't find the nanoapp on contexthub: %s",
+ mQueriedContextHub.getId()));
+ } else {
+ Log.e(
+ TAG,
+ String.format(
+ "Failed to communicate with contexthub: %s",
+ mQueriedContextHub.getId()));
+ }
+
+ mContextHubs.remove(mQueriedContextHub);
+ // If this is the last context hub response left to receive, indicate that
+ // there isn't a valid context available on this device.
+ if (mContextHubs.isEmpty()) {
+ mCallback.started(false);
+ }
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
new file mode 100644
index 0000000..a70ef13
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.content.Context;
+import android.hardware.location.NanoAppMessage;
+import android.nearby.NearbyDevice;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanFilter;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import service.proto.Blefilter;
+
+import java.util.Collections;
+import java.util.concurrent.Executor;
+
+/** Discovery provider that uses CHRE Nearby Nanoapp to do scanning. */
+public class ChreDiscoveryProvider extends AbstractDiscoveryProvider {
+ // Nanoapp ID reserved for Nearby Presence.
+ /** @hide */
+ @VisibleForTesting public static final long NANOAPP_ID = 0x476f6f676c001031L;
+ /** @hide */
+ @VisibleForTesting public static final int NANOAPP_MESSAGE_TYPE_FILTER = 3;
+ /** @hide */
+ @VisibleForTesting public static final int NANOAPP_MESSAGE_TYPE_FILTER_RESULT = 4;
+
+ private static final int PRESENCE_UUID = 0xFCF1;
+
+ private ChreCommunication mChreCommunication;
+ private ChreCallback mChreCallback;
+ private boolean mChreStarted = false;
+ private Blefilter.BleFilters mFilters = null;
+ private int mFilterId;
+
+ public ChreDiscoveryProvider(
+ Context context, ChreCommunication chreCommunication, Executor executor) {
+ super(context, executor);
+ mChreCommunication = chreCommunication;
+ mChreCallback = new ChreCallback();
+ mFilterId = 0;
+ }
+
+ @Override
+ protected void onStart() {
+ Log.d(TAG, "Start CHRE scan");
+ mChreCommunication.start(mChreCallback, Collections.singleton(NANOAPP_ID));
+ updateFilters();
+ }
+
+ @Override
+ protected void onStop() {
+ mChreStarted = false;
+ mChreCommunication.stop();
+ }
+
+ @Override
+ protected void invalidateScanMode() {
+ onStop();
+ onStart();
+ }
+
+ public boolean available() {
+ return mChreCommunication.available();
+ }
+
+ private synchronized void updateFilters() {
+ if (mScanFilters == null) {
+ Log.e(TAG, "ScanFilters not set.");
+ return;
+ }
+ Blefilter.BleFilters.Builder filtersBuilder = Blefilter.BleFilters.newBuilder();
+ for (ScanFilter scanFilter : mScanFilters) {
+ PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
+ Blefilter.BleFilter filter =
+ Blefilter.BleFilter.newBuilder()
+ .setId(mFilterId)
+ .setUuid(PRESENCE_UUID)
+ .setIntent(presenceScanFilter.getPresenceActions().get(0))
+ .build();
+ filtersBuilder.addFilter(filter);
+ mFilterId++;
+ }
+ mFilters = filtersBuilder.build();
+ if (mChreStarted) {
+ sendFilters(mFilters);
+ mFilters = null;
+ }
+ }
+
+ private void sendFilters(Blefilter.BleFilters filters) {
+ NanoAppMessage message =
+ NanoAppMessage.createMessageToNanoApp(
+ NANOAPP_ID, NANOAPP_MESSAGE_TYPE_FILTER, filters.toByteArray());
+ if (!mChreCommunication.sendMessageToNanoApp(message)) {
+ Log.e(TAG, "Failed to send filters to CHRE.");
+ }
+ }
+
+ private class ChreCallback implements ChreCommunication.ContextHubCommsCallback {
+
+ @Override
+ public void started(boolean success) {
+ if (success) {
+ synchronized (ChreDiscoveryProvider.this) {
+ Log.i(TAG, "CHRE communication started");
+ mChreStarted = true;
+ if (mFilters != null) {
+ sendFilters(mFilters);
+ mFilters = null;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onHubReset() {
+ // TODO(b/221082271): hooked with upper level codes.
+ Log.i(TAG, "CHRE reset.");
+ }
+
+ @Override
+ public void onNanoAppRestart(long nanoAppId) {
+ // TODO(b/221082271): hooked with upper level codes.
+ Log.i(TAG, String.format("CHRE NanoApp %d restart.", nanoAppId));
+ }
+
+ @Override
+ public void onMessageFromNanoApp(NanoAppMessage message) {
+ if (message.getNanoAppId() != NANOAPP_ID) {
+ Log.e(TAG, "Received message from unknown nano app.");
+ return;
+ }
+ if (mListener == null) {
+ Log.e(TAG, "the listener is not set in ChreDiscoveryProvider.");
+ return;
+ }
+ if (message.getMessageType() == NANOAPP_MESSAGE_TYPE_FILTER_RESULT) {
+ try {
+ Blefilter.BleFilterResults results =
+ Blefilter.BleFilterResults.parseFrom(message.getMessageBody());
+ for (Blefilter.BleFilterResult filterResult : results.getResultList()) {
+ Blefilter.PublicCredential credential = filterResult.getPublicCredential();
+ PublicCredential publicCredential =
+ new PublicCredential.Builder(
+ credential.getSecretId().toByteArray(),
+ credential.getAuthenticityKey().toByteArray(),
+ credential.getPublicKey().toByteArray(),
+ credential.getEncryptedMetadata().toByteArray(),
+ credential.getEncryptedMetadataTag().toByteArray())
+ .build();
+ NearbyDeviceParcelable device =
+ new NearbyDeviceParcelable.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .setMedium(NearbyDevice.Medium.BLE)
+ .setTxPower(filterResult.getTxPower())
+ .setRssi(filterResult.getRssi())
+ .setAction(filterResult.getIntent())
+ .setPublicCredential(publicCredential)
+ .build();
+ mExecutor.execute(() -> mListener.onNearbyDeviceDiscovered(device));
+ }
+ } catch (InvalidProtocolBufferException e) {
+ Log.e(
+ TAG,
+ String.format("Failed to decode the filter result %s", e.toString()));
+ }
+ }
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java
new file mode 100644
index 0000000..fa1a874
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import android.annotation.Nullable;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+
+import java.util.List;
+
+/** Interface for controlling discovery providers. */
+interface DiscoveryProviderController {
+
+ /**
+ * Sets the listener which can expect to receive all state updates from after this point. May be
+ * invoked at any time.
+ */
+ void setListener(@Nullable AbstractDiscoveryProvider.Listener listener);
+
+ /** Returns true if in the started state. */
+ boolean isStarted();
+
+ /**
+ * Starts the discovery provider. Must be invoked before any other method (except {@link
+ * #setListener(AbstractDiscoveryProvider.Listener)} (Listener)}).
+ */
+ void start();
+
+ /**
+ * Stops the discovery provider. No other methods may be invoked after this method (except
+ * {@link #setListener(AbstractDiscoveryProvider.Listener)} (Listener)}), until {@link #start()}
+ * is called again.
+ */
+ void stop();
+
+ /** Sets the desired scan mode. */
+ void setProviderScanMode(@ScanRequest.ScanMode int scanMode);
+
+ /** Gets the controller scan mode. */
+ @ScanRequest.ScanMode
+ int getProviderScanMode();
+
+ /** Sets the scan filters. */
+ void setProviderScanFilters(List<ScanFilter> filters);
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
new file mode 100644
index 0000000..53d61c2
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.metrics.NearbyMetrics;
+import com.android.server.nearby.presence.PresenceDiscoveryResult;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/** Manages all aspects of discovery providers. */
+public class DiscoveryProviderManager implements AbstractDiscoveryProvider.Listener {
+
+ protected final Object mLock = new Object();
+ private final Context mContext;
+ private final BleDiscoveryProvider mBleDiscoveryProvider;
+ @Nullable private final ChreDiscoveryProvider mChreDiscoveryProvider;
+ private @ScanRequest.ScanMode int mScanMode;
+
+ @GuardedBy("mLock")
+ private Map<IBinder, ScanListenerRecord> mScanTypeScanListenerRecordMap;
+
+ @Override
+ public void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice) {
+ synchronized (mLock) {
+ for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
+ ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
+ if (record == null) {
+ Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+ continue;
+ }
+ if (nearbyDevice.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
+ List<ScanFilter> presenceFilters =
+ record.getScanRequest().getScanFilters().stream()
+ .filter(
+ scanFilter ->
+ scanFilter.getType()
+ == SCAN_TYPE_NEARBY_PRESENCE)
+ .collect(Collectors.toList());
+ Log.i(
+ TAG,
+ String.format("match with filters size: %d", presenceFilters.size()));
+ if (!presenceFilterMatches(nearbyDevice, presenceFilters)) {
+ continue;
+ }
+ }
+ try {
+ record.getScanListener()
+ .onDiscovered(
+ PrivacyFilter.filter(
+ record.getScanRequest().getScanType(), nearbyDevice));
+ NearbyMetrics.logScanDeviceDiscovered(
+ record.hashCode(), record.getScanRequest(), nearbyDevice);
+ } catch (RemoteException e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report onDiscovered.", e);
+ }
+ }
+ }
+ }
+
+ public DiscoveryProviderManager(Context context, Injector injector) {
+ mContext = context;
+ mBleDiscoveryProvider = new BleDiscoveryProvider(mContext, injector);
+ Executor executor = Executors.newSingleThreadExecutor();
+ mChreDiscoveryProvider =
+ new ChreDiscoveryProvider(
+ mContext, new ChreCommunication(injector, executor), executor);
+ mScanTypeScanListenerRecordMap = new HashMap<>();
+ }
+
+ /**
+ * Registers the listener in the manager and starts scan according to the requested scan mode.
+ */
+ public boolean registerScanListener(ScanRequest scanRequest, IScanListener listener) {
+ Log.i(TAG, "DiscoveryProviderManager registerScanListener");
+ synchronized (mLock) {
+ IBinder listenerBinder = listener.asBinder();
+ if (mScanTypeScanListenerRecordMap.containsKey(listener.asBinder())) {
+ ScanRequest savedScanRequest =
+ mScanTypeScanListenerRecordMap.get(listenerBinder).getScanRequest();
+ if (scanRequest.equals(savedScanRequest)) {
+ Log.d(TAG, "Already registered the scanRequest: " + scanRequest);
+ return true;
+ }
+ }
+ ScanListenerRecord scanListenerRecord = new ScanListenerRecord(scanRequest, listener);
+ mScanTypeScanListenerRecordMap.put(listenerBinder, scanListenerRecord);
+
+ if (!startProviders(scanRequest)) {
+ return false;
+ }
+
+ NearbyMetrics.logScanStarted(scanListenerRecord.hashCode(), scanRequest);
+ if (mScanMode < scanRequest.getScanMode()) {
+ mScanMode = scanRequest.getScanMode();
+ invalidateProviderScanMode();
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
+ */
+ public void unregisterScanListener(IScanListener listener) {
+ IBinder listenerBinder = listener.asBinder();
+ synchronized (mLock) {
+ if (!mScanTypeScanListenerRecordMap.containsKey(listenerBinder)) {
+ Log.w(
+ TAG,
+ "Cannot unregister the scanRequest because the request is never "
+ + "registered.");
+ return;
+ }
+
+ ScanListenerRecord removedRecord =
+ mScanTypeScanListenerRecordMap.remove(listenerBinder);
+ Log.v(TAG, "DiscoveryProviderManager unregistered scan listener.");
+ NearbyMetrics.logScanStopped(removedRecord.hashCode(), removedRecord.getScanRequest());
+ if (mScanTypeScanListenerRecordMap.isEmpty()) {
+ Log.v(TAG, "DiscoveryProviderManager stops provider because there is no "
+ + "scan listener registered.");
+ stopProviders();
+ return;
+ }
+
+ // TODO(b/221082271): updates the scan with reduced filters.
+
+ // Removes current highest scan mode requested and sets the next highest scan mode.
+ if (removedRecord.getScanRequest().getScanMode() == mScanMode) {
+ Log.v(TAG, "DiscoveryProviderManager starts to find the new highest scan mode "
+ + "because the highest scan mode listener was unregistered.");
+ @ScanRequest.ScanMode int highestScanModeRequested = ScanRequest.SCAN_MODE_NO_POWER;
+ // find the next highest scan mode;
+ for (ScanListenerRecord record : mScanTypeScanListenerRecordMap.values()) {
+ @ScanRequest.ScanMode int scanMode = record.getScanRequest().getScanMode();
+ if (scanMode > highestScanModeRequested) {
+ highestScanModeRequested = scanMode;
+ }
+ }
+ if (mScanMode != highestScanModeRequested) {
+ mScanMode = highestScanModeRequested;
+ invalidateProviderScanMode();
+ }
+ }
+ }
+ }
+
+ // Returns false when fail to start all the providers. Returns true if any one of the provider
+ // starts successfully.
+ private boolean startProviders(ScanRequest scanRequest) {
+ if (scanRequest.isBleEnabled()) {
+ if (mChreDiscoveryProvider.available()
+ && scanRequest.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
+ startChreProvider();
+ } else {
+ startBleProvider(scanRequest);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private void startBleProvider(ScanRequest scanRequest) {
+ if (!mBleDiscoveryProvider.getController().isStarted()) {
+ Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+ mBleDiscoveryProvider.getController().start();
+ mBleDiscoveryProvider.getController().setListener(this);
+ mBleDiscoveryProvider.getController().setProviderScanMode(scanRequest.getScanMode());
+ }
+ }
+
+ private void startChreProvider() {
+ Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
+ synchronized (mLock) {
+ mChreDiscoveryProvider.getController().setListener(this);
+ List<ScanFilter> scanFilters = new ArrayList();
+ for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
+ ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
+ List<ScanFilter> presenceFilters =
+ record.getScanRequest().getScanFilters().stream()
+ .filter(
+ scanFilter ->
+ scanFilter.getType() == SCAN_TYPE_NEARBY_PRESENCE)
+ .collect(Collectors.toList());
+ scanFilters.addAll(presenceFilters);
+ }
+ mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
+ mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ mChreDiscoveryProvider.getController().start();
+ }
+ }
+
+ private void stopProviders() {
+ stopBleProvider();
+ stopChreProvider();
+ }
+
+ private void stopBleProvider() {
+ mBleDiscoveryProvider.getController().stop();
+ }
+
+ private void stopChreProvider() {
+ mChreDiscoveryProvider.getController().stop();
+ }
+
+ private void invalidateProviderScanMode() {
+ if (mBleDiscoveryProvider.getController().isStarted()) {
+ mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ } else {
+ Log.d(
+ TAG,
+ "Skip invalidating BleDiscoveryProvider scan mode because the provider not "
+ + "started.");
+ }
+ }
+
+ private static boolean presenceFilterMatches(
+ NearbyDeviceParcelable device, List<ScanFilter> scanFilters) {
+ if (scanFilters.isEmpty()) {
+ return true;
+ }
+ PresenceDiscoveryResult discoveryResult = PresenceDiscoveryResult.fromDevice(device);
+ for (ScanFilter scanFilter : scanFilters) {
+ PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
+ if (discoveryResult.matches(presenceScanFilter)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static class ScanListenerRecord {
+
+ private final ScanRequest mScanRequest;
+
+ private final IScanListener mScanListener;
+
+ ScanListenerRecord(ScanRequest scanRequest, IScanListener iScanListener) {
+ mScanListener = iScanListener;
+ mScanRequest = scanRequest;
+ }
+
+ IScanListener getScanListener() {
+ return mScanListener;
+ }
+
+ ScanRequest getScanRequest() {
+ return mScanRequest;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof ScanListenerRecord) {
+ ScanListenerRecord otherScanListenerRecord = (ScanListenerRecord) other;
+ return Objects.equals(mScanRequest, otherScanListenerRecord.mScanRequest)
+ && Objects.equals(mScanListener, otherScanListenerRecord.mScanListener);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mScanListener, mScanRequest);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java b/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java
new file mode 100644
index 0000000..0f99a2f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import android.accounts.Account;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.nearby.FastPairDataProviderService;
+import android.nearby.aidl.ByteArrayParcel;
+import android.nearby.aidl.FastPairAccountDevicesMetadataRequestParcel;
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataRequestParcel;
+import android.nearby.aidl.FastPairEligibleAccountsRequestParcel;
+import android.nearby.aidl.FastPairManageAccountDeviceRequestParcel;
+import android.nearby.aidl.FastPairManageAccountRequestParcel;
+import android.util.Log;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.server.nearby.common.bloomfilter.BloomFilter;
+import com.android.server.nearby.fastpair.footprint.FastPairUploadInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import service.proto.Data;
+import service.proto.Rpcs;
+
+/**
+ * FastPairDataProvider is a singleton that implements APIs to get FastPair data.
+ */
+public class FastPairDataProvider {
+
+ private static final String TAG = "FastPairDataProvider";
+
+ private static FastPairDataProvider sInstance;
+
+ private ProxyFastPairDataProvider mProxyFastPairDataProvider;
+
+ /**
+ * Initializes FastPairDataProvider singleton.
+ */
+ public static synchronized FastPairDataProvider init(Context context) {
+ if (sInstance == null) {
+ sInstance = new FastPairDataProvider(context);
+ }
+ if (sInstance.mProxyFastPairDataProvider == null) {
+ Log.w(TAG, "no proxy fast pair data provider found");
+ } else {
+ sInstance.mProxyFastPairDataProvider.register();
+ }
+ return sInstance;
+ }
+
+ @Nullable
+ public static synchronized FastPairDataProvider getInstance() {
+ return sInstance;
+ }
+
+ private FastPairDataProvider(Context context) {
+ mProxyFastPairDataProvider = ProxyFastPairDataProvider.create(
+ context, FastPairDataProviderService.ACTION_FAST_PAIR_DATA_PROVIDER);
+ if (mProxyFastPairDataProvider == null) {
+ Log.d("FastPairService", "fail to initiate the fast pair proxy provider");
+ } else {
+ Log.d("FastPairService", "the fast pair proxy provider initiated");
+ }
+ }
+
+ /**
+ * Loads FastPairAntispoofKeyDeviceMetadata.
+ *
+ * @throws IllegalStateException If ProxyFastPairDataProvider is not available.
+ */
+ @WorkerThread
+ @Nullable
+ public Rpcs.GetObservedDeviceResponse loadFastPairAntispoofKeyDeviceMetadata(byte[] modelId) {
+ if (mProxyFastPairDataProvider != null) {
+ FastPairAntispoofKeyDeviceMetadataRequestParcel requestParcel =
+ new FastPairAntispoofKeyDeviceMetadataRequestParcel();
+ requestParcel.modelId = modelId;
+ return Utils.convertToGetObservedDeviceResponse(
+ mProxyFastPairDataProvider
+ .loadFastPairAntispoofKeyDeviceMetadata(requestParcel));
+ }
+ throw new IllegalStateException("No ProxyFastPairDataProvider yet constructed");
+ }
+
+ /**
+ * Enrolls an account to Fast Pair.
+ *
+ * @throws IllegalStateException If ProxyFastPairDataProvider is not available.
+ */
+ public void optIn(Account account) {
+ if (mProxyFastPairDataProvider != null) {
+ FastPairManageAccountRequestParcel requestParcel =
+ new FastPairManageAccountRequestParcel();
+ requestParcel.account = account;
+ requestParcel.requestType = FastPairDataProviderService.MANAGE_REQUEST_ADD;
+ mProxyFastPairDataProvider.manageFastPairAccount(requestParcel);
+ return;
+ }
+ throw new IllegalStateException("No ProxyFastPairDataProvider yet constructed");
+ }
+
+ /**
+ * Uploads the device info to Fast Pair account.
+ *
+ * @throws IllegalStateException If ProxyFastPairDataProvider is not available.
+ */
+ public void upload(Account account, FastPairUploadInfo uploadInfo) {
+ if (mProxyFastPairDataProvider != null) {
+ FastPairManageAccountDeviceRequestParcel requestParcel =
+ new FastPairManageAccountDeviceRequestParcel();
+ requestParcel.account = account;
+ requestParcel.requestType = FastPairDataProviderService.MANAGE_REQUEST_ADD;
+ requestParcel.accountKeyDeviceMetadata =
+ Utils.convertToFastPairAccountKeyDeviceMetadata(uploadInfo);
+ mProxyFastPairDataProvider.manageFastPairAccountDevice(requestParcel);
+ return;
+ }
+ throw new IllegalStateException("No ProxyFastPairDataProvider yet constructed");
+ }
+
+ /**
+ * Get recognized device from bloom filter.
+ */
+ public Data.FastPairDeviceWithAccountKey getRecognizedDevice(BloomFilter bloomFilter,
+ byte[] salt) {
+ return Data.FastPairDeviceWithAccountKey.newBuilder().build();
+ }
+
+ /**
+ * Loads FastPair device accountKeys for a given account, but not other detailed fields.
+ *
+ * @throws IllegalStateException If ProxyFastPairDataProvider is not available.
+ */
+ public List<Data.FastPairDeviceWithAccountKey> loadFastPairDeviceWithAccountKey(
+ Account account) {
+ return loadFastPairDeviceWithAccountKey(account, new ArrayList<byte[]>(0));
+ }
+
+ /**
+ * Loads FastPair devices for a list of accountKeys of a given account.
+ *
+ * @param account The account of the FastPair devices.
+ * @param deviceAccountKeys The allow list of FastPair devices if it is not empty. Otherwise,
+ * the function returns accountKeys of all FastPair devices under the
+ * account, without detailed fields.
+ *
+ * @throws IllegalStateException If ProxyFastPairDataProvider is not available.
+ */
+ public List<Data.FastPairDeviceWithAccountKey> loadFastPairDeviceWithAccountKey(
+ Account account, List<byte[]> deviceAccountKeys) {
+ if (mProxyFastPairDataProvider != null) {
+ FastPairAccountDevicesMetadataRequestParcel requestParcel =
+ new FastPairAccountDevicesMetadataRequestParcel();
+ requestParcel.account = account;
+ requestParcel.deviceAccountKeys = new ByteArrayParcel[deviceAccountKeys.size()];
+ int i = 0;
+ for (byte[] deviceAccountKey : deviceAccountKeys) {
+ requestParcel.deviceAccountKeys[i] = new ByteArrayParcel();
+ requestParcel.deviceAccountKeys[i].byteArray = deviceAccountKey;
+ i = i + 1;
+ }
+ return Utils.convertToFastPairDevicesWithAccountKey(
+ mProxyFastPairDataProvider.loadFastPairAccountDevicesMetadata(requestParcel));
+ }
+ throw new IllegalStateException("No ProxyFastPairDataProvider yet constructed");
+ }
+
+ /**
+ * Loads FastPair Eligible Accounts.
+ *
+ * @throws IllegalStateException If ProxyFastPairDataProvider is not available.
+ */
+ public List<Account> loadFastPairEligibleAccounts() {
+ if (mProxyFastPairDataProvider != null) {
+ FastPairEligibleAccountsRequestParcel requestParcel =
+ new FastPairEligibleAccountsRequestParcel();
+ return Utils.convertToAccountList(
+ mProxyFastPairDataProvider.loadFastPairEligibleAccounts(requestParcel));
+ }
+ throw new IllegalStateException("No ProxyFastPairDataProvider yet constructed");
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/PrivacyFilter.java b/nearby/service/java/com/android/server/nearby/provider/PrivacyFilter.java
new file mode 100644
index 0000000..5c37f68
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/PrivacyFilter.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import android.annotation.Nullable;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.ScanRequest;
+
+/**
+ * Class strips out privacy sensitive data before delivering the callbacks to client.
+ */
+public class PrivacyFilter {
+
+ /**
+ * Strips sensitive data from {@link NearbyDeviceParcelable} according to
+ * different {@link android.nearby.ScanRequest.ScanType}s.
+ */
+ @Nullable
+ public static NearbyDeviceParcelable filter(@ScanRequest.ScanType int scanType,
+ NearbyDeviceParcelable scanResult) {
+ return scanResult;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/ProxyFastPairDataProvider.java b/nearby/service/java/com/android/server/nearby/provider/ProxyFastPairDataProvider.java
new file mode 100644
index 0000000..f0ade6c
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/ProxyFastPairDataProvider.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.nearby.aidl.FastPairAccountDevicesMetadataRequestParcel;
+import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataRequestParcel;
+import android.nearby.aidl.FastPairEligibleAccountParcel;
+import android.nearby.aidl.FastPairEligibleAccountsRequestParcel;
+import android.nearby.aidl.FastPairManageAccountDeviceRequestParcel;
+import android.nearby.aidl.FastPairManageAccountRequestParcel;
+import android.nearby.aidl.IFastPairAccountDevicesMetadataCallback;
+import android.nearby.aidl.IFastPairAntispoofKeyDeviceMetadataCallback;
+import android.nearby.aidl.IFastPairDataProvider;
+import android.nearby.aidl.IFastPairEligibleAccountsCallback;
+import android.nearby.aidl.IFastPairManageAccountCallback;
+import android.nearby.aidl.IFastPairManageAccountDeviceCallback;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.server.nearby.common.servicemonitor.CurrentUserServiceProvider;
+import com.android.server.nearby.common.servicemonitor.CurrentUserServiceProvider.BoundServiceInfo;
+import com.android.server.nearby.common.servicemonitor.ServiceMonitor;
+import com.android.server.nearby.common.servicemonitor.ServiceMonitor.ServiceListener;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Proxy for IFastPairDataProvider implementations.
+ */
+public class ProxyFastPairDataProvider implements ServiceListener<BoundServiceInfo> {
+
+ private static final int TIME_OUT_MILLIS = 10000;
+
+ /**
+ * Creates and registers this proxy. If no suitable service is available for the proxy, returns
+ * null.
+ */
+ @Nullable
+ public static ProxyFastPairDataProvider create(Context context, String action) {
+ ProxyFastPairDataProvider proxy = new ProxyFastPairDataProvider(context, action);
+ if (proxy.checkServiceResolves()) {
+ return proxy;
+ } else {
+ return null;
+ }
+ }
+
+ private final ServiceMonitor mServiceMonitor;
+
+ private ProxyFastPairDataProvider(Context context, String action) {
+ // safe to use direct executor since our locks are not acquired in a code path invoked by
+ // our owning provider
+
+ mServiceMonitor = ServiceMonitor.create(context, "FAST_PAIR_DATA_PROVIDER",
+ CurrentUserServiceProvider.create(context, action), this);
+ }
+
+ private boolean checkServiceResolves() {
+ return mServiceMonitor.checkServiceResolves();
+ }
+
+ /**
+ * User service watch to connect to actually services implemented by OEMs.
+ */
+ public void register() {
+ mServiceMonitor.register();
+ }
+
+ // Fast Pair Data Provider doesn't maintain a long running state.
+ // Therefore, it doesn't need setup at bind time.
+ @Override
+ public void onBind(IBinder binder, BoundServiceInfo boundServiceInfo) throws RemoteException {
+ }
+
+ // Fast Pair Data Provider doesn't maintain a long running state.
+ // Therefore, it doesn't need tear down at unbind time.
+ @Override
+ public void onUnbind() {
+ }
+
+ /**
+ * Invokes system api loadFastPairEligibleAccounts.
+ *
+ * @return an array of acccounts and their opt in status.
+ */
+ @WorkerThread
+ @Nullable
+ public FastPairEligibleAccountParcel[] loadFastPairEligibleAccounts(
+ FastPairEligibleAccountsRequestParcel requestParcel) {
+ final CountDownLatch waitForCompletionLatch = new CountDownLatch(1);
+ final AtomicReference<FastPairEligibleAccountParcel[]> response = new AtomicReference<>();
+ mServiceMonitor.runOnBinder(new ServiceMonitor.BinderOperation() {
+ @Override
+ public void run(IBinder binder) throws RemoteException {
+ IFastPairDataProvider provider = IFastPairDataProvider.Stub.asInterface(binder);
+ IFastPairEligibleAccountsCallback callback =
+ new IFastPairEligibleAccountsCallback.Stub() {
+ public void onFastPairEligibleAccountsReceived(
+ FastPairEligibleAccountParcel[] accountParcels) {
+ response.set(accountParcels);
+ waitForCompletionLatch.countDown();
+ }
+
+ public void onError(int code, String message) {
+ waitForCompletionLatch.countDown();
+ }
+ };
+ provider.loadFastPairEligibleAccounts(requestParcel, callback);
+ }
+
+ @Override
+ public void onError() {
+ waitForCompletionLatch.countDown();
+ }
+ });
+ try {
+ waitForCompletionLatch.await(TIME_OUT_MILLIS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ // skip.
+ }
+ return response.get();
+ }
+
+ /**
+ * Invokes system api manageFastPairAccount to opt in account, or opt out account.
+ */
+ @WorkerThread
+ public void manageFastPairAccount(FastPairManageAccountRequestParcel requestParcel) {
+ final CountDownLatch waitForCompletionLatch = new CountDownLatch(1);
+ mServiceMonitor.runOnBinder(new ServiceMonitor.BinderOperation() {
+ @Override
+ public void run(IBinder binder) throws RemoteException {
+ IFastPairDataProvider provider = IFastPairDataProvider.Stub.asInterface(binder);
+ IFastPairManageAccountCallback callback =
+ new IFastPairManageAccountCallback.Stub() {
+ public void onSuccess() {
+ waitForCompletionLatch.countDown();
+ }
+
+ public void onError(int code, String message) {
+ waitForCompletionLatch.countDown();
+ }
+ };
+ provider.manageFastPairAccount(requestParcel, callback);
+ }
+
+ @Override
+ public void onError() {
+ waitForCompletionLatch.countDown();
+ }
+ });
+ try {
+ waitForCompletionLatch.await(TIME_OUT_MILLIS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ // skip.
+ }
+ return;
+ }
+
+ /**
+ * Invokes system api manageFastPairAccountDevice to add or remove a device from a Fast Pair
+ * account.
+ */
+ @WorkerThread
+ public void manageFastPairAccountDevice(
+ FastPairManageAccountDeviceRequestParcel requestParcel) {
+ final CountDownLatch waitForCompletionLatch = new CountDownLatch(1);
+ mServiceMonitor.runOnBinder(new ServiceMonitor.BinderOperation() {
+ @Override
+ public void run(IBinder binder) throws RemoteException {
+ IFastPairDataProvider provider = IFastPairDataProvider.Stub.asInterface(binder);
+ IFastPairManageAccountDeviceCallback callback =
+ new IFastPairManageAccountDeviceCallback.Stub() {
+ public void onSuccess() {
+ waitForCompletionLatch.countDown();
+ }
+
+ public void onError(int code, String message) {
+ waitForCompletionLatch.countDown();
+ }
+ };
+ provider.manageFastPairAccountDevice(requestParcel, callback);
+ }
+
+ @Override
+ public void onError() {
+ waitForCompletionLatch.countDown();
+ }
+ });
+ try {
+ waitForCompletionLatch.await(TIME_OUT_MILLIS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ // skip.
+ }
+ return;
+ }
+
+ /**
+ * Invokes system api loadFastPairAntispoofKeyDeviceMetadata.
+ *
+ * @return the Fast Pair AntispoofKeyDeviceMetadata of a given device.
+ */
+ @WorkerThread
+ @Nullable
+ FastPairAntispoofKeyDeviceMetadataParcel loadFastPairAntispoofKeyDeviceMetadata(
+ FastPairAntispoofKeyDeviceMetadataRequestParcel requestParcel) {
+ final CountDownLatch waitForCompletionLatch = new CountDownLatch(1);
+ final AtomicReference<FastPairAntispoofKeyDeviceMetadataParcel> response =
+ new AtomicReference<>();
+ mServiceMonitor.runOnBinder(new ServiceMonitor.BinderOperation() {
+ @Override
+ public void run(IBinder binder) throws RemoteException {
+ IFastPairDataProvider provider = IFastPairDataProvider.Stub.asInterface(binder);
+ IFastPairAntispoofKeyDeviceMetadataCallback callback =
+ new IFastPairAntispoofKeyDeviceMetadataCallback.Stub() {
+ public void onFastPairAntispoofKeyDeviceMetadataReceived(
+ FastPairAntispoofKeyDeviceMetadataParcel metadata) {
+ response.set(metadata);
+ waitForCompletionLatch.countDown();
+ }
+
+ public void onError(int code, String message) {
+ waitForCompletionLatch.countDown();
+ }
+ };
+ provider.loadFastPairAntispoofKeyDeviceMetadata(requestParcel, callback);
+ }
+
+ @Override
+ public void onError() {
+ waitForCompletionLatch.countDown();
+ }
+ });
+ try {
+ waitForCompletionLatch.await(TIME_OUT_MILLIS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ // skip.
+ }
+ return response.get();
+ }
+
+ /**
+ * Invokes loadFastPairAccountDevicesMetadata.
+ *
+ * @return the metadata of Fast Pair devices that are associated with a given account.
+ */
+ @WorkerThread
+ @Nullable
+ FastPairAccountKeyDeviceMetadataParcel[] loadFastPairAccountDevicesMetadata(
+ FastPairAccountDevicesMetadataRequestParcel requestParcel) {
+ final CountDownLatch waitForCompletionLatch = new CountDownLatch(1);
+ final AtomicReference<FastPairAccountKeyDeviceMetadataParcel[]> response =
+ new AtomicReference<>();
+ mServiceMonitor.runOnBinder(new ServiceMonitor.BinderOperation() {
+ @Override
+ public void run(IBinder binder) throws RemoteException {
+ IFastPairDataProvider provider = IFastPairDataProvider.Stub.asInterface(binder);
+ IFastPairAccountDevicesMetadataCallback callback =
+ new IFastPairAccountDevicesMetadataCallback.Stub() {
+ public void onFastPairAccountDevicesMetadataReceived(
+ FastPairAccountKeyDeviceMetadataParcel[] metadatas) {
+ response.set(metadatas);
+ waitForCompletionLatch.countDown();
+ }
+
+ public void onError(int code, String message) {
+ waitForCompletionLatch.countDown();
+ }
+ };
+ provider.loadFastPairAccountDevicesMetadata(requestParcel, callback);
+ }
+
+ @Override
+ public void onError() {
+ waitForCompletionLatch.countDown();
+ }
+ });
+ try {
+ waitForCompletionLatch.await(TIME_OUT_MILLIS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ // skip.
+ }
+ return response.get();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/Utils.java b/nearby/service/java/com/android/server/nearby/provider/Utils.java
new file mode 100644
index 0000000..0f1c567
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/Utils.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import android.accounts.Account;
+import android.annotation.Nullable;
+import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
+import android.nearby.aidl.FastPairDeviceMetadataParcel;
+import android.nearby.aidl.FastPairDiscoveryItemParcel;
+import android.nearby.aidl.FastPairEligibleAccountParcel;
+
+import com.android.server.nearby.fastpair.footprint.FastPairUploadInfo;
+
+import com.google.protobuf.ByteString;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import service.proto.Cache;
+import service.proto.Data;
+import service.proto.FastPairString.FastPairStrings;
+import service.proto.Rpcs;
+
+/**
+ * Utility functions to convert between different data classes.
+ */
+class Utils {
+
+ static List<Data.FastPairDeviceWithAccountKey> convertToFastPairDevicesWithAccountKey(
+ @Nullable FastPairAccountKeyDeviceMetadataParcel[] metadataParcels) {
+ if (metadataParcels == null) {
+ return new ArrayList<Data.FastPairDeviceWithAccountKey>(0);
+ }
+
+ List<Data.FastPairDeviceWithAccountKey> fpDeviceList =
+ new ArrayList<>(metadataParcels.length);
+ for (FastPairAccountKeyDeviceMetadataParcel metadataParcel : metadataParcels) {
+ if (metadataParcel == null) {
+ continue;
+ }
+ Data.FastPairDeviceWithAccountKey.Builder fpDeviceBuilder =
+ Data.FastPairDeviceWithAccountKey.newBuilder();
+ if (metadataParcel.deviceAccountKey != null) {
+ fpDeviceBuilder.setAccountKey(
+ ByteString.copyFrom(metadataParcel.deviceAccountKey));
+ }
+ if (metadataParcel.sha256DeviceAccountKeyPublicAddress != null) {
+ fpDeviceBuilder.setSha256AccountKeyPublicAddress(
+ ByteString.copyFrom(metadataParcel.sha256DeviceAccountKeyPublicAddress));
+ }
+
+ Cache.StoredDiscoveryItem.Builder storedDiscoveryItemBuilder =
+ Cache.StoredDiscoveryItem.newBuilder();
+
+ if (metadataParcel.discoveryItem != null) {
+ if (metadataParcel.discoveryItem.actionUrl != null) {
+ storedDiscoveryItemBuilder.setActionUrl(metadataParcel.discoveryItem.actionUrl);
+ }
+ Cache.ResolvedUrlType urlType = Cache.ResolvedUrlType.forNumber(
+ metadataParcel.discoveryItem.actionUrlType);
+ if (urlType != null) {
+ storedDiscoveryItemBuilder.setActionUrlType(urlType);
+ }
+ if (metadataParcel.discoveryItem.appName != null) {
+ storedDiscoveryItemBuilder.setAppName(metadataParcel.discoveryItem.appName);
+ }
+ if (metadataParcel.discoveryItem.authenticationPublicKeySecp256r1 != null) {
+ storedDiscoveryItemBuilder.setAuthenticationPublicKeySecp256R1(
+ ByteString.copyFrom(
+ metadataParcel.discoveryItem.authenticationPublicKeySecp256r1));
+ }
+ if (metadataParcel.discoveryItem.description != null) {
+ storedDiscoveryItemBuilder.setDescription(
+ metadataParcel.discoveryItem.description);
+ }
+ if (metadataParcel.discoveryItem.deviceName != null) {
+ storedDiscoveryItemBuilder.setDeviceName(
+ metadataParcel.discoveryItem.deviceName);
+ }
+ if (metadataParcel.discoveryItem.displayUrl != null) {
+ storedDiscoveryItemBuilder.setDisplayUrl(
+ metadataParcel.discoveryItem.displayUrl);
+ }
+ storedDiscoveryItemBuilder.setFirstObservationTimestampMillis(
+ metadataParcel.discoveryItem.firstObservationTimestampMillis);
+ if (metadataParcel.discoveryItem.iconFifeUrl != null) {
+ storedDiscoveryItemBuilder.setIconFifeUrl(
+ metadataParcel.discoveryItem.iconFifeUrl);
+ }
+ if (metadataParcel.discoveryItem.iconPng != null) {
+ storedDiscoveryItemBuilder.setIconPng(
+ ByteString.copyFrom(metadataParcel.discoveryItem.iconPng));
+ }
+ if (metadataParcel.discoveryItem.id != null) {
+ storedDiscoveryItemBuilder.setId(metadataParcel.discoveryItem.id);
+ }
+ storedDiscoveryItemBuilder.setLastObservationTimestampMillis(
+ metadataParcel.discoveryItem.lastObservationTimestampMillis);
+ if (metadataParcel.discoveryItem.macAddress != null) {
+ storedDiscoveryItemBuilder.setMacAddress(
+ metadataParcel.discoveryItem.macAddress);
+ }
+ if (metadataParcel.discoveryItem.packageName != null) {
+ storedDiscoveryItemBuilder.setPackageName(
+ metadataParcel.discoveryItem.packageName);
+ }
+ storedDiscoveryItemBuilder.setPendingAppInstallTimestampMillis(
+ metadataParcel.discoveryItem.pendingAppInstallTimestampMillis);
+ storedDiscoveryItemBuilder.setRssi(metadataParcel.discoveryItem.rssi);
+ Cache.StoredDiscoveryItem.State state =
+ Cache.StoredDiscoveryItem.State.forNumber(
+ metadataParcel.discoveryItem.state);
+ if (state != null) {
+ storedDiscoveryItemBuilder.setState(state);
+ }
+ if (metadataParcel.discoveryItem.title != null) {
+ storedDiscoveryItemBuilder.setTitle(metadataParcel.discoveryItem.title);
+ }
+ if (metadataParcel.discoveryItem.triggerId != null) {
+ storedDiscoveryItemBuilder.setTriggerId(metadataParcel.discoveryItem.triggerId);
+ }
+ storedDiscoveryItemBuilder.setTxPower(metadataParcel.discoveryItem.txPower);
+ }
+ if (metadataParcel.metadata != null) {
+ FastPairStrings.Builder stringsBuilder = FastPairStrings.newBuilder();
+ if (metadataParcel.metadata.connectSuccessCompanionAppInstalled != null) {
+ stringsBuilder.setPairingFinishedCompanionAppInstalled(
+ metadataParcel.metadata.connectSuccessCompanionAppInstalled);
+ }
+ if (metadataParcel.metadata.connectSuccessCompanionAppNotInstalled != null) {
+ stringsBuilder.setPairingFinishedCompanionAppNotInstalled(
+ metadataParcel.metadata.connectSuccessCompanionAppNotInstalled);
+ }
+ if (metadataParcel.metadata.failConnectGoToSettingsDescription != null) {
+ stringsBuilder.setPairingFailDescription(
+ metadataParcel.metadata.failConnectGoToSettingsDescription);
+ }
+ if (metadataParcel.metadata.initialNotificationDescription != null) {
+ stringsBuilder.setTapToPairWithAccount(
+ metadataParcel.metadata.initialNotificationDescription);
+ }
+ if (metadataParcel.metadata.initialNotificationDescriptionNoAccount != null) {
+ stringsBuilder.setTapToPairWithoutAccount(
+ metadataParcel.metadata.initialNotificationDescriptionNoAccount);
+ }
+ if (metadataParcel.metadata.initialPairingDescription != null) {
+ stringsBuilder.setInitialPairingDescription(
+ metadataParcel.metadata.initialPairingDescription);
+ }
+ if (metadataParcel.metadata.retroactivePairingDescription != null) {
+ stringsBuilder.setRetroactivePairingDescription(
+ metadataParcel.metadata.retroactivePairingDescription);
+ }
+ if (metadataParcel.metadata.subsequentPairingDescription != null) {
+ stringsBuilder.setSubsequentPairingDescription(
+ metadataParcel.metadata.subsequentPairingDescription);
+ }
+ if (metadataParcel.metadata.waitLaunchCompanionAppDescription != null) {
+ stringsBuilder.setWaitAppLaunchDescription(
+ metadataParcel.metadata.waitLaunchCompanionAppDescription);
+ }
+ storedDiscoveryItemBuilder.setFastPairStrings(stringsBuilder.build());
+
+ Cache.FastPairInformation.Builder fpInformationBuilder =
+ Cache.FastPairInformation.newBuilder();
+ Rpcs.TrueWirelessHeadsetImages.Builder imagesBuilder =
+ Rpcs.TrueWirelessHeadsetImages.newBuilder();
+ if (metadataParcel.metadata.trueWirelessImageUrlCase != null) {
+ imagesBuilder.setCaseUrl(metadataParcel.metadata.trueWirelessImageUrlCase);
+ }
+ if (metadataParcel.metadata.trueWirelessImageUrlLeftBud != null) {
+ imagesBuilder.setLeftBudUrl(
+ metadataParcel.metadata.trueWirelessImageUrlLeftBud);
+ }
+ if (metadataParcel.metadata.trueWirelessImageUrlRightBud != null) {
+ imagesBuilder.setRightBudUrl(
+ metadataParcel.metadata.trueWirelessImageUrlRightBud);
+ }
+ fpInformationBuilder.setTrueWirelessImages(imagesBuilder.build());
+ Rpcs.DeviceType deviceType =
+ Rpcs.DeviceType.forNumber(metadataParcel.metadata.deviceType);
+ if (deviceType != null) {
+ fpInformationBuilder.setDeviceType(deviceType);
+ }
+
+ storedDiscoveryItemBuilder.setFastPairInformation(fpInformationBuilder.build());
+ }
+ fpDeviceBuilder.setDiscoveryItem(storedDiscoveryItemBuilder.build());
+ fpDeviceList.add(fpDeviceBuilder.build());
+ }
+ return fpDeviceList;
+ }
+
+ static List<Account> convertToAccountList(
+ @Nullable FastPairEligibleAccountParcel[] accountParcels) {
+ if (accountParcels == null) {
+ return new ArrayList<Account>(0);
+ }
+ List<Account> accounts = new ArrayList<Account>(accountParcels.length);
+ for (FastPairEligibleAccountParcel parcel : accountParcels) {
+ if (parcel != null && parcel.account != null) {
+ accounts.add(parcel.account);
+ }
+ }
+ return accounts;
+ }
+
+ private static @Nullable Rpcs.Device convertToDevice(
+ FastPairAntispoofKeyDeviceMetadataParcel metadata) {
+
+ Rpcs.Device.Builder deviceBuilder = Rpcs.Device.newBuilder();
+ if (metadata.antispoofPublicKey != null) {
+ deviceBuilder.setAntiSpoofingKeyPair(Rpcs.AntiSpoofingKeyPair.newBuilder()
+ .setPublicKey(ByteString.copyFrom(metadata.antispoofPublicKey))
+ .build());
+ }
+ if (metadata.deviceMetadata != null) {
+ Rpcs.TrueWirelessHeadsetImages.Builder imagesBuilder =
+ Rpcs.TrueWirelessHeadsetImages.newBuilder();
+ if (metadata.deviceMetadata.trueWirelessImageUrlLeftBud != null) {
+ imagesBuilder.setLeftBudUrl(metadata.deviceMetadata.trueWirelessImageUrlLeftBud);
+ }
+ if (metadata.deviceMetadata.trueWirelessImageUrlRightBud != null) {
+ imagesBuilder.setRightBudUrl(metadata.deviceMetadata.trueWirelessImageUrlRightBud);
+ }
+ if (metadata.deviceMetadata.trueWirelessImageUrlCase != null) {
+ imagesBuilder.setCaseUrl(metadata.deviceMetadata.trueWirelessImageUrlCase);
+ }
+ deviceBuilder.setTrueWirelessImages(imagesBuilder.build());
+ if (metadata.deviceMetadata.imageUrl != null) {
+ deviceBuilder.setImageUrl(metadata.deviceMetadata.imageUrl);
+ }
+ if (metadata.deviceMetadata.intentUri != null) {
+ deviceBuilder.setIntentUri(metadata.deviceMetadata.intentUri);
+ }
+ if (metadata.deviceMetadata.name != null) {
+ deviceBuilder.setName(metadata.deviceMetadata.name);
+ }
+ Rpcs.DeviceType deviceType =
+ Rpcs.DeviceType.forNumber(metadata.deviceMetadata.deviceType);
+ if (deviceType != null) {
+ deviceBuilder.setDeviceType(deviceType);
+ }
+ deviceBuilder.setBleTxPower(metadata.deviceMetadata.bleTxPower)
+ .setTriggerDistance(metadata.deviceMetadata.triggerDistance);
+ }
+
+ return deviceBuilder.build();
+ }
+
+ private static @Nullable ByteString convertToImage(
+ FastPairAntispoofKeyDeviceMetadataParcel metadata) {
+ if (metadata.deviceMetadata == null || metadata.deviceMetadata.image == null) {
+ return null;
+ }
+
+ return ByteString.copyFrom(metadata.deviceMetadata.image);
+ }
+
+ private static @Nullable Rpcs.ObservedDeviceStrings
+ convertToObservedDeviceStrings(FastPairAntispoofKeyDeviceMetadataParcel metadata) {
+ if (metadata.deviceMetadata == null) {
+ return null;
+ }
+
+ Rpcs.ObservedDeviceStrings.Builder stringsBuilder = Rpcs.ObservedDeviceStrings.newBuilder();
+ if (metadata.deviceMetadata.connectSuccessCompanionAppInstalled != null) {
+ stringsBuilder.setConnectSuccessCompanionAppInstalled(
+ metadata.deviceMetadata.connectSuccessCompanionAppInstalled);
+ }
+ if (metadata.deviceMetadata.connectSuccessCompanionAppNotInstalled != null) {
+ stringsBuilder.setConnectSuccessCompanionAppNotInstalled(
+ metadata.deviceMetadata.connectSuccessCompanionAppNotInstalled);
+ }
+ if (metadata.deviceMetadata.downloadCompanionAppDescription != null) {
+ stringsBuilder.setDownloadCompanionAppDescription(
+ metadata.deviceMetadata.downloadCompanionAppDescription);
+ }
+ if (metadata.deviceMetadata.failConnectGoToSettingsDescription != null) {
+ stringsBuilder.setFailConnectGoToSettingsDescription(
+ metadata.deviceMetadata.failConnectGoToSettingsDescription);
+ }
+ if (metadata.deviceMetadata.initialNotificationDescription != null) {
+ stringsBuilder.setInitialNotificationDescription(
+ metadata.deviceMetadata.initialNotificationDescription);
+ }
+ if (metadata.deviceMetadata.initialNotificationDescriptionNoAccount != null) {
+ stringsBuilder.setInitialNotificationDescriptionNoAccount(
+ metadata.deviceMetadata.initialNotificationDescriptionNoAccount);
+ }
+ if (metadata.deviceMetadata.initialPairingDescription != null) {
+ stringsBuilder.setInitialPairingDescription(
+ metadata.deviceMetadata.initialPairingDescription);
+ }
+ if (metadata.deviceMetadata.openCompanionAppDescription != null) {
+ stringsBuilder.setOpenCompanionAppDescription(
+ metadata.deviceMetadata.openCompanionAppDescription);
+ }
+ if (metadata.deviceMetadata.retroactivePairingDescription != null) {
+ stringsBuilder.setRetroactivePairingDescription(
+ metadata.deviceMetadata.retroactivePairingDescription);
+ }
+ if (metadata.deviceMetadata.subsequentPairingDescription != null) {
+ stringsBuilder.setSubsequentPairingDescription(
+ metadata.deviceMetadata.subsequentPairingDescription);
+ }
+ if (metadata.deviceMetadata.unableToConnectDescription != null) {
+ stringsBuilder.setUnableToConnectDescription(
+ metadata.deviceMetadata.unableToConnectDescription);
+ }
+ if (metadata.deviceMetadata.unableToConnectTitle != null) {
+ stringsBuilder.setUnableToConnectTitle(
+ metadata.deviceMetadata.unableToConnectTitle);
+ }
+ if (metadata.deviceMetadata.updateCompanionAppDescription != null) {
+ stringsBuilder.setUpdateCompanionAppDescription(
+ metadata.deviceMetadata.updateCompanionAppDescription);
+ }
+ if (metadata.deviceMetadata.waitLaunchCompanionAppDescription != null) {
+ stringsBuilder.setWaitLaunchCompanionAppDescription(
+ metadata.deviceMetadata.waitLaunchCompanionAppDescription);
+ }
+
+ return stringsBuilder.build();
+ }
+
+ static @Nullable Rpcs.GetObservedDeviceResponse
+ convertToGetObservedDeviceResponse(
+ @Nullable FastPairAntispoofKeyDeviceMetadataParcel metadata) {
+ if (metadata == null) {
+ return null;
+ }
+
+ Rpcs.GetObservedDeviceResponse.Builder responseBuilder =
+ Rpcs.GetObservedDeviceResponse.newBuilder();
+
+ Rpcs.Device device = convertToDevice(metadata);
+ if (device != null) {
+ responseBuilder.setDevice(device);
+ }
+ ByteString image = convertToImage(metadata);
+ if (image != null) {
+ responseBuilder.setImage(image);
+ }
+ Rpcs.ObservedDeviceStrings strings = convertToObservedDeviceStrings(metadata);
+ if (strings != null) {
+ responseBuilder.setStrings(strings);
+ }
+
+ return responseBuilder.build();
+ }
+
+ static @Nullable FastPairAccountKeyDeviceMetadataParcel
+ convertToFastPairAccountKeyDeviceMetadata(
+ @Nullable FastPairUploadInfo uploadInfo) {
+ if (uploadInfo == null) {
+ return null;
+ }
+
+ FastPairAccountKeyDeviceMetadataParcel accountKeyDeviceMetadataParcel =
+ new FastPairAccountKeyDeviceMetadataParcel();
+ if (uploadInfo.getAccountKey() != null) {
+ accountKeyDeviceMetadataParcel.deviceAccountKey =
+ uploadInfo.getAccountKey().toByteArray();
+ }
+ if (uploadInfo.getSha256AccountKeyPublicAddress() != null) {
+ accountKeyDeviceMetadataParcel.sha256DeviceAccountKeyPublicAddress =
+ uploadInfo.getSha256AccountKeyPublicAddress().toByteArray();
+ }
+ if (uploadInfo.getStoredDiscoveryItem() != null) {
+ accountKeyDeviceMetadataParcel.metadata =
+ convertToFastPairDeviceMetadata(uploadInfo.getStoredDiscoveryItem());
+ accountKeyDeviceMetadataParcel.discoveryItem =
+ convertToFastPairDiscoveryItem(uploadInfo.getStoredDiscoveryItem());
+ }
+
+ return accountKeyDeviceMetadataParcel;
+ }
+
+ private static @Nullable FastPairDiscoveryItemParcel
+ convertToFastPairDiscoveryItem(Cache.StoredDiscoveryItem storedDiscoveryItem) {
+ FastPairDiscoveryItemParcel discoveryItemParcel = new FastPairDiscoveryItemParcel();
+ discoveryItemParcel.actionUrl = storedDiscoveryItem.getActionUrl();
+ discoveryItemParcel.actionUrlType = storedDiscoveryItem.getActionUrlType().getNumber();
+ discoveryItemParcel.appName = storedDiscoveryItem.getAppName();
+ discoveryItemParcel.authenticationPublicKeySecp256r1 =
+ storedDiscoveryItem.getAuthenticationPublicKeySecp256R1().toByteArray();
+ discoveryItemParcel.description = storedDiscoveryItem.getDescription();
+ discoveryItemParcel.deviceName = storedDiscoveryItem.getDeviceName();
+ discoveryItemParcel.displayUrl = storedDiscoveryItem.getDisplayUrl();
+ discoveryItemParcel.firstObservationTimestampMillis =
+ storedDiscoveryItem.getFirstObservationTimestampMillis();
+ discoveryItemParcel.iconFifeUrl = storedDiscoveryItem.getIconFifeUrl();
+ discoveryItemParcel.iconPng = storedDiscoveryItem.getIconPng().toByteArray();
+ discoveryItemParcel.id = storedDiscoveryItem.getId();
+ discoveryItemParcel.lastObservationTimestampMillis =
+ storedDiscoveryItem.getLastObservationTimestampMillis();
+ discoveryItemParcel.macAddress = storedDiscoveryItem.getMacAddress();
+ discoveryItemParcel.packageName = storedDiscoveryItem.getPackageName();
+ discoveryItemParcel.pendingAppInstallTimestampMillis =
+ storedDiscoveryItem.getPendingAppInstallTimestampMillis();
+ discoveryItemParcel.rssi = storedDiscoveryItem.getRssi();
+ discoveryItemParcel.state = storedDiscoveryItem.getState().getNumber();
+ discoveryItemParcel.title = storedDiscoveryItem.getTitle();
+ discoveryItemParcel.triggerId = storedDiscoveryItem.getTriggerId();
+ discoveryItemParcel.txPower = storedDiscoveryItem.getTxPower();
+
+ return discoveryItemParcel;
+ }
+
+ /* Do we upload these?
+ String downloadCompanionAppDescription =
+ bundle.getString("downloadCompanionAppDescription");
+ String locale = bundle.getString("locale");
+ String openCompanionAppDescription = bundle.getString("openCompanionAppDescription");
+ float triggerDistance = bundle.getFloat("triggerDistance");
+ String unableToConnectDescription = bundle.getString("unableToConnectDescription");
+ String unableToConnectTitle = bundle.getString("unableToConnectTitle");
+ String updateCompanionAppDescription = bundle.getString("updateCompanionAppDescription");
+ */
+ private static @Nullable FastPairDeviceMetadataParcel
+ convertToFastPairDeviceMetadata(Cache.StoredDiscoveryItem storedDiscoveryItem) {
+ FastPairStrings fpStrings = storedDiscoveryItem.getFastPairStrings();
+
+ FastPairDeviceMetadataParcel metadataParcel = new FastPairDeviceMetadataParcel();
+ metadataParcel.connectSuccessCompanionAppInstalled =
+ fpStrings.getPairingFinishedCompanionAppInstalled();
+ metadataParcel.connectSuccessCompanionAppNotInstalled =
+ fpStrings.getPairingFinishedCompanionAppNotInstalled();
+ metadataParcel.failConnectGoToSettingsDescription = fpStrings.getPairingFailDescription();
+ metadataParcel.initialNotificationDescription = fpStrings.getTapToPairWithAccount();
+ metadataParcel.initialNotificationDescriptionNoAccount =
+ fpStrings.getTapToPairWithoutAccount();
+ metadataParcel.initialPairingDescription = fpStrings.getInitialPairingDescription();
+ metadataParcel.retroactivePairingDescription = fpStrings.getRetroactivePairingDescription();
+ metadataParcel.subsequentPairingDescription = fpStrings.getSubsequentPairingDescription();
+ metadataParcel.waitLaunchCompanionAppDescription = fpStrings.getWaitAppLaunchDescription();
+
+ Cache.FastPairInformation fpInformation = storedDiscoveryItem.getFastPairInformation();
+ metadataParcel.trueWirelessImageUrlCase =
+ fpInformation.getTrueWirelessImages().getCaseUrl();
+ metadataParcel.trueWirelessImageUrlLeftBud =
+ fpInformation.getTrueWirelessImages().getLeftBudUrl();
+ metadataParcel.trueWirelessImageUrlRightBud =
+ fpInformation.getTrueWirelessImages().getRightBudUrl();
+ metadataParcel.deviceType = fpInformation.getDeviceType().getNumber();
+
+ return metadataParcel;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
new file mode 100644
index 0000000..599843c
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util;
+
+import java.util.Arrays;
+
+/**
+ * ArrayUtils class that help manipulate array.
+ */
+public class ArrayUtils {
+ /** Concatenate N arrays of bytes into a single array. */
+ public static byte[] concatByteArrays(byte[]... arrays) {
+ // Degenerate case - no input provided.
+ if (arrays.length == 0) {
+ return new byte[0];
+ }
+
+ // Compute the total size.
+ int totalSize = 0;
+ for (int i = 0; i < arrays.length; i++) {
+ totalSize += arrays[i].length;
+ }
+
+ // Copy the arrays into the new array.
+ byte[] result = Arrays.copyOf(arrays[0], totalSize);
+ int pos = arrays[0].length;
+ for (int i = 1; i < arrays.length; i++) {
+ byte[] current = arrays[i];
+ System.arraycopy(current, 0, result, pos, current.length);
+ pos += current.length;
+ }
+ return result;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/DataUtils.java b/nearby/service/java/com/android/server/nearby/util/DataUtils.java
new file mode 100644
index 0000000..8bb83e9
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/DataUtils.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import service.proto.Cache.ScanFastPairStoreItem;
+import service.proto.Cache.StoredDiscoveryItem;
+import service.proto.FastPairString.FastPairStrings;
+import service.proto.Rpcs.Device;
+import service.proto.Rpcs.GetObservedDeviceResponse;
+import service.proto.Rpcs.ObservedDeviceStrings;
+
+/**
+ * Utils class converts different data types {@link ScanFastPairStoreItem},
+ * {@link StoredDiscoveryItem} and {@link GetObservedDeviceResponse},
+ *
+ */
+public final class DataUtils {
+
+ /**
+ * Converts a {@link GetObservedDeviceResponse} to a {@link ScanFastPairStoreItem}.
+ */
+ public static ScanFastPairStoreItem toScanFastPairStoreItem(
+ GetObservedDeviceResponse observedDeviceResponse,
+ @NonNull String bleAddress, @Nullable String account) {
+ Device device = observedDeviceResponse.getDevice();
+ String deviceName = device.getName();
+ return ScanFastPairStoreItem.newBuilder()
+ .setAddress(bleAddress)
+ .setActionUrl(device.getIntentUri())
+ .setDeviceName(deviceName)
+ .setIconPng(observedDeviceResponse.getImage())
+ .setIconFifeUrl(device.getImageUrl())
+ .setAntiSpoofingPublicKey(device.getAntiSpoofingKeyPair().getPublicKey())
+ .setFastPairStrings(getFastPairStrings(observedDeviceResponse, deviceName, account))
+ .build();
+ }
+
+ /**
+ * Prints readable string for a {@link ScanFastPairStoreItem}.
+ */
+ public static String toString(ScanFastPairStoreItem item) {
+ return "ScanFastPairStoreItem=[address:" + item.getAddress()
+ + ", actionUr:" + item.getActionUrl()
+ + ", deviceName:" + item.getDeviceName()
+ + ", iconPng:" + item.getIconPng()
+ + ", iconFifeUrl:" + item.getIconFifeUrl()
+ + ", antiSpoofingKeyPair:" + item.getAntiSpoofingPublicKey()
+ + ", fastPairStrings:" + toString(item.getFastPairStrings())
+ + "]";
+ }
+
+ /**
+ * Prints readable string for a {@link FastPairStrings}
+ */
+ public static String toString(FastPairStrings fastPairStrings) {
+ return "FastPairStrings["
+ + "tapToPairWithAccount=" + fastPairStrings.getTapToPairWithAccount()
+ + ", tapToPairWithoutAccount=" + fastPairStrings.getTapToPairWithoutAccount()
+ + ", initialPairingDescription=" + fastPairStrings.getInitialPairingDescription()
+ + ", pairingFinishedCompanionAppInstalled="
+ + fastPairStrings.getPairingFinishedCompanionAppInstalled()
+ + ", pairingFinishedCompanionAppNotInstalled="
+ + fastPairStrings.getPairingFinishedCompanionAppNotInstalled()
+ + ", subsequentPairingDescription="
+ + fastPairStrings.getSubsequentPairingDescription()
+ + ", retroactivePairingDescription="
+ + fastPairStrings.getRetroactivePairingDescription()
+ + ", waitAppLaunchDescription=" + fastPairStrings.getWaitAppLaunchDescription()
+ + ", pairingFailDescription=" + fastPairStrings.getPairingFailDescription()
+ + "]";
+ }
+
+ private static FastPairStrings getFastPairStrings(GetObservedDeviceResponse response,
+ String deviceName, @Nullable String account) {
+ ObservedDeviceStrings strings = response.getStrings();
+ return FastPairStrings.newBuilder()
+ .setTapToPairWithAccount(strings.getInitialNotificationDescription())
+ .setTapToPairWithoutAccount(
+ strings.getInitialNotificationDescriptionNoAccount())
+ .setInitialPairingDescription(account == null
+ ? strings.getInitialNotificationDescriptionNoAccount()
+ : String.format(strings.getInitialPairingDescription(),
+ deviceName, account))
+ .setPairingFinishedCompanionAppInstalled(
+ strings.getConnectSuccessCompanionAppInstalled())
+ .setPairingFinishedCompanionAppNotInstalled(
+ strings.getConnectSuccessCompanionAppNotInstalled())
+ .setSubsequentPairingDescription(strings.getSubsequentPairingDescription())
+ .setRetroactivePairingDescription(strings.getRetroactivePairingDescription())
+ .setWaitAppLaunchDescription(strings.getWaitLaunchCompanionAppDescription())
+ .setPairingFailDescription(strings.getFailConnectGoToSettingsDescription())
+ .build();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/Environment.java b/nearby/service/java/com/android/server/nearby/util/Environment.java
new file mode 100644
index 0000000..d397862
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/Environment.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util;
+
+import android.content.ApexEnvironment;
+import android.content.pm.ApplicationInfo;
+import android.os.UserHandle;
+
+import java.io.File;
+
+/**
+ * Provides function to make sure the function caller is from the same apex.
+ */
+public class Environment {
+ /**
+ * NEARBY apex name.
+ */
+ private static final String NEARBY_APEX_NAME = "com.android.tethering";
+
+ /**
+ * The path where the Nearby apex is mounted.
+ * Current value = "/apex/com.android.tethering"
+ */
+ private static final String NEARBY_APEX_PATH =
+ new File("/apex", NEARBY_APEX_NAME).getAbsolutePath();
+
+ /**
+ * Nearby shared folder.
+ */
+ public static File getNearbyDirectory() {
+ return ApexEnvironment.getApexEnvironment(NEARBY_APEX_NAME).getDeviceProtectedDataDir();
+ }
+
+ /**
+ * Nearby user specific folder.
+ */
+ public static File getNearbyDirectory(int userId) {
+ return ApexEnvironment.getApexEnvironment(NEARBY_APEX_NAME)
+ .getCredentialProtectedDataDirForUser(UserHandle.of(userId));
+ }
+
+ /**
+ * Returns true if the app is in the nearby apex, false otherwise.
+ * Checks if the app's path starts with "/apex/com.android.tethering".
+ */
+ public static boolean isAppInNearbyApex(ApplicationInfo appInfo) {
+ return appInfo.sourceDir.startsWith(NEARBY_APEX_PATH);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/FastPairDecoder.java b/nearby/service/java/com/android/server/nearby/util/FastPairDecoder.java
new file mode 100644
index 0000000..6021ff6
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/FastPairDecoder.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util;
+
+import android.annotation.Nullable;
+import android.bluetooth.le.ScanRecord;
+import android.os.ParcelUuid;
+import android.util.SparseArray;
+
+import com.android.server.nearby.common.ble.BleFilter;
+import com.android.server.nearby.common.ble.BleRecord;
+
+import java.util.Arrays;
+
+/**
+ * Parses Fast Pair information out of {@link BleRecord}s.
+ *
+ * <p>There are 2 different packet formats that are supported, which is used can be determined by
+ * packet length:
+ *
+ * <p>For 3-byte packets, the full packet is the model ID.
+ *
+ * <p>For all other packets, the first byte is the header, followed by the model ID, followed by
+ * zero or more extra fields. Each field has its own header byte followed by the field value. The
+ * packet header is formatted as 0bVVVLLLLR (V = version, L = model ID length, R = reserved) and
+ * each extra field header is 0bLLLLTTTT (L = field length, T = field type).
+ */
+public class FastPairDecoder {
+
+ private static final int FIELD_TYPE_BLOOM_FILTER = 0;
+ private static final int FIELD_TYPE_BLOOM_FILTER_SALT = 1;
+ private static final int FIELD_TYPE_BLOOM_FILTER_NO_NOTIFICATION = 2;
+ private static final int FIELD_TYPE_BATTERY = 3;
+ private static final int FIELD_TYPE_BATTERY_NO_NOTIFICATION = 4;
+ public static final int FIELD_TYPE_CONNECTION_STATE = 5;
+ private static final int FIELD_TYPE_RANDOM_RESOLVABLE_DATA = 6;
+
+
+ /** FE2C is the 16-bit Service UUID. The rest is the base UUID. See BluetoothUuid (hidden). */
+ private static final ParcelUuid FAST_PAIR_SERVICE_PARCEL_UUID =
+ ParcelUuid.fromString("0000FE2C-0000-1000-8000-00805F9B34FB");
+
+ /** The filter you use to scan for Fast Pair BLE advertisements. */
+ public static final BleFilter FILTER =
+ new BleFilter.Builder().setServiceData(FAST_PAIR_SERVICE_PARCEL_UUID,
+ new byte[0]).build();
+
+ // NOTE: Ensure that all bitmasks are always ints, not bytes so that bitshifting works correctly
+ // without needing worry about signing errors.
+ private static final int HEADER_VERSION_BITMASK = 0b11100000;
+ private static final int HEADER_LENGTH_BITMASK = 0b00011110;
+ private static final int HEADER_VERSION_OFFSET = 5;
+ private static final int HEADER_LENGTH_OFFSET = 1;
+
+ private static final int EXTRA_FIELD_LENGTH_BITMASK = 0b11110000;
+ private static final int EXTRA_FIELD_TYPE_BITMASK = 0b00001111;
+ private static final int EXTRA_FIELD_LENGTH_OFFSET = 4;
+ private static final int EXTRA_FIELD_TYPE_OFFSET = 0;
+
+ private static final int MIN_ID_LENGTH = 3;
+ private static final int MAX_ID_LENGTH = 14;
+ private static final int HEADER_INDEX = 0;
+ private static final int HEADER_LENGTH = 1;
+ private static final int FIELD_HEADER_LENGTH = 1;
+
+ // Not using java.util.IllegalFormatException because it is unchecked.
+ private static class IllegalFormatException extends Exception {
+ private IllegalFormatException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Gets model id data from broadcast
+ */
+ @Nullable
+ public static byte[] getModelId(@Nullable byte[] serviceData) {
+ if (serviceData == null) {
+ return null;
+ }
+
+ if (serviceData.length >= MIN_ID_LENGTH) {
+ if (serviceData.length == MIN_ID_LENGTH) {
+ // If the length == 3, all bytes are the ID. See flag docs for more about
+ // endianness.
+ return serviceData;
+ } else {
+ // Otherwise, the first byte is a header which contains the length of the big-endian
+ // model ID that follows. The model ID will be trimmed if it contains leading zeros.
+ int idIndex = 1;
+ int end = idIndex + getIdLength(serviceData);
+ while (serviceData[idIndex] == 0 && end - idIndex > MIN_ID_LENGTH) {
+ idIndex++;
+ }
+ return Arrays.copyOfRange(serviceData, idIndex, end);
+ }
+ }
+ return null;
+ }
+
+ /** Gets the FastPair service data array if available, otherwise returns null. */
+ @Nullable
+ public static byte[] getServiceDataArray(BleRecord bleRecord) {
+ return bleRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
+ }
+
+ /** Gets the FastPair service data array if available, otherwise returns null. */
+ @Nullable
+ public static byte[] getServiceDataArray(ScanRecord scanRecord) {
+ return scanRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
+ }
+
+ /** Gets the bloom filter from the extra fields if available, otherwise returns null. */
+ @Nullable
+ public static byte[] getBloomFilter(@Nullable byte[] serviceData) {
+ return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER);
+ }
+
+ /** Gets the bloom filter salt from the extra fields if available, otherwise returns null. */
+ @Nullable
+ public static byte[] getBloomFilterSalt(byte[] serviceData) {
+ return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER_SALT);
+ }
+
+ /**
+ * Gets the suppress notification with bloom filter from the extra fields if available,
+ * otherwise returns null.
+ */
+ @Nullable
+ public static byte[] getBloomFilterNoNotification(@Nullable byte[] serviceData) {
+ return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER_NO_NOTIFICATION);
+ }
+
+ /**
+ * Get random resolvableData
+ */
+ @Nullable
+ public static byte[] getRandomResolvableData(byte[] serviceData) {
+ return getExtraField(serviceData, FIELD_TYPE_RANDOM_RESOLVABLE_DATA);
+ }
+
+ @Nullable
+ private static byte[] getExtraField(@Nullable byte[] serviceData, int fieldId) {
+ if (serviceData == null || serviceData.length < HEADER_INDEX + HEADER_LENGTH) {
+ return null;
+ }
+ try {
+ return getExtraFields(serviceData).get(fieldId);
+ } catch (IllegalFormatException e) {
+ return null;
+ }
+ }
+
+ /** Gets extra field data at the end of the packet, defined by the extra field header. */
+ private static SparseArray<byte[]> getExtraFields(byte[] serviceData)
+ throws IllegalFormatException {
+ SparseArray<byte[]> extraFields = new SparseArray<>();
+ if (getVersion(serviceData) != 0) {
+ return extraFields;
+ }
+ int headerIndex = getFirstExtraFieldHeaderIndex(serviceData);
+ while (headerIndex < serviceData.length) {
+ int length = getExtraFieldLength(serviceData, headerIndex);
+ int index = headerIndex + FIELD_HEADER_LENGTH;
+ int type = getExtraFieldType(serviceData, headerIndex);
+ int end = index + length;
+ if (extraFields.get(type) == null) {
+ if (end <= serviceData.length) {
+ extraFields.put(type, Arrays.copyOfRange(serviceData, index, end));
+ } else {
+ throw new IllegalFormatException(
+ "Invalid length, " + end + " is longer than service data size "
+ + serviceData.length);
+ }
+ }
+ headerIndex = end;
+ }
+ return extraFields;
+ }
+
+ /** Checks whether or not a valid ID is included in the service data packet. */
+ public static boolean hasBeaconIdBytes(BleRecord bleRecord) {
+ byte[] serviceData = bleRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
+ return checkModelId(serviceData);
+ }
+
+ /** Check whether byte array is FastPair model id or not. */
+ public static boolean checkModelId(@Nullable byte[] scanResult) {
+ return scanResult != null
+ // The 3-byte format has no header byte (all bytes are the ID).
+ && (scanResult.length == MIN_ID_LENGTH
+ // Header byte exists. We support only format version 0. (A different version
+ // indicates
+ // a breaking change in the format.)
+ || (scanResult.length > MIN_ID_LENGTH
+ && getVersion(scanResult) == 0
+ && isIdLengthValid(scanResult)));
+ }
+
+ /** Checks whether or not bloom filter is included in the service data packet. */
+ public static boolean hasBloomFilter(BleRecord bleRecord) {
+ return (getBloomFilter(getServiceDataArray(bleRecord)) != null
+ || getBloomFilterNoNotification(getServiceDataArray(bleRecord)) != null);
+ }
+
+ /** Checks whether or not bloom filter is included in the service data packet. */
+ public static boolean hasBloomFilter(ScanRecord scanRecord) {
+ return (getBloomFilter(getServiceDataArray(scanRecord)) != null
+ || getBloomFilterNoNotification(getServiceDataArray(scanRecord)) != null);
+ }
+
+ private static int getVersion(byte[] serviceData) {
+ return serviceData.length == MIN_ID_LENGTH
+ ? 0
+ : (serviceData[HEADER_INDEX] & HEADER_VERSION_BITMASK) >> HEADER_VERSION_OFFSET;
+ }
+
+ private static int getIdLength(byte[] serviceData) {
+ return serviceData.length == MIN_ID_LENGTH
+ ? MIN_ID_LENGTH
+ : (serviceData[HEADER_INDEX] & HEADER_LENGTH_BITMASK) >> HEADER_LENGTH_OFFSET;
+ }
+
+ private static int getFirstExtraFieldHeaderIndex(byte[] serviceData) {
+ return HEADER_INDEX + HEADER_LENGTH + getIdLength(serviceData);
+ }
+
+ private static int getExtraFieldLength(byte[] serviceData, int extraFieldIndex) {
+ return (serviceData[extraFieldIndex] & EXTRA_FIELD_LENGTH_BITMASK)
+ >> EXTRA_FIELD_LENGTH_OFFSET;
+ }
+
+ private static int getExtraFieldType(byte[] serviceData, int extraFieldIndex) {
+ return (serviceData[extraFieldIndex] & EXTRA_FIELD_TYPE_BITMASK) >> EXTRA_FIELD_TYPE_OFFSET;
+ }
+
+ private static boolean isIdLengthValid(byte[] serviceData) {
+ int idLength = getIdLength(serviceData);
+ return MIN_ID_LENGTH <= idLength
+ && idLength <= MAX_ID_LENGTH
+ && idLength + HEADER_LENGTH <= serviceData.length;
+ }
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/util/ForegroundThread.java b/nearby/service/java/com/android/server/nearby/util/ForegroundThread.java
new file mode 100644
index 0000000..793ab9a
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/ForegroundThread.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * Shared singleton foreground thread.
+ */
+public class ForegroundThread extends HandlerThread {
+ private static final Object sLock = new Object();
+
+ @GuardedBy("sLock")
+ private static ForegroundThread sInstance;
+ @GuardedBy("sLock")
+ private static Handler sHandler;
+ @GuardedBy("sLock")
+ private static Executor sExecutor;
+
+ private ForegroundThread() {
+ super(ForegroundThread.class.getName());
+ }
+
+ @GuardedBy("sLock")
+ private static void ensureInstanceLocked() {
+ if (sInstance == null) {
+ sInstance = new ForegroundThread();
+ sInstance.start();
+ sHandler = new Handler(sInstance.getLooper());
+ sExecutor = new HandlerExecutor(sHandler);
+ }
+ }
+
+ /**
+ * Get the singleton instance of thi class.
+ *
+ * @return the singleton instance of thi class
+ */
+ @NonNull
+ public static ForegroundThread get() {
+ synchronized (sLock) {
+ ensureInstanceLocked();
+ return sInstance;
+ }
+ }
+
+ /**
+ * Get the {@link Handler} for this thread.
+ *
+ * @return the {@link Handler} for this thread.
+ */
+ @NonNull
+ public static Handler getHandler() {
+ synchronized (sLock) {
+ ensureInstanceLocked();
+ return sHandler;
+ }
+ }
+
+ /**
+ * Get the {@link Executor} for this thread.
+ *
+ * @return the {@link Executor} for this thread.
+ */
+ @NonNull
+ public static Executor getExecutor() {
+ synchronized (sLock) {
+ ensureInstanceLocked();
+ return sExecutor;
+ }
+ }
+
+ /**
+ * An adapter {@link Executor} that posts all executed tasks onto the given
+ * {@link Handler}.
+ */
+ private static class HandlerExecutor implements Executor {
+ private final Handler mHandler;
+
+ HandlerExecutor(@NonNull Handler handler) {
+ mHandler = Preconditions.checkNotNull(handler);
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ if (!mHandler.post(command)) {
+ throw new RejectedExecutionException(mHandler + " is shutting down");
+ }
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/Hex.java b/nearby/service/java/com/android/server/nearby/util/Hex.java
new file mode 100644
index 0000000..1d1d855
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/Hex.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util;
+
+/**
+ * Hex class that contains hex related functions.
+ */
+public class Hex {
+
+ private static final char[] HEX_UPPERCASE = {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+ };
+
+ private static final char[] HEX_LOWERCASE = {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
+ };
+
+ /**
+ * Bytes array to lower case string.
+ */
+ public static String bytesToStringLowercase(byte[] bytes) {
+ char[] hexChars = new char[bytes.length * 2];
+ int j = 0;
+ for (byte aByte : bytes) {
+ int v = aByte & 0xFF;
+ hexChars[j++] = HEX_LOWERCASE[v >>> 4];
+ hexChars[j++] = HEX_LOWERCASE[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+
+ /**
+ * Encodes the byte array to string.
+ */
+ public static String bytesToStringUppercase(byte[] bytes) {
+ return bytesToStringUppercase(bytes, false /* zeroTerminated */);
+ }
+
+ /** Encodes a byte array as a hexadecimal representation of bytes. */
+ public static String bytesToStringUppercase(byte[] bytes, boolean zeroTerminated) {
+ int length = bytes.length;
+ StringBuilder out = new StringBuilder(length * 2);
+ for (int i = 0; i < length; i++) {
+ if (zeroTerminated && i == length - 1 && (bytes[i] & 0xff) == 0) {
+ break;
+ }
+ out.append(HEX_UPPERCASE[(bytes[i] & 0xf0) >>> 4]);
+ out.append(HEX_UPPERCASE[bytes[i] & 0x0f]);
+ }
+ return out.toString();
+ }
+ /**
+ * Converts string to byte array.
+ */
+ public static byte[] stringToBytes(String hex) throws IllegalArgumentException {
+ int length = hex.length();
+ if (length % 2 != 0) {
+ throw new IllegalArgumentException("Hex string has odd number of characters");
+ }
+ byte[] out = new byte[length / 2];
+ for (int i = 0; i < length; i += 2) {
+ // Byte.parseByte() doesn't work here because it expects a hex value in -128, 127, and
+ // our hex values are in 0, 255.
+ out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
+ }
+ return out;
+ }
+}
diff --git a/nearby/service/proto/Android.bp b/nearby/service/proto/Android.bp
new file mode 100644
index 0000000..1b00cf6
--- /dev/null
+++ b/nearby/service/proto/Android.bp
@@ -0,0 +1,44 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "fast-pair-lite-protos",
+ proto: {
+ type: "lite",
+ canonical_path_from_root: false,
+ },
+ sdk_version: "system_current",
+ min_sdk_version: "30",
+ srcs: ["src/fastpair/*.proto"],
+ apex_available: [
+ "com.android.tethering",
+ ],
+}
+
+java_library {
+ name: "presence-lite-protos",
+ proto: {
+ type: "lite",
+ canonical_path_from_root: false,
+ },
+ sdk_version: "system_current",
+ min_sdk_version: "30",
+ srcs: ["src/presence/*.proto"],
+ apex_available: [
+ "com.android.tethering",
+ ],
+}
\ No newline at end of file
diff --git a/nearby/service/proto/src/fastpair/cache.proto b/nearby/service/proto/src/fastpair/cache.proto
new file mode 100644
index 0000000..d4c7c3d
--- /dev/null
+++ b/nearby/service/proto/src/fastpair/cache.proto
@@ -0,0 +1,427 @@
+syntax = "proto3";
+package service.proto;
+import "src/fastpair/rpcs.proto";
+import "src/fastpair/fast_pair_string.proto";
+
+// db information for Fast Pair that gets from server.
+message ServerResponseDbItem {
+ // Device's model id.
+ string model_id = 1;
+
+ // Response was received from the server. Contains data needed to display
+ // FastPair notification such as device name, txPower of device, image used
+ // in the notification, etc.
+ GetObservedDeviceResponse get_observed_device_response = 2;
+
+ // The timestamp that make the server fetch.
+ int64 last_fetch_info_timestamp_millis = 3;
+
+ // Whether the item in the cache is expirable or not (when offline mode this
+ // will be false).
+ bool expirable = 4;
+}
+
+
+// Client side scan result.
+message StoredScanResult {
+ // REQUIRED
+ // Unique ID generated based on scan result
+ string id = 1;
+
+ // REQUIRED
+ NearbyType type = 2;
+
+ // REQUIRED
+ // The most recent all upper case mac associated with this item.
+ // (Mac-to-DiscoveryItem is a many-to-many relationship)
+ string mac_address = 4;
+
+ // Beacon's RSSI value
+ int32 rssi = 10;
+
+ // Beacon's tx power
+ int32 tx_power = 11;
+
+ // The mac address encoded in beacon advertisement. Currently only used by
+ // chromecast.
+ string device_setup_mac = 12;
+
+ // Uptime of the device in minutes. Stops incrementing at 255.
+ int32 uptime_minutes = 13;
+
+ // REQUIRED
+ // Client timestamp when the beacon was first observed in BLE scan.
+ int64 first_observation_timestamp_millis = 14;
+
+ // REQUIRED
+ // Client timestamp when the beacon was last observed in BLE scan.
+ int64 last_observation_timestamp_millis = 15;
+
+ // Deprecated fields.
+ reserved 3, 5, 6, 7, 8, 9;
+}
+
+
+// Data for a DiscoveryItem created from server response and client scan result.
+// Only caching original data from scan result, server response, timestamps
+// and user actions. Do not save generated data in this object.
+// Next ID: 50
+message StoredDiscoveryItem {
+ enum State {
+ // Default unknown state.
+ STATE_UNKNOWN = 0;
+
+ // The item is normal.
+ STATE_ENABLED = 1;
+
+ // The item has been muted by user.
+ STATE_MUTED = 2;
+
+ // The item has been disabled by us (likely temporarily).
+ STATE_DISABLED_BY_SYSTEM = 3;
+ }
+
+ // The status of the item.
+ // TODO(b/204409421) remove enum
+ enum DebugMessageCategory {
+ // Default unknown state.
+ STATUS_UNKNOWN = 0;
+
+ // The item is valid and visible in notification.
+ STATUS_VALID_NOTIFICATION = 1;
+
+ // The item made it to list but not to notification.
+ STATUS_VALID_LIST_VIEW = 2;
+
+ // The item is filtered out on client. Never made it to list view.
+ STATUS_DISABLED_BY_CLIENT = 3;
+
+ // The item is filtered out by server. Never made it to client.
+ STATUS_DISABLED_BY_SERVER = 4;
+ }
+
+ enum ExperienceType {
+ EXPERIENCE_UNKNOWN = 0;
+ EXPERIENCE_GOOD = 1;
+ EXPERIENCE_BAD = 2;
+ }
+
+ // REQUIRED
+ // Offline item: unique ID generated on client.
+ // Online item: unique ID generated on server.
+ string id = 1;
+
+ // REQUIRED
+ // The most recent all upper case mac associated with this item.
+ // (Mac-to-DiscoveryItem is a many-to-many relationship)
+ string mac_address = 4;
+
+ // REQUIRED
+ string action_url = 5;
+
+ // The bluetooth device name from advertisment
+ string device_name = 6;
+
+ // REQUIRED
+ // Item's title
+ string title = 7;
+
+ // Item's description.
+ string description = 8;
+
+ // The URL for display
+ string display_url = 9;
+
+ // REQUIRED
+ // Client timestamp when the beacon was last observed in BLE scan.
+ int64 last_observation_timestamp_millis = 10;
+
+ // REQUIRED
+ // Client timestamp when the beacon was first observed in BLE scan.
+ int64 first_observation_timestamp_millis = 11;
+
+ // REQUIRED
+ // Item's current state. e.g. if the item is blocked.
+ State state = 17;
+
+ // The resolved url type for the action_url.
+ ResolvedUrlType action_url_type = 19;
+
+ // The timestamp when the user is redirected to Play Store after clicking on
+ // the item.
+ int64 pending_app_install_timestamp_millis = 20;
+
+ // Beacon's RSSI value
+ int32 rssi = 22;
+
+ // Beacon's tx power
+ int32 tx_power = 23;
+
+ // Human readable name of the app designated to open the uri
+ // Used in the second line of the notification, "Open in {} app"
+ string app_name = 25;
+
+ // The timestamp when the attachment was created on PBS server. In case there
+ // are duplicate
+ // items with the same scanId/groupID, only show the one with the latest
+ // timestamp.
+ int64 attachment_creation_sec = 28;
+
+ // Package name of the App that owns this item.
+ string package_name = 30;
+
+ // The average star rating of the app.
+ float star_rating = 31;
+
+ // TriggerId identifies the trigger/beacon that is attached with a message.
+ // It's generated from server for online messages to synchronize formatting
+ // across client versions.
+ // Example:
+ // * BLE_UID: 3||deadbeef
+ // * BLE_URL: http://trigger.id
+ // See go/discovery-store-message-and-trigger-id for more details.
+ string trigger_id = 34;
+
+ // Bytes of item icon in PNG format displayed in Discovery item list.
+ bytes icon_png = 36;
+
+ // A FIFE URL of the item icon displayed in Discovery item list.
+ string icon_fife_url = 49;
+
+ // See equivalent field in NearbyItem.
+ bytes authentication_public_key_secp256r1 = 45;
+
+ // See equivalent field in NearbyItem.
+ FastPairInformation fast_pair_information = 46;
+
+ // Companion app detail.
+ CompanionAppDetails companion_detail = 47;
+
+ // Fast pair strings
+ FastPairStrings fast_pair_strings = 48;
+
+ // Deprecated fields.
+ reserved 2, 3, 12, 13, 14, 15, 16, 18, 21, 24, 26, 27, 29, 32, 33, 35, 37, 38, 39, 40, 41, 42, 43, 44;
+}
+enum ResolvedUrlType {
+ RESOLVED_URL_TYPE_UNKNOWN = 0;
+
+ // The url is resolved to a web page that is not a play store app.
+ // This can be considered as the default resolved type when it's
+ // not the other specific types.
+ WEBPAGE = 1;
+
+ // The url is resolved to the Google Play store app
+ // ie. play.google.com/store
+ APP = 2;
+}
+enum DiscoveryAttachmentType {
+ DISCOVERY_ATTACHMENT_TYPE_UNKNOWN = 0;
+
+ // The attachment is posted in the prod namespace (without "-debug")
+ DISCOVERY_ATTACHMENT_TYPE_NORMAL = 1;
+
+ // The attachment is posted in the debug namespace (with "-debug")
+ DISCOVERY_ATTACHMENT_TYPE_DEBUG = 2;
+}
+// Additional information relevant only for Fast Pair devices.
+message FastPairInformation {
+ // When true, Fast Pair will only create a bond with the device and not
+ // attempt to connect any profiles (for example, A2DP or HFP).
+ bool data_only_connection = 1;
+
+ // Additional images that are attached specifically for true wireless Fast
+ // Pair devices.
+ TrueWirelessHeadsetImages true_wireless_images = 3;
+
+ // When true, this device can support assistant function.
+ bool assistant_supported = 4;
+
+ // Features supported by the Fast Pair device.
+ repeated FastPairFeature features = 5;
+
+ // Optional, the name of the company producing this Fast Pair device.
+ string company_name = 6;
+
+ // Optional, the type of device.
+ DeviceType device_type = 7;
+
+ reserved 2;
+}
+
+
+enum NearbyType {
+ NEARBY_TYPE_UNKNOWN = 0;
+ // Proximity Beacon Service (PBS). This is the only type of nearbyItems which
+ // can be customized by 3p and therefore the intents passed should not be
+ // completely trusted. Deprecated already.
+ NEARBY_PROXIMITY_BEACON = 1;
+ // Physical Web URL beacon. Deprecated already.
+ NEARBY_PHYSICAL_WEB = 2;
+ // Chromecast beacon. Used on client-side only.
+ NEARBY_CHROMECAST = 3;
+ // Wear beacon. Used on client-side only.
+ NEARBY_WEAR = 4;
+ // A device (e.g. a Magic Pair device that needs to be set up). The special-
+ // case devices above (e.g. ChromeCast, Wear) might migrate to this type.
+ NEARBY_DEVICE = 6;
+ // Popular apps/urls based on user's current geo-location.
+ NEARBY_POPULAR_HERE = 7;
+
+ reserved 5;
+}
+
+// A locally cached Fast Pair device associating an account key with the
+// bluetooth address of the device.
+message StoredFastPairItem {
+ // The device's public mac address.
+ string mac_address = 1;
+
+ // The account key written to the device.
+ bytes account_key = 2;
+
+ // When user need to update provider name, enable this value to trigger
+ // writing new name to provider.
+ bool need_to_update_provider_name = 3;
+
+ // The retry times to update name into provider.
+ int32 update_name_retries = 4;
+
+ // Latest firmware version from the server.
+ string latest_firmware_version = 5;
+
+ // The firmware version that is on the device.
+ string device_firmware_version = 6;
+
+ // The timestamp from the last time we fetched the firmware version from the
+ // device.
+ int64 last_check_firmware_timestamp_millis = 7;
+
+ // The timestamp from the last time we fetched the firmware version from
+ // server.
+ int64 last_server_query_timestamp_millis = 8;
+
+ // Only allows one bloom filter check process to create gatt connection and
+ // try to read the firmware version value.
+ bool can_read_firmware = 9;
+
+ // Device's model id.
+ string model_id = 10;
+
+ // Features that this Fast Pair device supports.
+ repeated FastPairFeature features = 11;
+
+ // Keeps the stored discovery item in local cache, we can have most
+ // information of fast pair device locally without through footprints, i.e. we
+ // can have most fast pair features locally.
+ StoredDiscoveryItem discovery_item = 12;
+
+ // When true, the latest uploaded event to FMA is connected. We use
+ // it as the previous ACL state when getting the BluetoothAdapter STATE_OFF to
+ // determine if need to upload the disconnect event to FMA.
+ bool fma_state_is_connected = 13;
+
+ // Device's buffer size range.
+ repeated BufferSizeRange buffer_size_range = 18;
+
+ // The additional account key if this device could be associated with multiple
+ // accounts. Notes that for this device, the account_key field is the basic
+ // one which will not be associated with the accounts.
+ repeated bytes additional_account_key = 19;
+
+ // Deprecated fields.
+ reserved 14, 15, 16, 17;
+}
+
+// Contains information about Fast Pair devices stored through our scanner.
+// Next ID: 29
+message ScanFastPairStoreItem {
+ // Device's model id.
+ string model_id = 1;
+
+ // Device's RSSI value
+ int32 rssi = 2;
+
+ // Device's tx power
+ int32 tx_power = 3;
+
+ // Bytes of item icon in PNG format displayed in Discovery item list.
+ bytes icon_png = 4;
+
+ // A FIFE URL of the item icon displayed in Discovery item list.
+ string icon_fife_url = 28;
+
+ // Device name like "Bose QC 35".
+ string device_name = 5;
+
+ // Client timestamp when user last saw Fast Pair device.
+ int64 last_observation_timestamp_millis = 6;
+
+ // Action url after user click the notification.
+ string action_url = 7;
+
+ // Device's bluetooth address.
+ string address = 8;
+
+ // The computed threshold rssi value that would trigger FastPair notifications
+ int32 threshold_rssi = 9;
+
+ // Populated with the contents of the bloom filter in the event that
+ // the scanned device is advertising a bloom filter instead of a model id
+ bytes bloom_filter = 10;
+
+ // Device name from the BLE scan record
+ string ble_device_name = 11;
+
+ // Strings used for the FastPair UI
+ FastPairStrings fast_pair_strings = 12;
+
+ // A key used to authenticate advertising device.
+ // See NearbyItem.authentication_public_key_secp256r1 for more information.
+ bytes anti_spoofing_public_key = 13;
+
+ // When true, Fast Pair will only create a bond with the device and not
+ // attempt to connect any profiles (for example, A2DP or HFP).
+ bool data_only_connection = 14;
+
+ // The type of the manufacturer (first party, third party, etc).
+ int32 manufacturer_type_num = 15;
+
+ // Additional images that are attached specifically for true wireless Fast
+ // Pair devices.
+ TrueWirelessHeadsetImages true_wireless_images = 16;
+
+ // When true, this device can support assistant function.
+ bool assistant_supported = 17;
+
+ // Optional, the name of the company producing this Fast Pair device.
+ string company_name = 18;
+
+ // Features supported by the Fast Pair device.
+ FastPairFeature features = 19;
+
+ // The interaction type that this scan should trigger
+ InteractionType interaction_type = 20;
+
+ // The copy of the advertisement bytes, used to pass along to other
+ // apps that use Fast Pair as the discovery vehicle.
+ bytes full_ble_record = 21;
+
+ // Companion app related information
+ CompanionAppDetails companion_detail = 22;
+
+ // Client timestamp when user first saw Fast Pair device.
+ int64 first_observation_timestamp_millis = 23;
+
+ // The type of the device (wearable, headphones, etc).
+ int32 device_type_num = 24;
+
+ // The type of notification (app launch smart setup, etc).
+ NotificationType notification_type = 25;
+
+ // The customized title.
+ string customized_title = 26;
+
+ // The customized description.
+ string customized_description = 27;
+}
diff --git a/nearby/service/proto/src/fastpair/data.proto b/nearby/service/proto/src/fastpair/data.proto
new file mode 100644
index 0000000..6f4fadd
--- /dev/null
+++ b/nearby/service/proto/src/fastpair/data.proto
@@ -0,0 +1,26 @@
+syntax = "proto3";
+
+package service.proto;
+import "src/fastpair/cache.proto";
+
+// A device that has been Fast Paired with.
+message FastPairDeviceWithAccountKey {
+ // The account key which was written to the device after pairing completed.
+ bytes account_key = 1;
+
+ // The stored discovery item which represents the notification that should be
+ // associated with the device. Note, this is stored as a raw byte array
+ // instead of StoredDiscoveryItem because icing only supports proto lite and
+ // StoredDiscoveryItem is handed around as a nano proto in implementation,
+ // which are not compatible with each other.
+ StoredDiscoveryItem discovery_item = 3;
+
+ // SHA256 of "account key + headset's public address", this is used to
+ // identify the paired headset. Because of adding account key to generate the
+ // hash value, it makes the information anonymous, even for the same headset,
+ // different accounts have different values.
+ bytes sha256_account_key_public_address = 4;
+
+ // Deprecated fields.
+ reserved 2;
+}
diff --git a/nearby/service/proto/src/fastpair/fast_pair_string.proto b/nearby/service/proto/src/fastpair/fast_pair_string.proto
new file mode 100644
index 0000000..f318c1a
--- /dev/null
+++ b/nearby/service/proto/src/fastpair/fast_pair_string.proto
@@ -0,0 +1,40 @@
+syntax = "proto2";
+
+package service.proto;
+
+message FastPairStrings {
+ // Required for initial pairing, used when there is a Google account on the
+ // device
+ optional string tap_to_pair_with_account = 1;
+
+ // Required for initial pairing, used when there is no Google account on the
+ // device
+ optional string tap_to_pair_without_account = 2;
+
+ // Description for initial pairing
+ optional string initial_pairing_description = 3;
+
+ // Description after successfully paired the device with companion app
+ // installed
+ optional string pairing_finished_companion_app_installed = 4;
+
+ // Description after successfully paired the device with companion app not
+ // installed
+ optional string pairing_finished_companion_app_not_installed = 5;
+
+ // Description when phone found the device that associates with user's account
+ // before remind user to pair with new device.
+ optional string subsequent_pairing_description = 6;
+
+ // Description when fast pair finds the user paired with device manually
+ // reminds user to opt the device into cloud.
+ optional string retroactive_pairing_description = 7;
+
+ // Description when user click setup device while device is still pairing
+ optional string wait_app_launch_description = 8;
+
+ // Description when user fail to pair with device
+ optional string pairing_fail_description = 9;
+
+ reserved 10, 11, 12, 13, 14,15, 16, 17, 18;
+}
diff --git a/nearby/service/proto/src/fastpair/rpcs.proto b/nearby/service/proto/src/fastpair/rpcs.proto
new file mode 100644
index 0000000..bce4378
--- /dev/null
+++ b/nearby/service/proto/src/fastpair/rpcs.proto
@@ -0,0 +1,301 @@
+// RPCs for the Nearby Console service.
+syntax = "proto3";
+
+package service.proto;
+// Response containing an observed device.
+message GetObservedDeviceResponse {
+ // The device from the request.
+ Device device = 1;
+
+ // The image icon that shows in the notification
+ bytes image = 3;
+
+ // Strings to be displayed on notifications during the pairing process.
+ ObservedDeviceStrings strings = 4;
+
+ reserved 2;
+}
+
+message Device {
+ // Output only. The server-generated ID of the device.
+ int64 id = 1;
+
+ // The pantheon project number the device is created under. Only Nearby admins
+ // can change this.
+ int64 project_number = 2;
+
+ // How the notification will be displayed to the user
+ NotificationType notification_type = 3;
+
+ // The image to show on the notification.
+ string image_url = 4;
+
+ // The name of the device.
+ string name = 5;
+
+ // The intent that will be launched via the notification.
+ string intent_uri = 6;
+
+ // The transmit power of the device's BLE chip.
+ int32 ble_tx_power = 7;
+
+ // The distance that the device must be within to show a notification.
+ // If no distance is set, we default to 0.6 meters. Only Nearby admins can
+ // change this.
+ float trigger_distance = 8;
+
+ // Output only. Fast Pair only - The anti-spoofing key pair for the device.
+ AntiSpoofingKeyPair anti_spoofing_key_pair = 9;
+
+ // Output only. The current status of the device.
+ Status status = 10;
+
+
+ // DEPRECATED - check for published_version instead.
+ // Output only.
+ // Whether the device has a different, already published version.
+ bool has_published_version = 12;
+
+ // Fast Pair only - The type of device being registered.
+ DeviceType device_type = 13;
+
+
+ // Fast Pair only - Additional images for true wireless headsets.
+ TrueWirelessHeadsetImages true_wireless_images = 15;
+
+ // Fast Pair only - When true, this device can support assistant function.
+ bool assistant_supported = 16;
+
+ // Output only.
+ // The published version of a device that has been approved to be displayed
+ // as a notification - only populated if the device has a different published
+ // version. (A device that only has a published version would not have this
+ // populated).
+ Device published_version = 17;
+
+ // Fast Pair only - When true, Fast Pair will only create a bond with the
+ // device and not attempt to connect any profiles (for example, A2DP or HFP).
+ bool data_only_connection = 18;
+
+ // Name of the company/brand that will be selling the product.
+ string company_name = 19;
+
+ repeated FastPairFeature features = 20;
+
+ // Name of the device that is displayed on the console.
+ string display_name = 21;
+
+ // How the device will be interacted with by the user when the scan record
+ // is detected.
+ InteractionType interaction_type = 22;
+
+ // Companion app information.
+ CompanionAppDetails companion_detail = 23;
+
+ reserved 11, 14;
+}
+
+
+// Represents the format of the final device notification (which is directly
+// correlated to the action taken by the notification).
+enum NotificationType {
+ // Unspecified notification type.
+ NOTIFICATION_TYPE_UNSPECIFIED = 0;
+ // Notification launches the fast pair intent.
+ // Example Notification Title: "Bose SoundLink II"
+ // Notification Description: "Tap to pair with this device"
+ FAST_PAIR = 1;
+ // Notification launches an app.
+ // Notification Title: "[X]" where X is type/name of the device.
+ // Notification Description: "Tap to setup this device"
+ APP_LAUNCH = 2;
+ // Notification launches for Nearby Setup. The notification title and
+ // description is the same as APP_LAUNCH.
+ NEARBY_SETUP = 3;
+ // Notification launches the fast pair intent, but doesn't include an anti-
+ // spoofing key. The notification title and description is the same as
+ // FAST_PAIR.
+ FAST_PAIR_ONE = 4;
+ // Notification launches Smart Setup on devices.
+ // These notifications are identical to APP_LAUNCH except that they always
+ // launch Smart Setup intents within GMSCore.
+ SMART_SETUP = 5;
+}
+
+// How the device will be interacted with when it is seen.
+enum InteractionType {
+ INTERACTION_TYPE_UNKNOWN = 0;
+ AUTO_LAUNCH = 1;
+ NOTIFICATION = 2;
+}
+
+// Features that can be enabled for a Fast Pair device.
+enum FastPairFeature {
+ FAST_PAIR_FEATURE_UNKNOWN = 0;
+ SILENCE_MODE = 1;
+ WIRELESS_CHARGING = 2;
+ DYNAMIC_BUFFER_SIZE = 3;
+ NO_PERSONALIZED_NAME = 4;
+ EDDYSTONE_TRACKING = 5;
+}
+
+message CompanionAppDetails {
+ // Companion app slice provider's authority.
+ string authority = 1;
+
+ // Companion app certificate value.
+ string certificate_hash = 2;
+
+ // Deprecated fields.
+ reserved 3;
+}
+
+// Additional images for True Wireless Fast Pair devices.
+message TrueWirelessHeadsetImages {
+ // Image URL for the left bud.
+ string left_bud_url = 1;
+
+ // Image URL for the right bud.
+ string right_bud_url = 2;
+
+ // Image URL for the case.
+ string case_url = 3;
+}
+
+// Represents the type of device that is being registered.
+enum DeviceType {
+ DEVICE_TYPE_UNSPECIFIED = 0;
+ HEADPHONES = 1;
+ SPEAKER = 2;
+ WEARABLE = 3;
+ INPUT_DEVICE = 4;
+ AUTOMOTIVE = 5;
+ OTHER = 6;
+ TRUE_WIRELESS_HEADPHONES = 7;
+ WEAR_OS = 8;
+ ANDROID_AUTO = 9;
+}
+
+// An anti-spoofing key pair for a device that allows us to verify the device is
+// broadcasting legitimately.
+message AntiSpoofingKeyPair {
+ // The private key (restricted to only be viewable by trusted clients).
+ bytes private_key = 1;
+
+ // The public key.
+ bytes public_key = 2;
+}
+
+// Various states that a customer-configured device notification can be in.
+// PUBLISHED is the only state that shows notifications to the public.
+message Status {
+ // Status types available for each device.
+ enum StatusType {
+ // Unknown status.
+ TYPE_UNSPECIFIED = 0;
+ // Drafted device.
+ DRAFT = 1;
+ // Submitted and waiting for approval.
+ SUBMITTED = 2;
+ // Fully approved and available for end users.
+ PUBLISHED = 3;
+ // Rejected and not available for end users.
+ REJECTED = 4;
+ }
+
+ // Details about a device that has been rejected.
+ message RejectionDetails {
+ // The reason for the rejection.
+ enum RejectionReason {
+ // Unspecified reason.
+ REASON_UNSPECIFIED = 0;
+ // Name is not valid.
+ NAME = 1;
+ // Image is not valid.
+ IMAGE = 2;
+ // Tests have failed.
+ TESTS = 3;
+ // Other reason.
+ OTHER = 4;
+ }
+
+ // A list of reasons the device was rejected.
+ repeated RejectionReason reasons = 1;
+ // Comment about an OTHER rejection reason.
+ string additional_comment = 2;
+ }
+
+ // The status of the device.
+ StatusType status_type = 1;
+
+ // Accompanies Status.REJECTED.
+ RejectionDetails rejection_details = 2;
+}
+
+// Strings to be displayed in notifications surfaced for a device.
+message ObservedDeviceStrings {
+ // The notification description for when the device is initially discovered.
+ string initial_notification_description = 2;
+
+ // The notification description for when the device is initially discovered
+ // and no account is logged in.
+ string initial_notification_description_no_account = 3;
+
+ // The notification description for once we have finished pairing and the
+ // companion app has been opened. For google assistant devices, this string will point
+ // users to setting up the assistant.
+ string open_companion_app_description = 4;
+
+ // The notification description for once we have finished pairing and the
+ // companion app needs to be updated before use.
+ string update_companion_app_description = 5;
+
+ // The notification description for once we have finished pairing and the
+ // companion app needs to be installed.
+ string download_companion_app_description = 6;
+
+ // The notification title when a pairing fails.
+ string unable_to_connect_title = 7;
+
+ // The notification summary when a pairing fails.
+ string unable_to_connect_description = 8;
+
+ // The description that helps user initially paired with device.
+ string initial_pairing_description = 9;
+
+ // The description that let user open the companion app.
+ string connect_success_companion_app_installed = 10;
+
+ // The description that let user download the companion app.
+ string connect_success_companion_app_not_installed = 11;
+
+ // The description that reminds user there is a paired device nearby.
+ string subsequent_pairing_description = 12;
+
+ // The description that reminds users opt in their device.
+ string retroactive_pairing_description = 13;
+
+ // The description that indicates companion app is about to launch.
+ string wait_launch_companion_app_description = 14;
+
+ // The description that indicates go to bluetooth settings when connection
+ // fail.
+ string fail_connect_go_to_settings_description = 15;
+
+ reserved 1, 16, 17, 18, 19, 20, 21, 22, 23, 24;
+}
+
+// The buffer size range of a Fast Pair devices support dynamic buffer size.
+message BufferSizeRange {
+ // The max buffer size in ms.
+ int32 max_size = 1;
+
+ // The min buffer size in ms.
+ int32 min_size = 2;
+
+ // The default buffer size in ms.
+ int32 default_size = 3;
+
+ // The codec of this buffer size range.
+ int32 codec = 4;
+}
diff --git a/nearby/service/proto/src/presence/blefilter.proto b/nearby/service/proto/src/presence/blefilter.proto
new file mode 100644
index 0000000..9f75d34
--- /dev/null
+++ b/nearby/service/proto/src/presence/blefilter.proto
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+
+// Proto Messages define the interface between Nearby nanoapp and its host.
+//
+// Host registers its interest in BLE event by configuring nanoapp with Filters.
+// The nanoapp keeps watching BLE events and notifies host once an event matches
+// a Filter.
+//
+// Each Filter is defined by its id (required) with optional fields of rssi,
+// uuid, MAC etc. The host should guarantee the uniqueness of ids. It is
+// convenient to assign id incrementally when adding a Filter such that its id
+// is the same as the index of the repeated field in Filters.
+//
+// The nanoapp compares each BLE event against the list of Filters, and notifies
+// host when the event matches a Filter. The Field's id will be sent back to
+// host in the FilterResult.
+//
+// It is possible for the nanoapp to return multiple ids when an event matches
+// multiple Filters.
+
+syntax = "proto2";
+
+package service.proto;
+
+// Certificate to verify BLE events from trusted devices.
+// When receiving an advertisement from a remote device, it will
+// be decrypted by authenticity_key and SHA hashed. The device
+// is verified as trusted if the hash result is equal to
+// metadata_encryption_key_tag.
+// See details in go/ns-certificates.
+message PublicateCertificate {
+ optional bytes authenticity_key = 1;
+ optional bytes metadata_encryption_key_tag = 2;
+}
+
+message PublicCredential {
+ optional bytes secret_id = 1;
+ optional bytes authenticity_key = 2;
+ optional bytes public_key = 3;
+ optional bytes encrypted_metadata = 4;
+ optional bytes encrypted_metadata_tag = 5;
+}
+
+message BleFilter {
+ optional uint32 id = 1; // Required, unique id of this filter.
+ // Maximum delay to notify the client after an event occurs.
+ optional uint32 latency_ms = 2;
+ optional uint32 uuid = 3;
+ // MAC address of the advertising device.
+ optional bytes mac_address = 4;
+ optional bytes mac_mask = 5;
+ // Represents an action that scanners should take when they receive this
+ // packet. See go/nearby-presence-spec for details.
+ optional uint32 intent = 6;
+ // Notify the client if the advertising device is within the distance.
+ // For moving object, the distance is averaged over data sampled within
+ // the period of latency defined above.
+ optional float distance_m = 7;
+ // Used to verify the list of trusted devices.
+ repeated PublicateCertificate certficate = 8;
+}
+
+message BleFilters {
+ repeated BleFilter filter = 1;
+}
+
+// FilterResult is returned to host when a BLE event matches a Filter.
+message BleFilterResult {
+ optional uint32 id = 1; // id of the matched Filter.
+ optional uint32 tx_power = 2;
+ optional uint32 rssi = 3;
+ optional uint32 intent = 4;
+ optional bytes bluetooth_address = 5;
+ optional PublicCredential public_credential = 6;
+}
+
+message BleFilterResults {
+ repeated BleFilterResult result = 1;
+}
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
new file mode 100644
index 0000000..845ed84
--- /dev/null
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -0,0 +1,47 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "CtsNearbyFastPairTestCases",
+ defaults: ["cts_defaults"],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "androidx.test.ext.truth",
+ "androidx.test.rules",
+ "bluetooth-test-util-lib",
+ "compatibility-device-util-axt",
+ "ctstestrunner-axt",
+ "truth-prebuilt",
+ ],
+ libs: [
+ "android.test.base",
+ "framework-bluetooth.stubs.module_lib",
+ "framework-connectivity-t.impl",
+ ],
+ srcs: ["src/**/*.java"],
+ test_suites: [
+ "cts",
+ "general-tests",
+ "mts-tethering",
+ ],
+ certificate: "platform",
+ platform_apis: true,
+ sdk_version: "module_current",
+ min_sdk_version: "30",
+ target_sdk_version: "32",
+}
diff --git a/nearby/tests/cts/fastpair/AndroidManifest.xml b/nearby/tests/cts/fastpair/AndroidManifest.xml
new file mode 100644
index 0000000..ce841f2
--- /dev/null
+++ b/nearby/tests/cts/fastpair/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.nearby.cts">
+ <uses-sdk android:minSdkVersion="32" android:targetSdkVersion="32" />
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
+ <application>
+ <uses-library android:name="android.test.runner"/>
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.nearby.cts"
+ android:label="CTS tests for android.nearby Fast Pair">
+ <meta-data android:name="listener"
+ android:value="com.android.cts.runner.CtsTestRunListener"/>
+ </instrumentation>
+</manifest>
diff --git a/nearby/tests/cts/fastpair/AndroidTest.xml b/nearby/tests/cts/fastpair/AndroidTest.xml
new file mode 100644
index 0000000..360bbf3
--- /dev/null
+++ b/nearby/tests/cts/fastpair/AndroidTest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Config for CTS Nearby Fast Pair test cases">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="location" />
+ <!-- Instant cannot access NearbyManager. -->
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <option name="config-descriptor:metadata" key="parameter" value="all_foldable_states" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="CtsNearbyFastPairTestCases.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.nearby.cts" />
+ </test>
+ <!-- Only run NearbyUnitTests in MTS if the Nearby Mainline module is installed. -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+</configuration>
diff --git a/nearby/tests/cts/fastpair/OWNERS b/nearby/tests/cts/fastpair/OWNERS
new file mode 100644
index 0000000..1756bba
--- /dev/null
+++ b/nearby/tests/cts/fastpair/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 1092133
+
+chunzhang@google.com
+weiwa@google.com
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
new file mode 100644
index 0000000..aacb6d8
--- /dev/null
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.CredentialElement;
+import android.os.Build;
+import android.os.Parcel;
+
+import androidx.annotation.RequiresApi;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public class CredentialElementTest {
+ private static final String KEY = "SECRETE_ID";
+ private static final byte[] VALUE = new byte[]{1, 2, 3, 4};
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBuilder() {
+ CredentialElement element = new CredentialElement(KEY, VALUE);
+
+ assertThat(element.getKey()).isEqualTo(KEY);
+ assertThat(Arrays.equals(element.getValue(), VALUE)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWriteParcel() {
+ CredentialElement element = new CredentialElement(KEY, VALUE);
+
+ Parcel parcel = Parcel.obtain();
+ element.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ CredentialElement elementFromParcel = element.CREATOR.createFromParcel(
+ parcel);
+ parcel.recycle();
+
+ assertThat(elementFromParcel.getKey()).isEqualTo(KEY);
+ assertThat(Arrays.equals(elementFromParcel.getValue(), VALUE)).isTrue();
+ }
+
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
new file mode 100644
index 0000000..ec6e89a
--- /dev/null
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.DataElement;
+import android.os.Build;
+import android.os.Parcel;
+
+import androidx.annotation.RequiresApi;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+
+@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public class DataElementTest {
+
+ private static final int KEY = 1234;
+ private static final byte[] VALUE = new byte[]{1, 1, 1, 1};
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBuilder() {
+ DataElement dataElement = new DataElement(KEY, VALUE);
+
+ assertThat(dataElement.getKey()).isEqualTo(KEY);
+ assertThat(Arrays.equals(dataElement.getValue(), VALUE)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWriteParcel() {
+ DataElement dataElement = new DataElement(KEY, VALUE);
+
+ Parcel parcel = Parcel.obtain();
+ dataElement.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ DataElement elementFromParcel = DataElement.CREATOR.createFromParcel(
+ parcel);
+ parcel.recycle();
+
+ assertThat(elementFromParcel.getKey()).isEqualTo(KEY);
+ assertThat(Arrays.equals(elementFromParcel.getValue(), VALUE)).isTrue();
+ }
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
new file mode 100644
index 0000000..b9ab95f
--- /dev/null
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.NearbyDevice;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PublicCredential;
+import android.os.Build;
+import android.os.Parcel;
+
+import androidx.annotation.RequiresApi;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public class NearbyDeviceParcelableTest {
+
+ private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
+ private static final byte[] SCAN_DATA = new byte[] {1, 2, 3, 4};
+ private static final String FAST_PAIR_MODEL_ID = "1234";
+ private static final int RSSI = -60;
+
+ private NearbyDeviceParcelable.Builder mBuilder;
+
+ @Before
+ public void setUp() {
+ mBuilder =
+ new NearbyDeviceParcelable.Builder()
+ .setName("testDevice")
+ .setMedium(NearbyDevice.Medium.BLE)
+ .setRssi(RSSI)
+ .setFastPairModelId(FAST_PAIR_MODEL_ID)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setData(SCAN_DATA);
+ }
+
+ /** Verify toString returns expected string. */
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testToString() {
+ PublicCredential publicCredential =
+ new PublicCredential.Builder(
+ new byte[] {1},
+ new byte[] {2},
+ new byte[] {3},
+ new byte[] {4},
+ new byte[] {5})
+ .build();
+ NearbyDeviceParcelable nearbyDeviceParcelable =
+ mBuilder.setFastPairModelId(null)
+ .setData(null)
+ .setPublicCredential(publicCredential)
+ .build();
+
+ assertThat(nearbyDeviceParcelable.toString())
+ .isEqualTo(
+ "NearbyDeviceParcelable[name=testDevice, medium=BLE, txPower=0, rssi=-60,"
+ + " action=0, bluetoothAddress="
+ + BLUETOOTH_ADDRESS
+ + ", fastPairModelId=null, data=null]");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void test_defaultNullFields() {
+ NearbyDeviceParcelable nearbyDeviceParcelable =
+ new NearbyDeviceParcelable.Builder()
+ .setMedium(NearbyDevice.Medium.BLE)
+ .setRssi(RSSI)
+ .build();
+
+ assertThat(nearbyDeviceParcelable.getName()).isNull();
+ assertThat(nearbyDeviceParcelable.getFastPairModelId()).isNull();
+ assertThat(nearbyDeviceParcelable.getBluetoothAddress()).isNull();
+ assertThat(nearbyDeviceParcelable.getData()).isNull();
+
+ assertThat(nearbyDeviceParcelable.getMedium()).isEqualTo(NearbyDevice.Medium.BLE);
+ assertThat(nearbyDeviceParcelable.getRssi()).isEqualTo(RSSI);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testWriteParcel() {
+ NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.build();
+
+ Parcel parcel = Parcel.obtain();
+ nearbyDeviceParcelable.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ NearbyDeviceParcelable actualNearbyDevice =
+ NearbyDeviceParcelable.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+
+ assertThat(actualNearbyDevice.getRssi()).isEqualTo(RSSI);
+ assertThat(actualNearbyDevice.getFastPairModelId()).isEqualTo(FAST_PAIR_MODEL_ID);
+ assertThat(actualNearbyDevice.getBluetoothAddress()).isEqualTo(BLUETOOTH_ADDRESS);
+ assertThat(Arrays.equals(actualNearbyDevice.getData(), SCAN_DATA)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testWriteParcel_nullModelId() {
+ NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setFastPairModelId(null).build();
+
+ Parcel parcel = Parcel.obtain();
+ nearbyDeviceParcelable.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ NearbyDeviceParcelable actualNearbyDevice =
+ NearbyDeviceParcelable.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+
+ assertThat(actualNearbyDevice.getFastPairModelId()).isNull();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testWriteParcel_nullBluetoothAddress() {
+ NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setBluetoothAddress(null).build();
+
+ Parcel parcel = Parcel.obtain();
+ nearbyDeviceParcelable.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ NearbyDeviceParcelable actualNearbyDevice =
+ NearbyDeviceParcelable.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+
+ assertThat(actualNearbyDevice.getBluetoothAddress()).isNull();
+ }
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
new file mode 100644
index 0000000..f37800a
--- /dev/null
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.cts;
+
+import android.annotation.TargetApi;
+import android.nearby.FastPairDevice;
+import android.nearby.NearbyDevice;
+import android.os.Build;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.annotation.RequiresApi;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
+public class NearbyDeviceTest {
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_isValidMedium() {
+ assertThat(NearbyDevice.isValidMedium(1)).isTrue();
+ assertThat(NearbyDevice.isValidMedium(2)).isTrue();
+
+ assertThat(NearbyDevice.isValidMedium(0)).isFalse();
+ assertThat(NearbyDevice.isValidMedium(3)).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getMedium_fromChild() {
+ FastPairDevice fastPairDevice = new FastPairDevice.Builder()
+ .addMedium(NearbyDevice.Medium.BLE)
+ .setRssi(-60)
+ .build();
+
+ assertThat(fastPairDevice.getMediums()).contains(1);
+ assertThat(fastPairDevice.getRssi()).isEqualTo(-60);
+ }
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyFrameworkInitializerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyFrameworkInitializerTest.java
new file mode 100644
index 0000000..cf43cb1
--- /dev/null
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyFrameworkInitializerTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package 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;
+
+// NearbyFrameworkInitializer was added in T
+@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public class NearbyFrameworkInitializerTest {
+
+ @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)
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testThrowsException() {
+ NearbyFrameworkInitializer.registerServiceWrappers();
+ }
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
new file mode 100644
index 0000000..9720865
--- /dev/null
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.cts;
+
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.UiAutomation;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.cts.BTAdapterUtils;
+import android.content.Context;
+import android.nearby.BroadcastCallback;
+import android.nearby.BroadcastRequest;
+import android.nearby.NearbyDevice;
+import android.nearby.NearbyManager;
+import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PrivateCredential;
+import android.nearby.ScanCallback;
+import android.nearby.ScanRequest;
+import android.os.Build;
+import android.provider.DeviceConfig;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * TODO(b/215435939) This class doesn't include any logic yet. Because SELinux denies access to
+ * NearbyManager.
+ */
+@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public class NearbyManagerTest {
+ private static final byte[] SALT = new byte[]{1, 2};
+ private static final byte[] SECRETE_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] META_DATA_ENCRYPTION_KEY = new byte[14];
+ private static final byte[] AUTHENTICITY_KEY = new byte[]{0, 1, 1, 1};
+ private static final String DEVICE_NAME = "test_device";
+ private static final int BLE_MEDIUM = 1;
+
+ private Context mContext;
+ private NearbyManager mNearbyManager;
+ private UiAutomation mUiAutomation =
+ InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+ @Before
+ public void setUp() {
+ mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG);
+ DeviceConfig.setProperty(NAMESPACE_TETHERING, "nearby_enable_presence_broadcast_legacy",
+ "true", false);
+
+ mContext = InstrumentationRegistry.getContext();
+ mNearbyManager = mContext.getSystemService(NearbyManager.class);
+
+ enableBluetooth();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_startAndStopScan() {
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR)
+ .setScanMode(ScanRequest.SCAN_MODE_LOW_LATENCY)
+ .setBleEnabled(true)
+ .build();
+ ScanCallback scanCallback = new ScanCallback() {
+ @Override
+ public void onDiscovered(@NonNull NearbyDevice device) {
+ }
+
+ @Override
+ public void onUpdated(@NonNull NearbyDevice device) {
+
+ }
+
+ @Override
+ public void onLost(@NonNull NearbyDevice device) {
+
+ }
+ };
+ mNearbyManager.startScan(scanRequest, Executors.newSingleThreadExecutor(), scanCallback);
+ mNearbyManager.stopScan(scanCallback);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testStartStopBroadcast() throws InterruptedException {
+ PrivateCredential credential = new PrivateCredential.Builder(SECRETE_ID, AUTHENTICITY_KEY,
+ META_DATA_ENCRYPTION_KEY, DEVICE_NAME)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+ BroadcastRequest broadcastRequest =
+ new PresenceBroadcastRequest.Builder(
+ Collections.singletonList(BLE_MEDIUM), SALT, credential)
+ .addAction(123)
+ .build();
+
+ CountDownLatch latch = new CountDownLatch(1);
+ BroadcastCallback callback = status -> {
+ latch.countDown();
+ assertThat(status).isEqualTo(BroadcastCallback.STATUS_OK);
+ };
+ mNearbyManager.startBroadcast(broadcastRequest, Executors.newSingleThreadExecutor(),
+ callback);
+ latch.await(10, TimeUnit.SECONDS);
+ mNearbyManager.stopBroadcast(callback);
+ }
+
+ private void enableBluetooth() {
+ BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
+ BluetoothAdapter bluetoothAdapter = manager.getAdapter();
+ if (!bluetoothAdapter.isEnabled()) {
+ assertThat(BTAdapterUtils.enableAdapter(bluetoothAdapter, mContext)).isTrue();
+ }
+ }
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
new file mode 100644
index 0000000..1daa410
--- /dev/null
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.cts;
+
+import static android.nearby.BroadcastRequest.BROADCAST_TYPE_NEARBY_PRESENCE;
+import static android.nearby.BroadcastRequest.PRESENCE_VERSION_V0;
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.DataElement;
+import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PrivateCredential;
+import android.os.Build;
+import android.os.Parcel;
+
+import androidx.annotation.RequiresApi;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Tests for {@link PresenceBroadcastRequest}.
+ */
+@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public class PresenceBroadcastRequestTest {
+
+ private static final int VERSION = PRESENCE_VERSION_V0;
+ private static final int TX_POWER = 1;
+ private static final byte[] SALT = new byte[]{1, 2};
+ private static final int ACTION_ID = 123;
+ private static final int BLE_MEDIUM = 1;
+ private static final byte[] SECRETE_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] AUTHENTICITY_KEY = new byte[]{0, 1, 1, 1};
+ private static final byte[] METADATA_ENCRYPTION_KEY = new byte[]{1, 1, 3, 4, 5};
+ private static final int KEY = 1234;
+ private static final byte[] VALUE = new byte[]{1, 1, 1, 1};
+ private static final String DEVICE_NAME = "test_device";
+
+ private PresenceBroadcastRequest.Builder mBuilder;
+
+ @Before
+ public void setUp() {
+ PrivateCredential credential = new PrivateCredential.Builder(SECRETE_ID, AUTHENTICITY_KEY,
+ METADATA_ENCRYPTION_KEY, DEVICE_NAME)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+ DataElement element = new DataElement(KEY, VALUE);
+ mBuilder = new PresenceBroadcastRequest.Builder(Collections.singletonList(BLE_MEDIUM), SALT,
+ credential)
+ .setTxPower(TX_POWER)
+ .setVersion(VERSION)
+ .addAction(ACTION_ID)
+ .addExtendedProperty(element);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBuilder() {
+ PresenceBroadcastRequest broadcastRequest = mBuilder.build();
+
+ assertThat(broadcastRequest.getVersion()).isEqualTo(VERSION);
+ assertThat(Arrays.equals(broadcastRequest.getSalt(), SALT)).isTrue();
+ assertThat(broadcastRequest.getTxPower()).isEqualTo(TX_POWER);
+ assertThat(broadcastRequest.getActions()).containsExactly(ACTION_ID);
+ assertThat(broadcastRequest.getExtendedProperties().get(0).getKey()).isEqualTo(
+ KEY);
+ assertThat(broadcastRequest.getMediums()).containsExactly(BLE_MEDIUM);
+ assertThat(broadcastRequest.getCredential().getIdentityType()).isEqualTo(
+ IDENTITY_TYPE_PRIVATE);
+ assertThat(broadcastRequest.getType()).isEqualTo(BROADCAST_TYPE_NEARBY_PRESENCE);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWriteParcel() {
+ PresenceBroadcastRequest broadcastRequest = mBuilder.build();
+
+ Parcel parcel = Parcel.obtain();
+ broadcastRequest.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ PresenceBroadcastRequest parcelRequest = PresenceBroadcastRequest.CREATOR.createFromParcel(
+ parcel);
+ parcel.recycle();
+
+ assertThat(parcelRequest.getTxPower()).isEqualTo(TX_POWER);
+ assertThat(parcelRequest.getActions()).containsExactly(ACTION_ID);
+ assertThat(parcelRequest.getExtendedProperties().get(0).getKey()).isEqualTo(
+ KEY);
+ assertThat(parcelRequest.getMediums()).containsExactly(BLE_MEDIUM);
+ assertThat(parcelRequest.getCredential().getIdentityType()).isEqualTo(
+ IDENTITY_TYPE_PRIVATE);
+ assertThat(parcelRequest.getType()).isEqualTo(BROADCAST_TYPE_NEARBY_PRESENCE);
+
+ }
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
new file mode 100644
index 0000000..5fefc68
--- /dev/null
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.DataElement;
+import android.nearby.NearbyDevice;
+import android.nearby.PresenceDevice;
+import android.os.Build;
+import android.os.Parcel;
+
+import androidx.annotation.RequiresApi;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+/**
+ * Test for {@link PresenceDevice}.
+ */
+@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public class PresenceDeviceTest {
+ private static final int DEVICE_TYPE = PresenceDevice.DeviceType.PHONE;
+ private static final String DEVICE_ID = "123";
+ private static final String IMAGE_URL = "http://example.com/imageUrl";
+ private static final int RSSI = -40;
+ private static final int MEDIUM = NearbyDevice.Medium.BLE;
+ private static final String DEVICE_NAME = "testDevice";
+ private static final int KEY = 1234;
+ private static final byte[] VALUE = new byte[]{1, 1, 1, 1};
+ private static final byte[] SALT = new byte[]{2, 3};
+ private static final byte[] SECRET_ID = new byte[]{11, 13};
+ private static final byte[] ENCRYPTED_IDENTITY = new byte[]{1, 3, 5, 61};
+ private static final long DISCOVERY_MILLIS = 100L;
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBuilder() {
+ PresenceDevice device =
+ new PresenceDevice.Builder(DEVICE_ID, SALT, SECRET_ID, ENCRYPTED_IDENTITY)
+ .setDeviceType(DEVICE_TYPE)
+ .setDeviceImageUrl(IMAGE_URL)
+ .addExtendedProperty(new DataElement(KEY, VALUE))
+ .setRssi(RSSI)
+ .addMedium(MEDIUM)
+ .setName(DEVICE_NAME)
+ .setDiscoveryTimestampMillis(DISCOVERY_MILLIS)
+ .build();
+
+ assertThat(device.getDeviceType()).isEqualTo(DEVICE_TYPE);
+ assertThat(device.getDeviceId()).isEqualTo(DEVICE_ID);
+ assertThat(device.getDeviceImageUrl()).isEqualTo(IMAGE_URL);
+ DataElement dataElement = device.getExtendedProperties().get(0);
+ assertThat(dataElement.getKey()).isEqualTo(KEY);
+ assertThat(Arrays.equals(dataElement.getValue(), VALUE)).isTrue();
+ assertThat(device.getRssi()).isEqualTo(RSSI);
+ assertThat(device.getMediums()).containsExactly(MEDIUM);
+ assertThat(device.getName()).isEqualTo(DEVICE_NAME);
+ assertThat(Arrays.equals(device.getSalt(), SALT)).isTrue();
+ assertThat(Arrays.equals(device.getSecretId(), SECRET_ID)).isTrue();
+ assertThat(Arrays.equals(device.getEncryptedIdentity(), ENCRYPTED_IDENTITY)).isTrue();
+ assertThat(device.getDiscoveryTimestampMillis()).isEqualTo(DISCOVERY_MILLIS);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWriteParcel() {
+ PresenceDevice device =
+ new PresenceDevice.Builder(DEVICE_ID, SALT, SECRET_ID, ENCRYPTED_IDENTITY)
+ .addExtendedProperty(new DataElement(KEY, VALUE))
+ .setRssi(RSSI)
+ .addMedium(MEDIUM)
+ .setName(DEVICE_NAME)
+ .build();
+
+ Parcel parcel = Parcel.obtain();
+ device.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ PresenceDevice parcelDevice = PresenceDevice.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+
+ assertThat(parcelDevice.getDeviceId()).isEqualTo(DEVICE_ID);
+ assertThat(parcelDevice.getExtendedProperties().get(0).getKey()).isEqualTo(KEY);
+ assertThat(parcelDevice.getRssi()).isEqualTo(RSSI);
+ assertThat(parcelDevice.getMediums()).containsExactly(MEDIUM);
+ assertThat(parcelDevice.getName()).isEqualTo(DEVICE_NAME);
+ }
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
new file mode 100644
index 0000000..b7fe40a
--- /dev/null
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.cts;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.DataElement;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanRequest;
+import android.os.Build;
+import android.os.Parcel;
+
+import androidx.annotation.RequiresApi;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link android.nearby.PresenceScanFilter}.
+ */
+@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public class PresenceScanFilterTest {
+
+ private static final int RSSI = -40;
+ private static final int ACTION = 123;
+ private static final byte[] SECRETE_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] AUTHENTICITY_KEY = new byte[]{0, 1, 1, 1};
+ private static final byte[] PUBLIC_KEY = new byte[]{1, 1, 2, 2};
+ private static final byte[] ENCRYPTED_METADATA = new byte[]{1, 2, 3, 4, 5};
+ private static final byte[] METADATA_ENCRYPTION_KEY_TAG = new byte[]{1, 1, 3, 4, 5};
+ private static final int KEY = 1234;
+ private static final byte[] VALUE = new byte[]{1, 1, 1, 1};
+
+
+ private PublicCredential mPublicCredential =
+ new PublicCredential.Builder(SECRETE_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
+ ENCRYPTED_METADATA, METADATA_ENCRYPTION_KEY_TAG)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+ private PresenceScanFilter.Builder mBuilder = new PresenceScanFilter.Builder()
+ .setMaxPathLoss(RSSI)
+ .addCredential(mPublicCredential)
+ .addPresenceAction(ACTION)
+ .addExtendedProperty(new DataElement(KEY, VALUE));
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBuilder() {
+ PresenceScanFilter filter = mBuilder.build();
+
+ assertThat(filter.getMaxPathLoss()).isEqualTo(RSSI);
+ assertThat(filter.getCredentials().get(0).getIdentityType()).isEqualTo(
+ IDENTITY_TYPE_PRIVATE);
+ assertThat(filter.getPresenceActions()).containsExactly(ACTION);
+ assertThat(filter.getExtendedProperties().get(0).getKey()).isEqualTo(KEY);
+
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWriteParcel() {
+ PresenceScanFilter filter = mBuilder.build();
+
+ Parcel parcel = Parcel.obtain();
+ filter.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ PresenceScanFilter parcelFilter = PresenceScanFilter.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+
+ assertThat(parcelFilter.getType()).isEqualTo(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE);
+ assertThat(parcelFilter.getMaxPathLoss()).isEqualTo(RSSI);
+ assertThat(parcelFilter.getPresenceActions()).containsExactly(ACTION);
+ }
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
new file mode 100644
index 0000000..f05f65f
--- /dev/null
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.cts;
+
+import static android.nearby.PresenceCredential.CREDENTIAL_TYPE_PRIVATE;
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.CredentialElement;
+import android.nearby.PrivateCredential;
+import android.os.Build;
+import android.os.Parcel;
+
+import androidx.annotation.RequiresApi;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+/**
+ * Tests for {@link PrivateCredential}.
+ */
+@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public class PrivateCredentialTest {
+ private static final String DEVICE_NAME = "myDevice";
+ private static final byte[] SECRETE_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] AUTHENTICITY_KEY = new byte[]{0, 1, 1, 1};
+ private static final String KEY = "SecreteId";
+ private static final byte[] VALUE = new byte[]{1, 2, 3, 4, 5};
+ private static final byte[] METADATA_ENCRYPTION_KEY = new byte[]{1, 1, 3, 4, 5};
+
+ private PrivateCredential.Builder mBuilder;
+
+ @Before
+ public void setUp() {
+ mBuilder = new PrivateCredential.Builder(
+ SECRETE_ID, AUTHENTICITY_KEY, METADATA_ENCRYPTION_KEY, DEVICE_NAME)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .addCredentialElement(new CredentialElement(KEY, VALUE));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testBuilder() {
+ PrivateCredential credential = mBuilder.build();
+
+ assertThat(credential.getType()).isEqualTo(CREDENTIAL_TYPE_PRIVATE);
+ assertThat(credential.getIdentityType()).isEqualTo(IDENTITY_TYPE_PRIVATE);
+ assertThat(credential.getDeviceName()).isEqualTo(DEVICE_NAME);
+ assertThat(Arrays.equals(credential.getSecretId(), SECRETE_ID)).isTrue();
+ assertThat(Arrays.equals(credential.getAuthenticityKey(), AUTHENTICITY_KEY)).isTrue();
+ assertThat(Arrays.equals(credential.getMetadataEncryptionKey(),
+ METADATA_ENCRYPTION_KEY)).isTrue();
+ CredentialElement credentialElement = credential.getCredentialElements().get(0);
+ assertThat(credentialElement.getKey()).isEqualTo(KEY);
+ assertThat(Arrays.equals(credentialElement.getValue(), VALUE)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testWriteParcel() {
+ PrivateCredential credential = mBuilder.build();
+
+ Parcel parcel = Parcel.obtain();
+ credential.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ PrivateCredential credentialFromParcel = PrivateCredential.CREATOR.createFromParcel(
+ parcel);
+ parcel.recycle();
+
+ assertThat(credentialFromParcel.getType()).isEqualTo(CREDENTIAL_TYPE_PRIVATE);
+ assertThat(credentialFromParcel.getIdentityType()).isEqualTo(IDENTITY_TYPE_PRIVATE);
+ assertThat(Arrays.equals(credentialFromParcel.getSecretId(), SECRETE_ID)).isTrue();
+ assertThat(Arrays.equals(credentialFromParcel.getAuthenticityKey(),
+ AUTHENTICITY_KEY)).isTrue();
+ assertThat(Arrays.equals(credentialFromParcel.getMetadataEncryptionKey(),
+ METADATA_ENCRYPTION_KEY)).isTrue();
+ CredentialElement credentialElement = credentialFromParcel.getCredentialElements().get(0);
+ assertThat(credentialElement.getKey()).isEqualTo(KEY);
+ assertThat(Arrays.equals(credentialElement.getValue(), VALUE)).isTrue();
+ }
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
new file mode 100644
index 0000000..11bbacc
--- /dev/null
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.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.cts;
+
+import static android.nearby.PresenceCredential.CREDENTIAL_TYPE_PUBLIC;
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.CredentialElement;
+import android.nearby.PresenceCredential;
+import android.nearby.PublicCredential;
+import android.os.Build;
+import android.os.Parcel;
+
+import androidx.annotation.RequiresApi;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+/** Tests for {@link PresenceCredential}. */
+@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public class PublicCredentialTest {
+
+ private static final byte[] SECRETE_ID = new byte[] {1, 2, 3, 4};
+ private static final byte[] AUTHENTICITY_KEY = new byte[] {0, 1, 1, 1};
+ private static final byte[] PUBLIC_KEY = new byte[] {1, 1, 2, 2};
+ private static final byte[] ENCRYPTED_METADATA = new byte[] {1, 2, 3, 4, 5};
+ private static final byte[] METADATA_ENCRYPTION_KEY_TAG = new byte[] {1, 1, 3, 4, 5};
+ private static final String KEY = "KEY";
+ private static final byte[] VALUE = new byte[] {1, 2, 3, 4, 5};
+
+ private PublicCredential.Builder mBuilder;
+
+ @Before
+ public void setUp() {
+ mBuilder =
+ new PublicCredential.Builder(
+ SECRETE_ID,
+ AUTHENTICITY_KEY,
+ PUBLIC_KEY,
+ ENCRYPTED_METADATA,
+ METADATA_ENCRYPTION_KEY_TAG)
+ .addCredentialElement(new CredentialElement(KEY, VALUE))
+ .setIdentityType(IDENTITY_TYPE_PRIVATE);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBuilder() {
+ PublicCredential credential = mBuilder.build();
+
+ assertThat(credential.getType()).isEqualTo(CREDENTIAL_TYPE_PUBLIC);
+ assertThat(credential.getIdentityType()).isEqualTo(IDENTITY_TYPE_PRIVATE);
+ assertThat(credential.getCredentialElements().get(0).getKey()).isEqualTo(KEY);
+ assertThat(Arrays.equals(credential.getSecretId(), SECRETE_ID)).isTrue();
+ assertThat(Arrays.equals(credential.getAuthenticityKey(), AUTHENTICITY_KEY)).isTrue();
+ assertThat(Arrays.equals(credential.getPublicKey(), PUBLIC_KEY)).isTrue();
+ assertThat(Arrays.equals(credential.getEncryptedMetadata(), ENCRYPTED_METADATA)).isTrue();
+ assertThat(
+ Arrays.equals(
+ credential.getEncryptedMetadataKeyTag(),
+ METADATA_ENCRYPTION_KEY_TAG))
+ .isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWriteParcel() {
+ PublicCredential credential = mBuilder.build();
+
+ Parcel parcel = Parcel.obtain();
+ credential.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ PublicCredential credentialFromParcel = PublicCredential.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+
+ assertThat(credentialFromParcel.getType()).isEqualTo(CREDENTIAL_TYPE_PUBLIC);
+ assertThat(credentialFromParcel.getIdentityType()).isEqualTo(IDENTITY_TYPE_PRIVATE);
+ assertThat(Arrays.equals(credentialFromParcel.getSecretId(), SECRETE_ID)).isTrue();
+ assertThat(Arrays.equals(credentialFromParcel.getAuthenticityKey(), AUTHENTICITY_KEY))
+ .isTrue();
+ assertThat(Arrays.equals(credentialFromParcel.getPublicKey(), PUBLIC_KEY)).isTrue();
+ assertThat(Arrays.equals(credentialFromParcel.getEncryptedMetadata(), ENCRYPTED_METADATA))
+ .isTrue();
+ assertThat(
+ Arrays.equals(
+ credentialFromParcel.getEncryptedMetadataKeyTag(),
+ METADATA_ENCRYPTION_KEY_TAG))
+ .isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEquals() {
+ PublicCredential credentialOne =
+ new PublicCredential.Builder(
+ SECRETE_ID,
+ AUTHENTICITY_KEY,
+ PUBLIC_KEY,
+ ENCRYPTED_METADATA,
+ METADATA_ENCRYPTION_KEY_TAG)
+ .addCredentialElement(new CredentialElement(KEY, VALUE))
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+
+ PublicCredential credentialTwo =
+ new PublicCredential.Builder(
+ SECRETE_ID,
+ AUTHENTICITY_KEY,
+ PUBLIC_KEY,
+ ENCRYPTED_METADATA,
+ METADATA_ENCRYPTION_KEY_TAG)
+ .addCredentialElement(new CredentialElement(KEY, VALUE))
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+ assertThat(credentialOne.equals((Object) credentialTwo)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testUnEquals() {
+ byte[] idOne = new byte[] {1, 2, 3, 4};
+ byte[] idTwo = new byte[] {4, 5, 6, 7};
+ PublicCredential credentialOne =
+ new PublicCredential.Builder(
+ idOne,
+ AUTHENTICITY_KEY,
+ PUBLIC_KEY,
+ ENCRYPTED_METADATA,
+ METADATA_ENCRYPTION_KEY_TAG)
+ .build();
+
+ PublicCredential credentialTwo =
+ new PublicCredential.Builder(
+ idTwo,
+ AUTHENTICITY_KEY,
+ PUBLIC_KEY,
+ ENCRYPTED_METADATA,
+ METADATA_ENCRYPTION_KEY_TAG)
+ .build();
+ assertThat(credentialOne.equals((Object) credentialTwo)).isFalse();
+ }
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
new file mode 100644
index 0000000..3a73b9f
--- /dev/null
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.cts;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+import static android.nearby.ScanRequest.SCAN_MODE_BALANCED;
+import static android.nearby.ScanRequest.SCAN_MODE_LOW_LATENCY;
+import static android.nearby.ScanRequest.SCAN_MODE_LOW_POWER;
+import static android.nearby.ScanRequest.SCAN_MODE_NO_POWER;
+import static android.nearby.ScanRequest.SCAN_TYPE_FAST_PAIR;
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanRequest;
+import android.os.Build;
+import android.os.WorkSource;
+
+import androidx.annotation.RequiresApi;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public class ScanRequestTest {
+
+ private static final int UID = 1001;
+ private static final String APP_NAME = "android.nearby.tests";
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testScanType() {
+ ScanRequest request = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .build();
+
+ assertThat(request.getScanType()).isEqualTo(SCAN_TYPE_NEARBY_PRESENCE);
+ }
+
+ // Valid scan type must be set to one of ScanRequest#SCAN_TYPE_
+ @Test(expected = IllegalStateException.class)
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testScanType_notSet_throwsException() {
+ new ScanRequest.Builder().setScanMode(SCAN_MODE_BALANCED).build();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testScanMode_defaultLowPower() {
+ ScanRequest request = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_FAST_PAIR)
+ .build();
+
+ assertThat(request.getScanMode()).isEqualTo(SCAN_MODE_LOW_POWER);
+ }
+
+ /** Verify setting work source with null value in the scan request is allowed */
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSetWorkSource_nullValue() {
+ ScanRequest request = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_FAST_PAIR)
+ .setWorkSource(null)
+ .build();
+
+ // Null work source is allowed.
+ assertThat(request.getWorkSource().isEmpty()).isTrue();
+ }
+
+ /** Verify toString returns expected string. */
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testToString() {
+ WorkSource workSource = getWorkSource();
+ ScanRequest request = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_FAST_PAIR)
+ .setScanMode(SCAN_MODE_BALANCED)
+ .setBleEnabled(true)
+ .setWorkSource(workSource)
+ .build();
+
+ assertThat(request.toString()).isEqualTo(
+ "Request[scanType=1, scanMode=SCAN_MODE_BALANCED, "
+ + "enableBle=true, workSource=WorkSource{" + UID + " " + APP_NAME
+ + "}, scanFilters=[]]");
+ }
+
+ /** Verify toString works correctly with null WorkSource. */
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testToString_nullWorkSource() {
+ ScanRequest request = new ScanRequest.Builder().setScanType(
+ SCAN_TYPE_FAST_PAIR).setWorkSource(null).build();
+
+ assertThat(request.toString()).isEqualTo("Request[scanType=1, "
+ + "scanMode=SCAN_MODE_LOW_POWER, enableBle=true, workSource=WorkSource{}, "
+ + "scanFilters=[]]");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testisEnableBle_defaultTrue() {
+ ScanRequest request = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_FAST_PAIR)
+ .build();
+
+ assertThat(request.isBleEnabled()).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_isValidScanType() {
+ assertThat(ScanRequest.isValidScanType(SCAN_TYPE_FAST_PAIR)).isTrue();
+ assertThat(ScanRequest.isValidScanType(SCAN_TYPE_NEARBY_PRESENCE)).isTrue();
+
+ assertThat(ScanRequest.isValidScanType(0)).isFalse();
+ assertThat(ScanRequest.isValidScanType(5)).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_isValidScanMode() {
+ assertThat(ScanRequest.isValidScanMode(SCAN_MODE_LOW_LATENCY)).isTrue();
+ assertThat(ScanRequest.isValidScanMode(SCAN_MODE_BALANCED)).isTrue();
+ assertThat(ScanRequest.isValidScanMode(SCAN_MODE_LOW_POWER)).isTrue();
+ assertThat(ScanRequest.isValidScanMode(SCAN_MODE_NO_POWER)).isTrue();
+
+ assertThat(ScanRequest.isValidScanMode(3)).isFalse();
+ assertThat(ScanRequest.isValidScanMode(-2)).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_scanModeToString() {
+ assertThat(ScanRequest.scanModeToString(2)).isEqualTo("SCAN_MODE_LOW_LATENCY");
+ assertThat(ScanRequest.scanModeToString(1)).isEqualTo("SCAN_MODE_BALANCED");
+ assertThat(ScanRequest.scanModeToString(0)).isEqualTo("SCAN_MODE_LOW_POWER");
+ assertThat(ScanRequest.scanModeToString(-1)).isEqualTo("SCAN_MODE_NO_POWER");
+
+ assertThat(ScanRequest.scanModeToString(3)).isEqualTo("SCAN_MODE_INVALID");
+ assertThat(ScanRequest.scanModeToString(-2)).isEqualTo("SCAN_MODE_INVALID");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testScanFilter() {
+ final byte[] secretId = new byte[]{1, 2, 3, 4};
+ final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+ final byte[] publicKey = new byte[]{1, 1, 2, 2};
+ final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+ final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+ PublicCredential credential = new PublicCredential.Builder(
+ secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+
+ final int rssi = -40;
+ final int action = 123;
+ PresenceScanFilter filter = new PresenceScanFilter.Builder()
+ .addCredential(credential)
+ .setMaxPathLoss(rssi)
+ .addPresenceAction(action)
+ .build();
+
+ ScanRequest request = new ScanRequest.Builder().setScanType(
+ SCAN_TYPE_FAST_PAIR).addScanFilter(filter).build();
+
+ assertThat(request.getScanFilters()).isNotEmpty();
+ assertThat(request.getScanFilters().get(0).getMaxPathLoss()).isEqualTo(rssi);
+ }
+
+ private static WorkSource getWorkSource() {
+ return new WorkSource(UID, APP_NAME);
+ }
+}
diff --git a/nearby/tests/integration/OWNERS b/nearby/tests/integration/OWNERS
new file mode 100644
index 0000000..f4dbde2
--- /dev/null
+++ b/nearby/tests/integration/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 1092133
+
+ericth@google.com
+ryancllin@google.com
\ No newline at end of file
diff --git a/nearby/tests/integration/privileged/Android.bp b/nearby/tests/integration/privileged/Android.bp
new file mode 100644
index 0000000..e3250f6
--- /dev/null
+++ b/nearby/tests/integration/privileged/Android.bp
@@ -0,0 +1,33 @@
+// 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_test {
+ name: "NearbyIntegrationPrivilegedTests",
+ defaults: ["mts-target-sdk-version-current"],
+ sdk_version: "test_current",
+ certificate: "platform",
+
+ srcs: ["src/**/*.kt"],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "junit",
+ "truth-prebuilt",
+ ],
+ test_suites: ["device-tests"],
+}
diff --git a/nearby/tests/integration/privileged/AndroidManifest.xml b/nearby/tests/integration/privileged/AndroidManifest.xml
new file mode 100644
index 0000000..00845f1
--- /dev/null
+++ b/nearby/tests/integration/privileged/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?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.integration.privileged">
+
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.nearby.integration.privileged"
+ android:label="Nearby Mainline Module Integration Privileged Tests" />
+
+</manifest>
diff --git a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/FastPairSettingsProviderTest.kt b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/FastPairSettingsProviderTest.kt
new file mode 100644
index 0000000..af3f75f
--- /dev/null
+++ b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/FastPairSettingsProviderTest.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.integration.privileged
+
+import android.content.Context
+import android.provider.Settings
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
+
+data class FastPairSettingsFlag(val name: String, val value: Int) {
+ override fun toString() = name
+}
+
+@RunWith(Parameterized::class)
+class FastPairSettingsProviderTest(private val flag: FastPairSettingsFlag) {
+
+ /** Verify privileged app can enable/disable Fast Pair scan. */
+ @Test
+ fun testSettingsFastPairScan_fromPrivilegedApp() {
+ val appContext = ApplicationProvider.getApplicationContext<Context>()
+ val contentResolver = appContext.contentResolver
+
+ Settings.Secure.putInt(contentResolver, "fast_pair_scan_enabled", flag.value)
+
+ val actualValue = Settings.Secure.getInt(
+ contentResolver, "fast_pair_scan_enabled", /* default value */ -1)
+ assertThat(actualValue).isEqualTo(flag.value)
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}Succeed")
+ fun fastPairScanFlags() = listOf(
+ FastPairSettingsFlag(name = "disable", value = 0),
+ FastPairSettingsFlag(name = "enable", value = 1),
+ )
+ }
+}
diff --git a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
new file mode 100644
index 0000000..3b6337a
--- /dev/null
+++ b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
@@ -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 android.nearby.integration.privileged
+
+import android.content.Context
+import android.nearby.NearbyManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class NearbyManagerTest {
+
+ /** Verify privileged app can get Nearby service. */
+ @Test
+ fun testContextGetNearbySystemService_fromPrivilegedApp_returnsNoneNull() {
+ val appContext = ApplicationProvider.getApplicationContext<Context>()
+ val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+
+ assertThat(nearbyManager).isNotNull()
+ }
+}
diff --git a/nearby/tests/integration/untrusted/Android.bp b/nearby/tests/integration/untrusted/Android.bp
new file mode 100644
index 0000000..53dbfb7
--- /dev/null
+++ b/nearby/tests/integration/untrusted/Android.bp
@@ -0,0 +1,33 @@
+// 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_test {
+ name: "NearbyIntegrationUntrustedTests",
+ defaults: ["mts-target-sdk-version-current"],
+ sdk_version: "test_current",
+
+ srcs: ["src/**/*.kt"],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "junit",
+ "kotlin-test",
+ "truth-prebuilt",
+ ],
+ test_suites: ["device-tests"],
+}
diff --git a/nearby/tests/integration/untrusted/AndroidManifest.xml b/nearby/tests/integration/untrusted/AndroidManifest.xml
new file mode 100644
index 0000000..d73f6b2
--- /dev/null
+++ b/nearby/tests/integration/untrusted/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?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.integration.untrusted">
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.nearby.integration.untrusted"
+ android:label="Nearby Mainline Module Integration Untrusted Tests" />
+
+</manifest>
diff --git a/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/FastPairSettingsProviderTest.kt b/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/FastPairSettingsProviderTest.kt
new file mode 100644
index 0000000..c549073
--- /dev/null
+++ b/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/FastPairSettingsProviderTest.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.integration.untrusted
+
+import android.content.Context
+import android.content.ContentResolver
+import android.provider.Settings
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertFailsWith
+
+
+@RunWith(AndroidJUnit4::class)
+class FastPairSettingsProviderTest {
+ private lateinit var contentResolver: ContentResolver
+
+ @Before
+ fun setUp() {
+ contentResolver = ApplicationProvider.getApplicationContext<Context>().contentResolver
+ }
+
+ /** Verify untrusted app can read Fast Pair scan enabled setting. */
+ @Test
+ fun testSettingsFastPairScan_fromUnTrustedApp_readsSucceed() {
+ Settings.Secure.getInt(contentResolver,
+ "fast_pair_scan_enabled", /* default value */ -1)
+ }
+
+ /** Verify untrusted app can't write Fast Pair scan enabled setting. */
+ @Test
+ fun testSettingsFastPairScan_fromUnTrustedApp_writesFailed() {
+ assertFailsWith<SecurityException> {
+ Settings.Secure.putInt(contentResolver, "fast_pair_scan_enabled", 1)
+ }
+ }
+}
diff --git a/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt b/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt
new file mode 100644
index 0000000..04c5e30
--- /dev/null
+++ b/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.integration.untrusted
+
+import android.content.Context
+import android.nearby.NearbyManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class NearbyManagerTest {
+
+ /** Verify untrusted app can get Nearby service. */
+ @Test
+ fun testContextGetNearbyService_fromUnTrustedApp_returnsNotNull() {
+ val appContext = ApplicationProvider.getApplicationContext<Context>()
+ assertThat(appContext.getSystemService(Context.NEARBY_SERVICE)).isNotNull()
+ }
+}
diff --git a/nearby/tests/multidevices/OWNERS b/nearby/tests/multidevices/OWNERS
new file mode 100644
index 0000000..f4dbde2
--- /dev/null
+++ b/nearby/tests/multidevices/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 1092133
+
+ericth@google.com
+ryancllin@google.com
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/Android.bp b/nearby/tests/multidevices/clients/Android.bp
new file mode 100644
index 0000000..49bc2e9
--- /dev/null
+++ b/nearby/tests/multidevices/clients/Android.bp
@@ -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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+ name: "NearbyMultiDevicesClientsLib",
+ srcs: ["src/**/*.kt"],
+ sdk_version: "test_current",
+ static_libs: [
+ "MoblySnippetHelperLib",
+ "NearbyFastPairProviderLib",
+ "NearbyFastPairSeekerSharedLib",
+ "androidx.test.core",
+ "androidx.test.ext.junit",
+ "androidx.test.uiautomator_uiautomator",
+ "kotlin-stdlib",
+ "mobly-snippet-lib",
+ "truth-prebuilt",
+ ],
+}
+
+android_app {
+ name: "NearbyMultiDevicesClientsSnippets",
+ sdk_version: "test_current",
+ certificate: "platform",
+ static_libs: ["NearbyMultiDevicesClientsLib"],
+ optimize: {
+ enabled: true,
+ shrink: false,
+ proguard_flags_files: ["proguard.flags"],
+ },
+}
diff --git a/nearby/tests/multidevices/clients/AndroidManifest.xml b/nearby/tests/multidevices/clients/AndroidManifest.xml
new file mode 100644
index 0000000..86c10b2
--- /dev/null
+++ b/nearby/tests/multidevices/clients/AndroidManifest.xml
@@ -0,0 +1,53 @@
+<?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.multidevices">
+
+ <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"/>
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
+ <application>
+ <meta-data
+ android:name="mobly-log-tag"
+ android:value="NearbyMainlineSnippet" />
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="android.nearby.multidevices.fastpair.seeker.FastPairSeekerSnippet,
+ android.nearby.multidevices.fastpair.provider.FastPairProviderSimulatorSnippet" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:label="Nearby Mainline Module Instrumentation Test"
+ android:targetPackage="android.nearby.multidevices" />
+
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:label="Nearby Mainline Module Mobly Snippet"
+ android:targetPackage="android.nearby.multidevices" />
+
+</manifest>
diff --git a/nearby/tests/multidevices/clients/proguard.flags b/nearby/tests/multidevices/clients/proguard.flags
new file mode 100644
index 0000000..11938cd
--- /dev/null
+++ b/nearby/tests/multidevices/clients/proguard.flags
@@ -0,0 +1,29 @@
+# Keep all snippet classes.
+-keep class android.nearby.multidevices.** {
+ *;
+}
+
+# Keep AdvertisingSetCallback#onOwnAddressRead callback.
+-keep class * extends android.bluetooth.le.AdvertisingSetCallback {
+ *;
+}
+
+# Do not touch Mobly.
+-keep class com.google.android.mobly.** {
+ *;
+}
+
+# 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/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorSnippet.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorSnippet.kt
new file mode 100644
index 0000000..922e950
--- /dev/null
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorSnippet.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.provider
+
+import android.annotation.TargetApi
+import android.content.Context
+import android.nearby.multidevices.fastpair.provider.controller.FastPairProviderSimulatorController
+import android.nearby.multidevices.fastpair.provider.events.ProviderStatusEvents
+import android.os.Build
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.android.mobly.snippet.Snippet
+import com.google.android.mobly.snippet.rpc.AsyncRpc
+import com.google.android.mobly.snippet.rpc.Rpc
+
+/** Expose Mobly RPC methods for Python side to simulate fast pair provider role. */
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+class FastPairProviderSimulatorSnippet : Snippet {
+ private val context: Context = InstrumentationRegistry.getInstrumentation().context
+ private val fastPairProviderSimulatorController = FastPairProviderSimulatorController(context)
+
+ /** Sets up the Fast Pair provider simulator. */
+ @AsyncRpc(description = "Sets up FP provider simulator.")
+ fun setupProviderSimulator(callbackId: String) {
+ fastPairProviderSimulatorController.setupProviderSimulator(ProviderStatusEvents(callbackId))
+ }
+
+ /**
+ * Starts model id advertising for scanning and initial pairing.
+ *
+ * @param callbackId the callback ID corresponding to the
+ * [FastPairProviderSimulatorSnippet#startProviderSimulator] call that started the scanning.
+ * @param modelId a 3-byte hex string for seeker side to recognize the device (ex: 0x00000C).
+ * @param antiSpoofingKeyString a public key for registered headsets.
+ */
+ @AsyncRpc(description = "Starts model id advertising for scanning and initial pairing.")
+ fun startModelIdAdvertising(
+ callbackId: String,
+ modelId: String,
+ antiSpoofingKeyString: String
+ ) {
+ fastPairProviderSimulatorController.startModelIdAdvertising(
+ modelId,
+ antiSpoofingKeyString,
+ ProviderStatusEvents(callbackId)
+ )
+ }
+
+ /** Tears down the Fast Pair provider simulator. */
+ @Rpc(description = "Tears down FP provider simulator.")
+ fun teardownProviderSimulator() {
+ fastPairProviderSimulatorController.teardownProviderSimulator()
+ }
+
+ /** Gets BLE mac address of the Fast Pair provider simulator. */
+ @Rpc(description = "Gets BLE mac address of the Fast Pair provider simulator.")
+ fun getBluetoothLeAddress(): String {
+ return fastPairProviderSimulatorController.getProviderSimulatorBleAddress()
+ }
+
+ /** Gets the latest account key received on the Fast Pair provider simulator */
+ @Rpc(description = "Gets the latest account key received on the Fast Pair provider simulator.")
+ fun getLatestReceivedAccountKey(): String? {
+ return fastPairProviderSimulatorController.getLatestReceivedAccountKey()
+ }
+}
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/controller/FastPairProviderSimulatorController.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/controller/FastPairProviderSimulatorController.kt
new file mode 100644
index 0000000..a2d2659
--- /dev/null
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/controller/FastPairProviderSimulatorController.kt
@@ -0,0 +1,133 @@
+/*
+ * 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.provider.controller
+
+import android.bluetooth.le.AdvertiseSettings
+import android.content.Context
+import android.nearby.fastpair.provider.FastPairSimulator
+import android.nearby.fastpair.provider.bluetooth.BluetoothController
+import com.google.android.mobly.snippet.util.Log
+import com.google.common.io.BaseEncoding.base64
+
+class FastPairProviderSimulatorController(private val context: Context) :
+ FastPairSimulator.AdvertisingChangedCallback, BluetoothController.EventListener {
+ private lateinit var bluetoothController: BluetoothController
+ private lateinit var eventListener: EventListener
+ private var simulator: FastPairSimulator? = null
+
+ fun setupProviderSimulator(listener: EventListener) {
+ eventListener = listener
+
+ bluetoothController = BluetoothController(context, this)
+ bluetoothController.registerBluetoothStateReceiver()
+ bluetoothController.enableBluetooth()
+ bluetoothController.connectA2DPSinkProfile()
+ }
+
+ fun teardownProviderSimulator() {
+ simulator?.destroy()
+ bluetoothController.unregisterBluetoothStateReceiver()
+ }
+
+ fun startModelIdAdvertising(
+ modelId: String,
+ antiSpoofingKeyString: String,
+ listener: EventListener
+ ) {
+ eventListener = listener
+
+ val antiSpoofingKey = base64().decode(antiSpoofingKeyString)
+ simulator = FastPairSimulator(
+ context, FastPairSimulator.Options.builder(modelId)
+ .setAdvertisingModelId(modelId)
+ .setBluetoothAddress(null)
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
+ .setAdvertisingChangedCallback(this)
+ .setAntiSpoofingPrivateKey(antiSpoofingKey)
+ .setUseRandomSaltForAccountKeyRotation(false)
+ .setDataOnlyConnection(false)
+ .setShowsPasskeyConfirmation(false)
+ .setRemoveAllDevicesDuringPairing(true)
+ .build()
+ )
+ }
+
+ fun getProviderSimulatorBleAddress() = simulator!!.bleAddress!!
+
+ fun getLatestReceivedAccountKey() =
+ simulator!!.accountKey?.let { base64().encode(it.toByteArray()) }
+
+ /**
+ * Called when we change our BLE advertisement.
+ *
+ * @param isAdvertising the advertising status.
+ */
+ override fun onAdvertisingChanged(isAdvertising: Boolean) {
+ Log.i("FastPairSimulator onAdvertisingChanged(isAdvertising: $isAdvertising)")
+ eventListener.onAdvertisingChange(isAdvertising)
+ }
+
+ /** The callback for the first onServiceConnected of A2DP sink profile. */
+ override fun onA2DPSinkProfileConnected() {
+ eventListener.onA2DPSinkProfileConnected()
+ }
+
+ /**
+ * Reports the current bond state of the remote device.
+ *
+ * @param bondState the bond state of the remote device.
+ */
+ override fun onBondStateChanged(bondState: Int) {
+ }
+
+ /**
+ * Reports the current connection state of the remote device.
+ *
+ * @param connectionState the bond state of the remote device.
+ */
+ override fun onConnectionStateChanged(connectionState: Int) {
+ }
+
+ /**
+ * Reports the current scan mode of the local Adapter.
+ *
+ * @param mode the current scan mode of the local Adapter.
+ */
+ override fun onScanModeChange(mode: Int) {
+ eventListener.onScanModeChange(FastPairSimulator.scanModeToString(mode))
+ }
+
+ /** Interface for listening the events from Fast Pair Provider Simulator. */
+ interface EventListener {
+ /** Reports the first onServiceConnected of A2DP sink profile. */
+ fun onA2DPSinkProfileConnected()
+
+ /**
+ * Reports the current scan mode of the local Adapter.
+ *
+ * @param mode the current scan mode in string.
+ */
+ fun onScanModeChange(mode: String)
+
+ /**
+ * Indicates the advertising state of the Fast Pair provider simulator has changed.
+ *
+ * @param isAdvertising the current advertising state, true if advertising otherwise false.
+ */
+ fun onAdvertisingChange(isAdvertising: Boolean)
+ }
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/events/ProviderStatusEvents.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/events/ProviderStatusEvents.kt
new file mode 100644
index 0000000..2addd77
--- /dev/null
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/events/ProviderStatusEvents.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.provider.events
+
+import android.nearby.multidevices.fastpair.provider.controller.FastPairProviderSimulatorController
+import com.google.android.mobly.snippet.util.postSnippetEvent
+
+/** The Mobly snippet events to report to the Python side. */
+class ProviderStatusEvents(private val callbackId: String) :
+ FastPairProviderSimulatorController.EventListener {
+
+ /** Reports the first onServiceConnected of A2DP sink profile. */
+ override fun onA2DPSinkProfileConnected() {
+ postSnippetEvent(callbackId, "onA2DPSinkProfileConnected") {}
+ }
+
+ /**
+ * Indicates the Bluetooth scan mode of the Fast Pair provider simulator has changed.
+ *
+ * @param mode the current scan mode in String mapping by [FastPairSimulator#scanModeToString].
+ */
+ override fun onScanModeChange(mode: String) {
+ postSnippetEvent(callbackId, "onScanModeChange") { putString("mode", mode) }
+ }
+
+ /**
+ * Indicates the advertising state of the Fast Pair provider simulator has changed.
+ *
+ * @param isAdvertising the current advertising state, true if advertising otherwise false.
+ */
+ override fun onAdvertisingChange(isAdvertising: Boolean) {
+ postSnippetEvent(callbackId, "onAdvertisingChange") {
+ putBoolean("isAdvertising", isAdvertising)
+ }
+ }
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/FastPairSeekerSnippet.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/FastPairSeekerSnippet.kt
new file mode 100644
index 0000000..bfb7a50
--- /dev/null
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/FastPairSeekerSnippet.kt
@@ -0,0 +1,171 @@
+/*
+ * 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
+
+import android.content.Context
+import android.nearby.FastPairDeviceMetadata
+import android.nearby.NearbyManager
+import android.nearby.ScanCallback
+import android.nearby.ScanRequest
+import android.nearby.fastpair.seeker.FAKE_TEST_ACCOUNT_NAME
+import android.nearby.multidevices.fastpair.seeker.data.FastPairTestDataManager
+import android.nearby.multidevices.fastpair.seeker.events.PairingCallbackEvents
+import android.nearby.multidevices.fastpair.seeker.events.ScanCallbackEvents
+import android.nearby.multidevices.fastpair.seeker.ui.CheckNearbyHalfSheetUiTest
+import android.nearby.multidevices.fastpair.seeker.ui.DismissNearbyHalfSheetUiTest
+import android.nearby.multidevices.fastpair.seeker.ui.PairByNearbyHalfSheetUiTest
+import android.provider.Settings
+import androidx.test.core.app.ApplicationProvider
+import com.google.android.mobly.snippet.Snippet
+import com.google.android.mobly.snippet.rpc.AsyncRpc
+import com.google.android.mobly.snippet.rpc.Rpc
+import com.google.android.mobly.snippet.util.Log
+
+/** Expose Mobly RPC methods for Python side to test fast pair seeker role. */
+class FastPairSeekerSnippet : Snippet {
+ private val appContext = ApplicationProvider.getApplicationContext<Context>()
+ private val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+ private val fastPairTestDataManager = FastPairTestDataManager(appContext)
+ private lateinit var scanCallback: ScanCallback
+
+ /**
+ * Starts scanning as a Fast Pair seeker to find provider devices.
+ *
+ * @param callbackId the callback ID corresponding to the {@link FastPairSeekerSnippet#startScan}
+ * call that started the scanning.
+ */
+ @AsyncRpc(description = "Starts scanning as Fast Pair seeker to find provider devices.")
+ fun startScan(callbackId: String) {
+ val scanRequest = ScanRequest.Builder()
+ .setScanMode(ScanRequest.SCAN_MODE_LOW_LATENCY)
+ .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR)
+ .setBleEnabled(true)
+ .build()
+ scanCallback = ScanCallbackEvents(callbackId)
+
+ Log.i("Start Fast Pair scanning via BLE...")
+ nearbyManager.startScan(scanRequest, /* executor */ { it.run() }, scanCallback)
+ }
+
+ /** Stops the Fast Pair seeker scanning. */
+ @Rpc(description = "Stops the Fast Pair seeker scanning.")
+ fun stopScan() {
+ Log.i("Stop Fast Pair scanning.")
+ nearbyManager.stopScan(scanCallback)
+ }
+
+ /** Waits and asserts the HalfSheet showed for Fast Pair pairing.
+ *
+ * @param modelId the expected model id to be associated with the HalfSheet.
+ * @param timeout the number of seconds to wait before giving up.
+ */
+ @Rpc(description = "Waits the HalfSheet showed for Fast Pair pairing.")
+ fun waitAndAssertHalfSheetShowed(modelId: String, timeout: Int) {
+ Log.i("Waits and asserts the HalfSheet showed for Fast Pair model $modelId.")
+
+ val deviceMetadata: FastPairDeviceMetadata =
+ fastPairTestDataManager.testDataCache.getFastPairDeviceMetadata(modelId)
+ ?: throw IllegalArgumentException(
+ "Can't find $modelId-FastPairAntispoofKeyDeviceMetadata pair in " +
+ "FastPairTestDataCache."
+ )
+ val deviceName = deviceMetadata.name!!
+ val initialPairingDescriptionTemplateText = deviceMetadata.initialPairingDescription!!
+
+ CheckNearbyHalfSheetUiTest(
+ waitHalfSheetPopupTimeoutSeconds = timeout,
+ halfSheetTitleText = deviceName,
+ halfSheetSubtitleText = initialPairingDescriptionTemplateText.format(
+ deviceName,
+ FAKE_TEST_ACCOUNT_NAME
+ )
+ ).checkNearbyHalfSheetUi()
+ }
+
+ /** Puts a model id to FastPairAntispoofKeyDeviceMetadata pair into test data cache.
+ *
+ * @param modelId a string of model id to be associated with.
+ * @param json a string of FastPairAntispoofKeyDeviceMetadata JSON object.
+ */
+ @Rpc(
+ description =
+ "Puts a model id to FastPairAntispoofKeyDeviceMetadata pair into test data cache."
+ )
+ fun putAntispoofKeyDeviceMetadata(modelId: String, json: String) {
+ Log.i("Puts a model id to FastPairAntispoofKeyDeviceMetadata pair into test data cache.")
+ fastPairTestDataManager.sendAntispoofKeyDeviceMetadata(modelId, json)
+ }
+
+ /** Puts an array of FastPairAccountKeyDeviceMetadata into test data cache.
+ *
+ * @param json a string of FastPairAccountKeyDeviceMetadata JSON array.
+ */
+ @Rpc(description = "Puts an array of FastPairAccountKeyDeviceMetadata into test data cache.")
+ fun putAccountKeyDeviceMetadata(json: String) {
+ Log.i("Puts an array of FastPairAccountKeyDeviceMetadata into test data cache.")
+ fastPairTestDataManager.sendAccountKeyDeviceMetadataJsonArray(json)
+ }
+
+ /** Dumps all FastPairAccountKeyDeviceMetadata from the test data cache. */
+ @Rpc(description = "Dumps all FastPairAccountKeyDeviceMetadata from the test data cache.")
+ fun dumpAccountKeyDeviceMetadata(): String {
+ Log.i("Dumps all FastPairAccountKeyDeviceMetadata from the test data cache.")
+ return fastPairTestDataManager.testDataCache.dumpAccountKeyDeviceMetadataListAsJson()
+ }
+
+ /** Writes into {@link Settings} whether Fast Pair scan is enabled.
+ *
+ * @param enable whether the Fast Pair scan should be enabled.
+ */
+ @Rpc(description = "Writes into Settings whether Fast Pair scan is enabled.")
+ fun setFastPairScanEnabled(enable: Boolean) {
+ Log.i("Writes into Settings whether Fast Pair scan is enabled.")
+ // TODO(b/228406038): Change back to use NearbyManager.setFastPairScanEnabled once un-hide.
+ val resolver = appContext.contentResolver
+ Settings.Secure.putInt(resolver, "fast_pair_scan_enabled", if (enable) 1 else 0)
+ }
+
+ /** Dismisses the half sheet UI if showed. */
+ @Rpc(description = "Dismisses the half sheet UI if showed.")
+ fun dismissHalfSheet() {
+ Log.i("Dismisses the half sheet UI if showed.")
+
+ DismissNearbyHalfSheetUiTest().dismissHalfSheet()
+ }
+
+ /** Starts pairing by interacting with half sheet UI.
+ *
+ * @param callbackId the callback ID corresponding to the
+ * {@link FastPairSeekerSnippet#startPairing} call that started the pairing.
+ */
+ @AsyncRpc(description = "Starts pairing by interacting with half sheet UI.")
+ fun startPairing(callbackId: String) {
+ Log.i("Starts pairing by interacting with half sheet UI.")
+
+ PairByNearbyHalfSheetUiTest().clickConnectButton()
+ fastPairTestDataManager.registerDataReceiveListener(PairingCallbackEvents(callbackId))
+ }
+
+ /** Invokes when the snippet runner shutting down. */
+ override fun shutdown() {
+ super.shutdown()
+
+ Log.i("Resets the Fast Pair test data cache.")
+ fastPairTestDataManager.unregisterDataReceiveListener()
+ fastPairTestDataManager.sendResetCache()
+ }
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/data/FastPairTestDataManager.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/data/FastPairTestDataManager.kt
new file mode 100644
index 0000000..239ac61
--- /dev/null
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/data/FastPairTestDataManager.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.data
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.nearby.fastpair.seeker.ACTION_RESET_TEST_DATA_CACHE
+import android.nearby.fastpair.seeker.ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA
+import android.nearby.fastpair.seeker.ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA
+import android.nearby.fastpair.seeker.ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA
+import android.nearby.fastpair.seeker.DATA_JSON_STRING_KEY
+import android.nearby.fastpair.seeker.DATA_MODEL_ID_STRING_KEY
+import android.nearby.fastpair.seeker.FastPairTestDataCache
+import android.util.Log
+
+/** Manage local FastPairTestDataCache and send to/sync from the remote cache in data provider. */
+class FastPairTestDataManager(private val context: Context) : BroadcastReceiver() {
+ val testDataCache = FastPairTestDataCache()
+ var listener: EventListener? = null
+
+ /** Puts a model id to FastPairAntispoofKeyDeviceMetadata pair into local and remote cache.
+ *
+ * @param modelId a string of model id to be associated with.
+ * @param json a string of FastPairAntispoofKeyDeviceMetadata JSON object.
+ */
+ fun sendAntispoofKeyDeviceMetadata(modelId: String, json: String) {
+ Intent().also { intent ->
+ intent.action = ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA
+ intent.putExtra(DATA_MODEL_ID_STRING_KEY, modelId)
+ intent.putExtra(DATA_JSON_STRING_KEY, json)
+ context.sendBroadcast(intent)
+ }
+ testDataCache.putAntispoofKeyDeviceMetadata(modelId, json)
+ }
+
+ /** Puts account key device metadata array to local and remote cache.
+ *
+ * @param json a string of FastPairAccountKeyDeviceMetadata JSON array.
+ */
+ fun sendAccountKeyDeviceMetadataJsonArray(json: String) {
+ Intent().also { intent ->
+ intent.action = ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA
+ intent.putExtra(DATA_JSON_STRING_KEY, json)
+ context.sendBroadcast(intent)
+ }
+ testDataCache.putAccountKeyDeviceMetadataJsonArray(json)
+ }
+
+ /** Clears local and remote cache. */
+ fun sendResetCache() {
+ context.sendBroadcast(Intent(ACTION_RESET_TEST_DATA_CACHE))
+ testDataCache.reset()
+ }
+
+ /**
+ * Callback method for receiving Intent broadcast from FastPairTestDataProvider.
+ *
+ * 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) {
+ when (intent.action) {
+ ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA -> {
+ Log.d(TAG, "ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA received!")
+ val json = intent.getStringExtra(DATA_JSON_STRING_KEY)!!
+ testDataCache.putAccountKeyDeviceMetadataJsonObject(json)
+ listener?.onManageFastPairAccountDevice(json)
+ }
+ else -> Log.d(TAG, "Unknown action received!")
+ }
+ }
+
+ fun registerDataReceiveListener(listener: EventListener) {
+ this.listener = listener
+ val bondStateFilter = IntentFilter(ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA)
+ context.registerReceiver(this, bondStateFilter)
+ }
+
+ fun unregisterDataReceiveListener() {
+ this.listener = null
+ context.unregisterReceiver(this)
+ }
+
+ /** Interface for listening the data receive from the remote cache in data provider. */
+ interface EventListener {
+ /** Reports a FastPairAccountKeyDeviceMetadata write into the cache.
+ *
+ * @param json the FastPairAccountKeyDeviceMetadata as JSON object string.
+ */
+ fun onManageFastPairAccountDevice(json: String)
+ }
+
+ companion object {
+ private const val TAG = "FastPairTestDataManager"
+ }
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/PairingCallbackEvents.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/PairingCallbackEvents.kt
new file mode 100644
index 0000000..19de1d9
--- /dev/null
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/PairingCallbackEvents.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.events
+
+import android.nearby.multidevices.fastpair.seeker.data.FastPairTestDataManager
+import com.google.android.mobly.snippet.util.postSnippetEvent
+
+/** The Mobly snippet events to report to the Python side. */
+class PairingCallbackEvents(private val callbackId: String) :
+ FastPairTestDataManager.EventListener {
+
+ /** Reports a FastPairAccountKeyDeviceMetadata write into the cache.
+ *
+ * @param json the FastPairAccountKeyDeviceMetadata as JSON object string.
+ */
+ override fun onManageFastPairAccountDevice(json: String) {
+ postSnippetEvent(callbackId, "onManageAccountDevice") {
+ putString("accountDeviceJsonString", json)
+ }
+ }
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/ScanCallbackEvents.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/ScanCallbackEvents.kt
new file mode 100644
index 0000000..363355f
--- /dev/null
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/ScanCallbackEvents.kt
@@ -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.multidevices.fastpair.seeker.events
+
+import android.nearby.NearbyDevice
+import android.nearby.ScanCallback
+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 {
+
+ override fun onDiscovered(device: NearbyDevice) {
+ postSnippetEvent(callbackId, "onDiscovered") {
+ putString("device", device.toString())
+ }
+ }
+
+ override fun onUpdated(device: NearbyDevice) {
+ postSnippetEvent(callbackId, "onUpdated") {
+ putString("device", device.toString())
+ }
+ }
+
+ override fun onLost(device: NearbyDevice) {
+ postSnippetEvent(callbackId, "onLost") {
+ putString("device", device.toString())
+ }
+ }
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ui/CheckNearbyHalfSheetUiTest.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ui/CheckNearbyHalfSheetUiTest.kt
new file mode 100644
index 0000000..84b5e89
--- /dev/null
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ui/CheckNearbyHalfSheetUiTest.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.ui
+
+import android.os.Bundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** An instrumented test to check Nearby half sheet UI showed correctly.
+ *
+ * To run this test directly:
+ * am instrument -w -r \
+ * -e class android.nearby.multidevices.fastpair.seeker.ui.CheckNearbyHalfSheetUiTest \
+ * android.nearby.multidevices/androidx.test.runner.AndroidJUnitRunner
+ */
+@RunWith(AndroidJUnit4::class)
+class CheckNearbyHalfSheetUiTest {
+ private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ private val waitHalfSheetPopupTimeoutMs: Long
+ private val halfSheetTitleText: String
+ private val halfSheetSubtitleText: String
+
+ constructor() {
+ val arguments: Bundle = InstrumentationRegistry.getArguments()
+ waitHalfSheetPopupTimeoutMs = arguments.getLong(
+ WAIT_HALF_SHEET_POPUP_TIMEOUT_KEY,
+ DEFAULT_WAIT_HALF_SHEET_POPUP_TIMEOUT_MS
+ )
+ halfSheetTitleText =
+ arguments.getString(HALF_SHEET_TITLE_KEY, DEFAULT_HALF_SHEET_TITLE_TEXT)
+ halfSheetSubtitleText =
+ arguments.getString(HALF_SHEET_SUBTITLE_KEY, DEFAULT_HALF_SHEET_SUBTITLE_TEXT)
+ }
+
+ constructor(
+ waitHalfSheetPopupTimeoutSeconds: Int,
+ halfSheetTitleText: String,
+ halfSheetSubtitleText: String
+ ) {
+ this.waitHalfSheetPopupTimeoutMs = waitHalfSheetPopupTimeoutSeconds * 1000L
+ this.halfSheetTitleText = halfSheetTitleText
+ this.halfSheetSubtitleText = halfSheetSubtitleText
+ }
+
+ @Test
+ fun checkNearbyHalfSheetUi() {
+ // Check Nearby half sheet showed by checking button "Connect" on the DevicePairingFragment.
+ val isConnectButtonShowed = device.wait(
+ Until.hasObject(NearbyHalfSheetUiMap.DevicePairingFragment.connectButton),
+ waitHalfSheetPopupTimeoutMs
+ )
+ assertWithMessage("Nearby half sheet didn't show within $waitHalfSheetPopupTimeoutMs ms.")
+ .that(isConnectButtonShowed).isTrue()
+
+ val halfSheetTitle =
+ device.findObject(NearbyHalfSheetUiMap.DevicePairingFragment.halfSheetTitle)
+ assertThat(halfSheetTitle).isNotNull()
+ assertThat(halfSheetTitle.text).isEqualTo(halfSheetTitleText)
+
+ val halfSheetSubtitle =
+ device.findObject(NearbyHalfSheetUiMap.DevicePairingFragment.halfSheetSubtitle)
+ assertThat(halfSheetSubtitle).isNotNull()
+ assertThat(halfSheetSubtitle.text).isEqualTo(halfSheetSubtitleText)
+
+ val deviceImage = device.findObject(NearbyHalfSheetUiMap.DevicePairingFragment.deviceImage)
+ assertThat(deviceImage).isNotNull()
+
+ val infoButton = device.findObject(NearbyHalfSheetUiMap.DevicePairingFragment.infoButton)
+ assertThat(infoButton).isNotNull()
+ }
+
+ companion object {
+ private const val DEFAULT_WAIT_HALF_SHEET_POPUP_TIMEOUT_MS = 1000L
+ private const val DEFAULT_HALF_SHEET_TITLE_TEXT = "Fast Pair Provider Simulator"
+ private const val DEFAULT_HALF_SHEET_SUBTITLE_TEXT = "Fast Pair Provider Simulator will " +
+ "appear on devices linked with nearby-mainline-fpseeker@google.com"
+ private const val WAIT_HALF_SHEET_POPUP_TIMEOUT_KEY = "WAIT_HALF_SHEET_POPUP_TIMEOUT_MS"
+ private const val HALF_SHEET_TITLE_KEY = "HALF_SHEET_TITLE"
+ private const val HALF_SHEET_SUBTITLE_KEY = "HALF_SHEET_SUBTITLE"
+ }
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ui/DismissNearbyHalfSheetUiTest.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ui/DismissNearbyHalfSheetUiTest.kt
new file mode 100644
index 0000000..1d99d26
--- /dev/null
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ui/DismissNearbyHalfSheetUiTest.kt
@@ -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.multidevices.fastpair.seeker.ui
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** An instrumented test to dismiss Nearby half sheet UI.
+ *
+ * To run this test directly:
+ * am instrument -w -r \
+ * -e class android.nearby.multidevices.fastpair.seeker.ui.DismissNearbyHalfSheetUiTest \
+ * android.nearby.multidevices/androidx.test.runner.AndroidJUnitRunner
+ */
+@RunWith(AndroidJUnit4::class)
+class DismissNearbyHalfSheetUiTest {
+ private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+ @Test
+ fun dismissHalfSheet() {
+ device.pressHome()
+ device.waitForIdle()
+
+ assertWithMessage("Fail to dismiss Nearby half sheet.").that(
+ device.findObject(NearbyHalfSheetUiMap.DevicePairingFragment.connectButton)
+ ).isNull()
+ }
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ui/NearbyHalfSheetUiMap.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ui/NearbyHalfSheetUiMap.kt
new file mode 100644
index 0000000..c94ff01
--- /dev/null
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ui/NearbyHalfSheetUiMap.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.ui
+
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.BySelector
+
+/** UiMap for Nearby Mainline Half Sheet. */
+object NearbyHalfSheetUiMap {
+ private const val PACKAGE_NAME = "com.google.android.nearby.halfsheet"
+ private const val ANDROID_WIDGET_BUTTON = "android.widget.Button"
+ private const val ANDROID_WIDGET_IMAGE_VIEW = "android.widget.ImageView"
+ private const val ANDROID_WIDGET_TEXT_VIEW = "android.widget.TextView"
+
+ object DevicePairingFragment {
+ val halfSheetTitle: BySelector =
+ By.res(PACKAGE_NAME, "toolbar_title").clazz(ANDROID_WIDGET_TEXT_VIEW)
+ val halfSheetSubtitle: BySelector =
+ By.res(PACKAGE_NAME, "header_subtitle").clazz(ANDROID_WIDGET_TEXT_VIEW)
+ val deviceImage: BySelector =
+ By.res(PACKAGE_NAME, "pairing_pic").clazz(ANDROID_WIDGET_IMAGE_VIEW)
+ val connectButton: BySelector =
+ By.res(PACKAGE_NAME, "connect_btn").clazz(ANDROID_WIDGET_BUTTON).text("Connect")
+ val infoButton: BySelector =
+ By.res(PACKAGE_NAME, "info_icon").clazz(ANDROID_WIDGET_IMAGE_VIEW)
+ }
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ui/PairByNearbyHalfSheetUiTest.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ui/PairByNearbyHalfSheetUiTest.kt
new file mode 100644
index 0000000..9028668
--- /dev/null
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ui/PairByNearbyHalfSheetUiTest.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.ui
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** An instrumented test to start pairing by interacting with Nearby half sheet UI.
+ *
+ * To run this test directly:
+ * am instrument -w -r \
+ * -e class android.nearby.multidevices.fastpair.seeker.ui.PairByNearbyHalfSheetUiTest \
+ * android.nearby.multidevices/androidx.test.runner.AndroidJUnitRunner
+ */
+@RunWith(AndroidJUnit4::class)
+class PairByNearbyHalfSheetUiTest {
+ private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+ @Test
+ fun clickConnectButton() {
+ val connectButton = NearbyHalfSheetUiMap.DevicePairingFragment.connectButton
+ device.findObject(connectButton).click()
+ device.wait(Until.gone(connectButton), CONNECT_BUTTON_TIMEOUT_MILLS)
+ }
+
+ companion object {
+ const val CONNECT_BUTTON_TIMEOUT_MILLS = 3000L
+ }
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/Android.bp b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/Android.bp
new file mode 100644
index 0000000..328751a
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/Android.bp
@@ -0,0 +1,48 @@
+// 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_library {
+ name: "NearbyFastPairSeekerSharedLib",
+ srcs: ["shared/**/*.kt"],
+ sdk_version: "test_current",
+ static_libs: [
+ // TODO(b/228406038): Remove "framework-nearby-static" once Fast Pair system APIs add back.
+ "framework-nearby-static",
+ "guava",
+ "gson-prebuilt-jar",
+ ],
+}
+
+android_library {
+ name: "NearbyFastPairSeekerDataProviderLib",
+ srcs: ["src/**/*.kt"],
+ sdk_version: "test_current",
+ static_libs: ["NearbyFastPairSeekerSharedLib"],
+}
+
+android_app {
+ name: "NearbyFastPairSeekerDataProvider",
+ sdk_version: "test_current",
+ certificate: "platform",
+ static_libs: ["NearbyFastPairSeekerDataProviderLib"],
+ optimize: {
+ enabled: true,
+ shrink: true,
+ proguard_flags_files: ["proguard.flags"],
+ },
+}
diff --git a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/AndroidManifest.xml b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/AndroidManifest.xml
new file mode 100644
index 0000000..1d62f04
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/AndroidManifest.xml
@@ -0,0 +1,47 @@
+<?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.seeker.dataprovider">
+
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
+ <application>
+ <!-- Fast Pair Data Provider Service which acts as an "overlay" to the
+ framework Fast Pair Data Provider. Only supported on Android T and later.
+ All overlays are protected from non-system access via WRITE_SECURE_SETTINGS.
+ Must stay in the same process as Nearby Discovery Service.
+ -->
+ <service
+ android:name=".FastPairTestDataProviderService"
+ android:exported="true"
+ android:permission="android.permission.WRITE_SECURE_SETTINGS"
+ android:visibleToInstantApps="true">
+ <intent-filter>
+ <action android:name="android.nearby.action.FAST_PAIR_DATA_PROVIDER" />
+ </intent-filter>
+
+ <meta-data
+ android:name="instantapps.clients.allowed"
+ android:value="true" />
+ <meta-data
+ android:name="serviceVersion"
+ android:value="1" />
+ </service>
+ </application>
+
+</manifest>
diff --git a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/proguard.flags b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/proguard.flags
new file mode 100644
index 0000000..15debab
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/proguard.flags
@@ -0,0 +1,19 @@
+# Keep all receivers/service classes.
+-keep class android.nearby.fastpair.seeker.** {
+ *;
+}
+
+# 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/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/Constants.kt b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/Constants.kt
new file mode 100644
index 0000000..6070140
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/Constants.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.fastpair.seeker
+
+const val FAKE_TEST_ACCOUNT_NAME = "nearby-mainline-fpseeker@google.com"
+
+const val ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA =
+ "android.nearby.fastpair.seeker.action.ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA"
+const val ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA =
+ "android.nearby.fastpair.seeker.action.ACCOUNT_KEY_DEVICE_METADATA"
+const val ACTION_RESET_TEST_DATA_CACHE = "android.nearby.fastpair.seeker.action.RESET"
+const val ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA =
+ "android.nearby.fastpair.seeker.action.WRITE_ACCOUNT_KEY_DEVICE_METADATA"
+
+const val DATA_JSON_STRING_KEY = "json"
+const val DATA_MODEL_ID_STRING_KEY = "modelId"
diff --git a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/FastPairTestDataCache.kt b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/FastPairTestDataCache.kt
new file mode 100644
index 0000000..4fb8832
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/FastPairTestDataCache.kt
@@ -0,0 +1,265 @@
+/*
+ * 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.seeker
+
+import android.nearby.FastPairAccountKeyDeviceMetadata
+import android.nearby.FastPairAntispoofKeyDeviceMetadata
+import android.nearby.FastPairDeviceMetadata
+import android.nearby.FastPairDiscoveryItem
+import com.google.common.io.BaseEncoding
+import com.google.gson.GsonBuilder
+import com.google.gson.annotations.SerializedName
+
+/** Manage a cache of Fast Pair test data for testing. */
+class FastPairTestDataCache {
+ private val gson = GsonBuilder().disableHtmlEscaping().create()
+ private val accountKeyDeviceMetadataList = mutableListOf<FastPairAccountKeyDeviceMetadata>()
+ private val antispoofKeyDeviceMetadataDataMap =
+ mutableMapOf<String, FastPairAntispoofKeyDeviceMetadataData>()
+
+ fun putAccountKeyDeviceMetadataJsonArray(json: String) {
+ accountKeyDeviceMetadataList +=
+ gson.fromJson(json, Array<FastPairAccountKeyDeviceMetadataData>::class.java)
+ .map { it.toFastPairAccountKeyDeviceMetadata() }
+ }
+
+ fun putAccountKeyDeviceMetadataJsonObject(json: String) {
+ accountKeyDeviceMetadataList +=
+ gson.fromJson(json, FastPairAccountKeyDeviceMetadataData::class.java)
+ .toFastPairAccountKeyDeviceMetadata()
+ }
+
+ fun putAccountKeyDeviceMetadata(accountKeyDeviceMetadata: FastPairAccountKeyDeviceMetadata) {
+ accountKeyDeviceMetadataList += accountKeyDeviceMetadata
+ }
+
+ fun getAccountKeyDeviceMetadataList(): List<FastPairAccountKeyDeviceMetadata> =
+ accountKeyDeviceMetadataList.toList()
+
+ fun dumpAccountKeyDeviceMetadataAsJson(metadata: FastPairAccountKeyDeviceMetadata): String =
+ gson.toJson(FastPairAccountKeyDeviceMetadataData(metadata))
+
+ fun dumpAccountKeyDeviceMetadataListAsJson(): String =
+ gson.toJson(accountKeyDeviceMetadataList.map { FastPairAccountKeyDeviceMetadataData(it) })
+
+ fun putAntispoofKeyDeviceMetadata(modelId: String, json: String) {
+ antispoofKeyDeviceMetadataDataMap[modelId] =
+ gson.fromJson(json, FastPairAntispoofKeyDeviceMetadataData::class.java)
+ }
+
+ fun getAntispoofKeyDeviceMetadata(modelId: String): FastPairAntispoofKeyDeviceMetadata? {
+ return antispoofKeyDeviceMetadataDataMap[modelId]?.toFastPairAntispoofKeyDeviceMetadata()
+ }
+
+ fun getFastPairDeviceMetadata(modelId: String): FastPairDeviceMetadata? =
+ antispoofKeyDeviceMetadataDataMap[modelId]?.deviceMeta?.toFastPairDeviceMetadata()
+
+ fun reset() {
+ accountKeyDeviceMetadataList.clear()
+ antispoofKeyDeviceMetadataDataMap.clear()
+ }
+
+ data class FastPairAccountKeyDeviceMetadataData(
+ @SerializedName("account_key") val accountKey: String?,
+ @SerializedName("sha256_account_key_public_address") val accountKeyPublicAddress: String?,
+ @SerializedName("fast_pair_device_metadata") val deviceMeta: FastPairDeviceMetadataData?,
+ @SerializedName("fast_pair_discovery_item") val discoveryItem: FastPairDiscoveryItemData?
+ ) {
+ constructor(meta: FastPairAccountKeyDeviceMetadata) : this(
+ accountKey = meta.deviceAccountKey?.base64Encode(),
+ accountKeyPublicAddress = meta.sha256DeviceAccountKeyPublicAddress?.base64Encode(),
+ deviceMeta = meta.fastPairDeviceMetadata?.let { FastPairDeviceMetadataData(it) },
+ discoveryItem = meta.fastPairDiscoveryItem?.let { FastPairDiscoveryItemData(it) }
+ )
+
+ fun toFastPairAccountKeyDeviceMetadata(): FastPairAccountKeyDeviceMetadata {
+ return FastPairAccountKeyDeviceMetadata.Builder()
+ .setDeviceAccountKey(accountKey?.base64Decode())
+ .setSha256DeviceAccountKeyPublicAddress(accountKeyPublicAddress?.base64Decode())
+ .setFastPairDeviceMetadata(deviceMeta?.toFastPairDeviceMetadata())
+ .setFastPairDiscoveryItem(discoveryItem?.toFastPairDiscoveryItem())
+ .build()
+ }
+ }
+
+ data class FastPairAntispoofKeyDeviceMetadataData(
+ @SerializedName("anti_spoofing_public_key_str") val antispoofPublicKey: String?,
+ @SerializedName("fast_pair_device_metadata") val deviceMeta: FastPairDeviceMetadataData?
+ ) {
+ fun toFastPairAntispoofKeyDeviceMetadata(): FastPairAntispoofKeyDeviceMetadata {
+ return FastPairAntispoofKeyDeviceMetadata.Builder()
+ .setAntispoofPublicKey(antispoofPublicKey?.base64Decode())
+ .setFastPairDeviceMetadata(deviceMeta?.toFastPairDeviceMetadata())
+ .build()
+ }
+ }
+
+ data class FastPairDeviceMetadataData(
+ @SerializedName("ble_tx_power") val bleTxPower: Int,
+ @SerializedName("connect_success_companion_app_installed") val compAppInstalled: String?,
+ @SerializedName("connect_success_companion_app_not_installed") val comAppNotIns: String?,
+ @SerializedName("device_type") val deviceType: Int,
+ @SerializedName("download_companion_app_description") val downloadComApp: String?,
+ @SerializedName("fail_connect_go_to_settings_description") val failConnectDes: String?,
+ @SerializedName("image_url") val imageUrl: String?,
+ @SerializedName("initial_notification_description") val initNotification: String?,
+ @SerializedName("initial_notification_description_no_account") val initNoAccount: String?,
+ @SerializedName("initial_pairing_description") val initialPairingDescription: String?,
+ @SerializedName("intent_uri") val intentUri: String?,
+ @SerializedName("name") val name: String?,
+ @SerializedName("open_companion_app_description") val openCompanionAppDescription: String?,
+ @SerializedName("retroactive_pairing_description") val retroactivePairingDes: String?,
+ @SerializedName("subsequent_pairing_description") val subsequentPairingDescription: String?,
+ @SerializedName("trigger_distance") val triggerDistance: Double,
+ @SerializedName("case_url") val trueWirelessImageUrlCase: String?,
+ @SerializedName("left_bud_url") val trueWirelessImageUrlLeftBud: String?,
+ @SerializedName("right_bud_url") val trueWirelessImageUrlRightBud: String?,
+ @SerializedName("unable_to_connect_description") val unableToConnectDescription: String?,
+ @SerializedName("unable_to_connect_title") val unableToConnectTitle: String?,
+ @SerializedName("update_companion_app_description") val updateCompAppDes: String?,
+ @SerializedName("wait_launch_companion_app_description") val waitLaunchCompApp: String?
+ ) {
+ constructor(meta: FastPairDeviceMetadata) : this(
+ bleTxPower = meta.bleTxPower,
+ compAppInstalled = meta.connectSuccessCompanionAppInstalled,
+ comAppNotIns = meta.connectSuccessCompanionAppNotInstalled,
+ deviceType = meta.deviceType,
+ downloadComApp = meta.downloadCompanionAppDescription,
+ failConnectDes = meta.failConnectGoToSettingsDescription,
+ imageUrl = meta.imageUrl,
+ initNotification = meta.initialNotificationDescription,
+ initNoAccount = meta.initialNotificationDescriptionNoAccount,
+ initialPairingDescription = meta.initialPairingDescription,
+ intentUri = meta.intentUri,
+ name = meta.name,
+ openCompanionAppDescription = meta.openCompanionAppDescription,
+ retroactivePairingDes = meta.retroactivePairingDescription,
+ subsequentPairingDescription = meta.subsequentPairingDescription,
+ triggerDistance = meta.triggerDistance.toDouble(),
+ trueWirelessImageUrlCase = meta.trueWirelessImageUrlCase,
+ trueWirelessImageUrlLeftBud = meta.trueWirelessImageUrlLeftBud,
+ trueWirelessImageUrlRightBud = meta.trueWirelessImageUrlRightBud,
+ unableToConnectDescription = meta.unableToConnectDescription,
+ unableToConnectTitle = meta.unableToConnectTitle,
+ updateCompAppDes = meta.updateCompanionAppDescription,
+ waitLaunchCompApp = meta.waitLaunchCompanionAppDescription
+ )
+
+ fun toFastPairDeviceMetadata(): FastPairDeviceMetadata {
+ return FastPairDeviceMetadata.Builder()
+ .setBleTxPower(bleTxPower)
+ .setConnectSuccessCompanionAppInstalled(compAppInstalled)
+ .setConnectSuccessCompanionAppNotInstalled(comAppNotIns)
+ .setDeviceType(deviceType)
+ .setDownloadCompanionAppDescription(downloadComApp)
+ .setFailConnectGoToSettingsDescription(failConnectDes)
+ .setImageUrl(imageUrl)
+ .setInitialNotificationDescription(initNotification)
+ .setInitialNotificationDescriptionNoAccount(initNoAccount)
+ .setInitialPairingDescription(initialPairingDescription)
+ .setIntentUri(intentUri)
+ .setName(name)
+ .setOpenCompanionAppDescription(openCompanionAppDescription)
+ .setRetroactivePairingDescription(retroactivePairingDes)
+ .setSubsequentPairingDescription(subsequentPairingDescription)
+ .setTriggerDistance(triggerDistance.toFloat())
+ .setTrueWirelessImageUrlCase(trueWirelessImageUrlCase)
+ .setTrueWirelessImageUrlLeftBud(trueWirelessImageUrlLeftBud)
+ .setTrueWirelessImageUrlRightBud(trueWirelessImageUrlRightBud)
+ .setUnableToConnectDescription(unableToConnectDescription)
+ .setUnableToConnectTitle(unableToConnectTitle)
+ .setUpdateCompanionAppDescription(updateCompAppDes)
+ .setWaitLaunchCompanionAppDescription(waitLaunchCompApp)
+ .build()
+ }
+ }
+
+ data class FastPairDiscoveryItemData(
+ @SerializedName("action_url") val actionUrl: String?,
+ @SerializedName("action_url_type") val actionUrlType: Int,
+ @SerializedName("app_name") val appName: String?,
+ @SerializedName("authentication_public_key_secp256r1") val authenticationPublicKey: String?,
+ @SerializedName("description") val description: String?,
+ @SerializedName("device_name") val deviceName: String?,
+ @SerializedName("display_url") val displayUrl: String?,
+ @SerializedName("first_observation_timestamp_millis") val firstObservationMs: Long,
+ @SerializedName("icon_fife_url") val iconFfeUrl: String?,
+ @SerializedName("icon_png") val iconPng: String?,
+ @SerializedName("id") val id: String?,
+ @SerializedName("last_observation_timestamp_millis") val lastObservationMs: Long,
+ @SerializedName("mac_address") val macAddress: String?,
+ @SerializedName("package_name") val packageName: String?,
+ @SerializedName("pending_app_install_timestamp_millis") val pendingAppInstallMs: Long,
+ @SerializedName("rssi") val rssi: Int,
+ @SerializedName("state") val state: Int,
+ @SerializedName("title") val title: String?,
+ @SerializedName("trigger_id") val triggerId: String?,
+ @SerializedName("tx_power") val txPower: Int
+ ) {
+ constructor(item: FastPairDiscoveryItem) : this(
+ actionUrl = item.actionUrl,
+ actionUrlType = item.actionUrlType,
+ appName = item.appName,
+ authenticationPublicKey = item.authenticationPublicKeySecp256r1?.base64Encode(),
+ description = item.description,
+ deviceName = item.deviceName,
+ displayUrl = item.displayUrl,
+ firstObservationMs = item.firstObservationTimestampMillis,
+ iconFfeUrl = item.iconFfeUrl,
+ iconPng = item.iconPng?.base64Encode(),
+ id = item.id,
+ lastObservationMs = item.lastObservationTimestampMillis,
+ macAddress = item.macAddress,
+ packageName = item.packageName,
+ pendingAppInstallMs = item.pendingAppInstallTimestampMillis,
+ rssi = item.rssi,
+ state = item.state,
+ title = item.title,
+ triggerId = item.triggerId,
+ txPower = item.txPower
+ )
+
+ fun toFastPairDiscoveryItem(): FastPairDiscoveryItem {
+ return FastPairDiscoveryItem.Builder()
+ .setActionUrl(actionUrl)
+ .setActionUrlType(actionUrlType)
+ .setAppName(appName)
+ .setAuthenticationPublicKeySecp256r1(authenticationPublicKey?.base64Decode())
+ .setDescription(description)
+ .setDeviceName(deviceName)
+ .setDisplayUrl(displayUrl)
+ .setFirstObservationTimestampMillis(firstObservationMs)
+ .setIconFfeUrl(iconFfeUrl)
+ .setIconPng(iconPng?.base64Decode())
+ .setId(id)
+ .setLastObservationTimestampMillis(lastObservationMs)
+ .setMacAddress(macAddress)
+ .setPackageName(packageName)
+ .setPendingAppInstallTimestampMillis(pendingAppInstallMs)
+ .setRssi(rssi)
+ .setState(state)
+ .setTitle(title)
+ .setTriggerId(triggerId)
+ .setTxPower(txPower)
+ .build()
+ }
+ }
+}
+
+private fun String.base64Decode(): ByteArray = BaseEncoding.base64().decode(this)
+
+private fun ByteArray.base64Encode(): String = BaseEncoding.base64().encode(this)
diff --git a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/data/FastPairTestDataManager.kt b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/data/FastPairTestDataManager.kt
new file mode 100644
index 0000000..e924da1
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/data/FastPairTestDataManager.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.seeker.data
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.nearby.FastPairAccountKeyDeviceMetadata
+import android.nearby.fastpair.seeker.ACTION_RESET_TEST_DATA_CACHE
+import android.nearby.fastpair.seeker.ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA
+import android.nearby.fastpair.seeker.ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA
+import android.nearby.fastpair.seeker.ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA
+import android.nearby.fastpair.seeker.DATA_JSON_STRING_KEY
+import android.nearby.fastpair.seeker.DATA_MODEL_ID_STRING_KEY
+import android.nearby.fastpair.seeker.FastPairTestDataCache
+import android.util.Log
+
+/** Manage local FastPairTestDataCache and receive/update the remote cache in test snippet. */
+class FastPairTestDataManager(private val context: Context) : BroadcastReceiver() {
+ val testDataCache = FastPairTestDataCache()
+
+ /** Writes a FastPairAccountKeyDeviceMetadata into local and remote cache.
+ *
+ * @param accountKeyDeviceMetadata the FastPairAccountKeyDeviceMetadata to write.
+ * @return a json object string of the accountKeyDeviceMetadata.
+ */
+ fun writeAccountKeyDeviceMetadata(
+ accountKeyDeviceMetadata: FastPairAccountKeyDeviceMetadata
+ ): String {
+ testDataCache.putAccountKeyDeviceMetadata(accountKeyDeviceMetadata)
+
+ val json =
+ testDataCache.dumpAccountKeyDeviceMetadataAsJson(accountKeyDeviceMetadata)
+ Intent().also { intent ->
+ intent.action = ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA
+ intent.putExtra(DATA_JSON_STRING_KEY, json)
+ context.sendBroadcast(intent)
+ }
+ return json
+ }
+
+ /**
+ * Callback method for receiving Intent broadcast from test snippet.
+ *
+ * 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) {
+ when (intent.action) {
+ ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA -> {
+ Log.d(TAG, "ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA received!")
+ val modelId = intent.getStringExtra(DATA_MODEL_ID_STRING_KEY)!!
+ val json = intent.getStringExtra(DATA_JSON_STRING_KEY)!!
+ testDataCache.putAntispoofKeyDeviceMetadata(modelId, json)
+ }
+ ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA -> {
+ Log.d(TAG, "ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA received!")
+ val json = intent.getStringExtra(DATA_JSON_STRING_KEY)!!
+ testDataCache.putAccountKeyDeviceMetadataJsonArray(json)
+ }
+ ACTION_RESET_TEST_DATA_CACHE -> {
+ Log.d(TAG, "ACTION_RESET_TEST_DATA_CACHE received!")
+ testDataCache.reset()
+ }
+ else -> Log.d(TAG, "Unknown action received!")
+ }
+ }
+
+ companion object {
+ private const val TAG = "FastPairTestDataManager"
+ }
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/dataprovider/FastPairTestDataProviderService.kt b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/dataprovider/FastPairTestDataProviderService.kt
new file mode 100644
index 0000000..aec1379
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/dataprovider/FastPairTestDataProviderService.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.seeker.dataprovider
+
+import android.accounts.Account
+import android.content.IntentFilter
+import android.nearby.FastPairDataProviderService
+import android.nearby.FastPairEligibleAccount
+import android.nearby.fastpair.seeker.ACTION_RESET_TEST_DATA_CACHE
+import android.nearby.fastpair.seeker.ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA
+import android.nearby.fastpair.seeker.ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA
+import android.nearby.fastpair.seeker.FAKE_TEST_ACCOUNT_NAME
+import android.nearby.fastpair.seeker.data.FastPairTestDataManager
+import android.util.Log
+
+/**
+ * Fast Pair Test Data Provider Service entry point for platform overlay.
+ */
+class FastPairTestDataProviderService : FastPairDataProviderService(TAG) {
+ private lateinit var testDataManager: FastPairTestDataManager
+
+ override fun onCreate() {
+ Log.d(TAG, "onCreate()")
+ testDataManager = FastPairTestDataManager(this)
+
+ val bondStateFilter = IntentFilter(ACTION_RESET_TEST_DATA_CACHE).apply {
+ addAction(ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA)
+ addAction(ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA)
+ }
+ registerReceiver(testDataManager, bondStateFilter)
+ }
+
+ override fun onDestroy() {
+ Log.d(TAG, "onDestroy()")
+ unregisterReceiver(testDataManager)
+
+ super.onDestroy()
+ }
+
+ override fun onLoadFastPairAntispoofKeyDeviceMetadata(
+ request: FastPairAntispoofKeyDeviceMetadataRequest,
+ callback: FastPairAntispoofKeyDeviceMetadataCallback
+ ) {
+ val requestedModelId = request.modelId.bytesToStringLowerCase()
+ Log.d(TAG, "onLoadFastPairAntispoofKeyDeviceMetadata(modelId: $requestedModelId)")
+
+ val fastPairAntispoofKeyDeviceMetadata =
+ testDataManager.testDataCache.getAntispoofKeyDeviceMetadata(requestedModelId)
+ if (fastPairAntispoofKeyDeviceMetadata != null) {
+ callback.onFastPairAntispoofKeyDeviceMetadataReceived(
+ fastPairAntispoofKeyDeviceMetadata
+ )
+ } else {
+ Log.d(TAG, "No metadata available for $requestedModelId!")
+ callback.onError(ERROR_CODE_BAD_REQUEST, "No metadata available for $requestedModelId")
+ }
+ }
+
+ override fun onLoadFastPairAccountDevicesMetadata(
+ request: FastPairAccountDevicesMetadataRequest,
+ callback: FastPairAccountDevicesMetadataCallback
+ ) {
+ val requestedAccount = request.account
+ val requestedAccountKeys = request.deviceAccountKeys
+ Log.d(
+ TAG, "onLoadFastPairAccountDevicesMetadata(" +
+ "account: $requestedAccount, accountKeys:$requestedAccountKeys)"
+ )
+ Log.d(TAG, testDataManager.testDataCache.dumpAccountKeyDeviceMetadataListAsJson())
+
+ callback.onFastPairAccountDevicesMetadataReceived(
+ testDataManager.testDataCache.getAccountKeyDeviceMetadataList()
+ )
+ }
+
+ override fun onLoadFastPairEligibleAccounts(
+ request: FastPairEligibleAccountsRequest,
+ callback: FastPairEligibleAccountsCallback
+ ) {
+ Log.d(TAG, "onLoadFastPairEligibleAccounts()")
+ callback.onFastPairEligibleAccountsReceived(ELIGIBLE_ACCOUNTS_TEST_CONSTANT)
+ }
+
+ override fun onManageFastPairAccount(
+ request: FastPairManageAccountRequest,
+ callback: FastPairManageActionCallback
+ ) {
+ val requestedAccount = request.account
+ val requestType = request.requestType
+ Log.d(TAG, "onManageFastPairAccount(account: $requestedAccount, requestType: $requestType)")
+
+ callback.onSuccess()
+ }
+
+ override fun onManageFastPairAccountDevice(
+ request: FastPairManageAccountDeviceRequest,
+ callback: FastPairManageActionCallback
+ ) {
+ val requestedAccount = request.account
+ val requestType = request.requestType
+ val requestTypeString = if (requestType == MANAGE_REQUEST_ADD) "Add" else "Remove"
+ val requestedAccountKeyDeviceMetadata = request.accountKeyDeviceMetadata
+ Log.d(
+ TAG,
+ "onManageFastPairAccountDevice(requestedAccount: $requestedAccount, " +
+ "requestType: $requestTypeString,"
+ )
+
+ val requestedAccountKeyDeviceMetadataInJson =
+ testDataManager.writeAccountKeyDeviceMetadata(requestedAccountKeyDeviceMetadata)
+ Log.d(TAG, "requestedAccountKeyDeviceMetadata: $requestedAccountKeyDeviceMetadataInJson)")
+
+ callback.onSuccess()
+ }
+
+ companion object {
+ private const val TAG = "FastPairTestDataProviderService"
+ private val ELIGIBLE_ACCOUNTS_TEST_CONSTANT = listOf(
+ FastPairEligibleAccount.Builder()
+ .setAccount(Account(FAKE_TEST_ACCOUNT_NAME, "FakeTestAccount"))
+ .setOptIn(true)
+ .build()
+ )
+
+ private fun ByteArray.bytesToStringLowerCase(): String =
+ joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
+ }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp b/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp
new file mode 100644
index 0000000..298c9dc
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp
@@ -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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+ name: "NearbyFastPairProviderLib",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ sdk_version: "core_platform",
+ libs: [
+ // order matters: classes in framework-bluetooth are resolved before framework, meaning
+ // @hide APIs in framework-bluetooth are resolved before @SystemApi stubs in framework
+ "framework-bluetooth.impl",
+ "framework",
+
+ // if sdk_version="" this gets automatically included, but here we need to add manually.
+ "framework-res",
+ ],
+ static_libs: [
+ "NearbyFastPairProviderLiteProtos",
+ "androidx.core_core",
+ "androidx.test.core",
+ "error_prone_annotations",
+ "fast-pair-lite-protos",
+ "framework-annotations-lib",
+ "guava",
+ "kotlin-stdlib",
+ "nearby-common-lib",
+ ],
+}
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/test_support/fastpair_provider/proto/Android.bp b/nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/Android.bp
new file mode 100644
index 0000000..7ae43e5
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/Android.bp
@@ -0,0 +1,30 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "NearbyFastPairProviderLiteProtos",
+ proto: {
+ type: "lite",
+ canonical_path_from_root: false,
+ },
+ sdk_version: "system_current",
+ min_sdk_version: "30",
+ srcs: ["*.proto"],
+}
+
+
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/event_stream_protocol.proto b/nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/event_stream_protocol.proto
new file mode 100644
index 0000000..54db34a
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/event_stream_protocol.proto
@@ -0,0 +1,85 @@
+syntax = "proto2";
+
+package android.nearby.fastpair.provider;
+
+option java_package = "android.nearby.fastpair.provider";
+option java_outer_classname = "EventStreamProtocol";
+
+enum EventGroup {
+ UNSPECIFIED = 0;
+ BLUETOOTH = 1;
+ LOGGING = 2;
+ DEVICE = 3;
+ DEVICE_ACTION = 4;
+ DEVICE_CONFIGURATION = 5;
+ DEVICE_CAPABILITY_SYNC = 6;
+ SMART_AUDIO_SOURCE_SWITCHING = 7;
+ ACKNOWLEDGEMENT = 255;
+}
+
+enum BluetoothEventCode {
+ BLUETOOTH_UNSPECIFIED = 0;
+ BLUETOOTH_ENABLE_SILENCE_MODE = 1;
+ BLUETOOTH_DISABLE_SILENCE_MODE = 2;
+}
+
+enum LoggingEventCode {
+ LOG_UNSPECIFIED = 0;
+ LOG_FULL = 1;
+ LOG_SAVE_TO_BUFFER = 2;
+}
+
+enum DeviceEventCode {
+ DEVICE_UNSPECIFIED = 0;
+ DEVICE_MODEL_ID = 1;
+ DEVICE_BLE_ADDRESS = 2;
+ DEVICE_BATTERY_INFO = 3;
+ ACTIVE_COMPONENTS_REQUEST = 5;
+ ACTIVE_COMPONENTS_RESPONSE = 6;
+ DEVICE_CAPABILITY = 7;
+ PLATFORM_TYPE = 8;
+ FIRMWARE_VERSION = 9;
+ SECTION_NONCE = 10;
+}
+
+enum DeviceActionEventCode {
+ DEVICE_ACTION_UNSPECIFIED = 0;
+ DEVICE_ACTION_RING = 1;
+}
+
+enum DeviceConfigurationEventCode {
+ CONFIGURATION_UNSPECIFIED = 0;
+ CONFIGURATION_BUFFER_SIZE = 1;
+}
+
+enum DeviceCapabilitySyncEventCode {
+ REQUEST_UNSPECIFIED = 0;
+ REQUEST_CAPABILITY_UPDATE = 1;
+ CONFIGURABLE_BUFFER_SIZE_RANGE = 2;
+}
+
+enum AcknowledgementEventCode {
+ ACKNOWLEDGEMENT_UNSPECIFIED = 0;
+ ACKNOWLEDGEMENT_ACK = 1;
+ ACKNOWLEDGEMENT_NAK = 2;
+}
+
+enum PlatformType {
+ PLATFORM_TYPE_UNKNOWN = 0;
+ ANDROID = 1;
+}
+
+enum SassEventCode {
+ EVENT_UNSPECIFIED = 0;
+ EVENT_GET_CAPABILITY_OF_SASS = 0x10;
+ EVENT_NOTIFY_CAPABILITY_OF_SASS = 0x11;
+ EVENT_SET_MULTI_POINT_STATE = 0x12;
+ EVENT_SWITCH_AUDIO_SOURCE_BETWEEN_CONNECTED_DEVICES = 0x30;
+ EVENT_SWITCH_BACK = 0x31;
+ EVENT_NOTIFY_MULTIPOINT_SWITCH_EVENT = 0x32;
+ EVENT_GET_CONNECTION_STATUS = 0x33;
+ EVENT_NOTIFY_CONNECTION_STATUS = 0x34;
+ EVENT_SASS_INITIATED_CONNECTION = 0x40;
+ EVENT_INDICATE_IN_USE_ACCOUNT_KEY = 0x41;
+ EVENT_SET_CUSTOM_DATA = 0x42;
+}
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..125c34e
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/Android.bp
@@ -0,0 +1,57 @@
+// 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"],
+}
+
+// Build and install NearbyFastPairProviderSimulatorApp to your phone:
+// m NearbyFastPairProviderSimulatorApp
+// adb root
+// adb remount && adb reboot (make first time remount work)
+//
+// adb root
+// adb remount
+// adb push ${ANDROID_PRODUCT_OUT}/system/app/NearbyFastPairProviderSimulatorApp /system/app/
+// adb reboot
+// Grant all permissions requested to NearbyFastPairProviderSimulatorApp before launching it.
+android_app {
+ name: "NearbyFastPairProviderSimulatorApp",
+ sdk_version: "test_current",
+ // Sign with "platform" certificate for accessing Bluetooth @SystemAPI
+ certificate: "platform",
+ 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..0827c60
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proguard.flags
@@ -0,0 +1,19 @@
+# Keep AdvertisingSetCallback#onOwnAddressRead callback.
+-keep class * extends android.bluetooth.le.AdvertisingSetCallback {
+ *;
+}
+
+# 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/test_support/fastpair_provider/simulator_app/proto/Android.bp b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proto/Android.bp
new file mode 100644
index 0000000..e964800
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proto/Android.bp
@@ -0,0 +1,30 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "NearbyFastPairProviderSimulatorLiteProtos",
+ proto: {
+ type: "lite",
+ canonical_path_from_root: false,
+ },
+ sdk_version: "system_current",
+ min_sdk_version: "30",
+ 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/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..e916c53
--- /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,1044 @@
+/*
+ * 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 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.bluetooth.BluetoothController;
+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 {
+ public static final String TAG = "FastPairProviderSimulatorApp";
+ private final Logger mLogger = new Logger(TAG);
+
+ /** 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;
+ }
+
+ mLogger.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))) {
+ mLogger.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) {
+ mLogger.log(e, "Failed to create stream IO handler");
+ }
+ }
+
+ @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)) {
+ mLogger.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) {
+ mLogger.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))
+ .setAdvertisingChangedCallback(isAdvertising -> updateStatusView())
+ .setAntiSpoofingPrivateKey(antiSpoofingKey)
+ .setUseRandomSaltForAccountKeyRotation(useRandomSaltForAccountKeyRotation)
+ .setDataOnlyConnection(device != null && device.getDataOnlyConnection())
+ .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/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/InputStreamListener.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/InputStreamListener.java
new file mode 100644
index 0000000..b29225a
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/src/android/nearby/fastpair/provider/simulator/testing/InputStreamListener.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.fastpair.provider.simulator.testing;
+
+import com.google.protobuf.ByteString;
+
+/** Listener for input stream. */
+public interface InputStreamListener {
+
+ /** Called when new data {@code byteString} is read from the input stream. */
+ void onInputData(ByteString byteString);
+
+ /** 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/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/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairAdvertiser.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairAdvertiser.java
new file mode 100644
index 0000000..95c077b
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairAdvertiser.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+import androidx.annotation.Nullable;
+
+/** Helper for advertising Fast Pair data. */
+public interface FastPairAdvertiser {
+
+ void startAdvertising(@Nullable byte[] serviceData);
+
+ void stopAdvertising();
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulator.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulator.java
new file mode 100644
index 0000000..0d5563e
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulator.java
@@ -0,0 +1,2391 @@
+/*
+ * 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;
+
+import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
+import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE;
+import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
+import static android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE;
+import static android.bluetooth.BluetoothAdapter.STATE_OFF;
+import static android.bluetooth.BluetoothAdapter.STATE_ON;
+import static android.bluetooth.BluetoothDevice.ERROR;
+import static android.bluetooth.BluetoothGattCharacteristic.PERMISSION_READ;
+import static android.bluetooth.BluetoothGattCharacteristic.PERMISSION_WRITE;
+import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_INDICATE;
+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;
+import static com.android.server.nearby.common.bluetooth.fastpair.Bytes.toBytes;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.A2DP_SINK_SERVICE_UUID;
+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.google.common.io.BaseEncoding.base16;
+import static com.google.common.primitives.Bytes.concat;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothClass.Device.Major;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.le.AdvertiseSettings;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+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;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.util.Consumer;
+
+import com.android.server.nearby.common.bloomfilter.BloomFilter;
+import com.android.server.nearby.common.bloomfilter.FastPairBloomFilterHasher;
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.BluetoothGattException;
+import com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption;
+import com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress;
+import com.android.server.nearby.common.bluetooth.fastpair.Bytes.Value;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AccountKeyCharacteristic;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.BeaconActionsCharacteristic;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.FirmwareVersionCharacteristic;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.NameCharacteristic;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.PasskeyCharacteristic;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.TransportDiscoveryService;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.TransportDiscoveryService.BrHandoverDataCharacteristic;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.TransportDiscoveryService.ControlPointCharacteristic;
+import com.android.server.nearby.common.bluetooth.fastpair.EllipticCurveDiffieHellmanExchange;
+import com.android.server.nearby.common.bluetooth.fastpair.Ltv;
+import com.android.server.nearby.common.bluetooth.fastpair.MessageStreamHmacEncoder;
+import com.android.server.nearby.common.bluetooth.fastpair.NamingEncoder;
+
+import com.google.common.base.Ascii;
+import com.google.common.primitives.Bytes;
+import com.google.protobuf.ByteString;
+
+import java.lang.reflect.Method;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Simulates a Fast Pair device (e.g. a headset).
+ *
+ * <p>Note: There are two deviations from the spec:
+ *
+ * <ul>
+ * <li>Instead of using the public address when in pairing mode (discoverable), it always uses the
+ * random private address (RPA), because that's how stock Android works. To work around this,
+ * it implements the BR/EDR Handover profile (which is no longer part of the Fast Pair spec)
+ * when simulating a keyless device (i.e. Fast Pair 1.0), which allows the phone to ask for
+ * the public address. When there is an anti-spoofing key, i.e. Fast Pair 2.0, the public
+ * address is delivered via the Key-based Pairing handshake. b/79374759 tracks fixing this.
+ * <li>The simulator always identifies its device capabilities as Keyboard/Display, even when
+ * simulating a keyless (Fast Pair 1.0) device that should identify as NoInput/NoOutput.
+ * b/79377125 tracks fixing this.
+ * </ul>
+ *
+ * @see {http://go/fast-pair-2-spec}
+ */
+public class FastPairSimulator {
+ public static final String TAG = "FastPairSimulator";
+ private final Logger mLogger;
+
+ private static final int BECOME_DISCOVERABLE_TIMEOUT_SEC = 3;
+
+ private static final int SCAN_MODE_REFRESH_SEC = 30;
+
+ /**
+ * Headphones. Generated by
+ * http://bluetooth-pentest.narod.ru/software/bluetooth_class_of_device-service_generator.html
+ */
+ private static final Value CLASS_OF_DEVICE =
+ new Value(base16().decode("200418"), ByteOrder.BIG_ENDIAN);
+
+ private static final byte[] SUPPORTED_SERVICES_LTV = new Ltv(
+ TransportDiscoveryService.SERVICE_UUIDS_16_BIT_LIST_TYPE,
+ toBytes(ByteOrder.LITTLE_ENDIAN, A2DP_SINK_SERVICE_UUID)
+ ).getBytes();
+ private static final byte[] TDS_CONTROL_POINT_RESPONSE_PARAMETER =
+ Bytes.concat(new byte[]{BLUETOOTH_SIG_ORGANIZATION_ID}, SUPPORTED_SERVICES_LTV);
+
+ private static final String SIMULATOR_FAKE_BLE_ADDRESS = "11:22:33:44:55:66";
+
+ private static final long ADVERTISING_REFRESH_DELAY_1_MIN = TimeUnit.MINUTES.toMillis(1);
+
+ /**
+ * The size of account key filter in bytes is (1.2*n + 3), n represents the size of account key,
+ * see https://developers.google.com/nearby/fast-pair/spec#advertising_when_not_discoverable.
+ * However we'd like to advertise something else, so we could only afford 8 account keys.
+ *
+ * <ul>
+ * <li>BLE flags: 3 bytes
+ * <li>TxPower: 3 bytes
+ * <li>FastPair: max 25 bytes
+ * <ul>
+ * <li>FastPair service data: 4 bytes
+ * <li>Flags: 1 byte
+ * <li>Account key filter: max 14 bytes (1 byte: length + type, 13 bytes: max 8 account
+ * keys)
+ * <li>Salt: 2 bytes
+ * <li>Battery: 4 bytes
+ * </ul>
+ * </ul>
+ */
+ private String mDeviceFirmwareVersion = "1.1.0";
+
+ private byte[] mSessionNonce;
+
+ private boolean mUseLogFullEvent = true;
+
+ private enum ResultCode {
+ SUCCESS((byte) 0x00),
+ OP_CODE_NOT_SUPPORTED((byte) 0x01),
+ INVALID_PARAMETER((byte) 0x02),
+ UNSUPPORTED_ORGANIZATION_ID((byte) 0x03),
+ OPERATION_FAILED((byte) 0x04);
+
+ private final byte mByteValue;
+
+ ResultCode(byte byteValue) {
+ this.mByteValue = byteValue;
+ }
+ }
+
+ private enum TransportState {
+ OFF((byte) 0x00),
+ ON((byte) 0x01),
+ TEMPORARILY_UNAVAILABLE((byte) 0x10);
+
+ private final byte mByteValue;
+
+ TransportState(byte byteValue) {
+ this.mByteValue = byteValue;
+ }
+ }
+
+ 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 mExecutor =
+ Executors.newSingleThreadScheduledExecutor(); // exempt
+ private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mShouldFailPairing) {
+ mLogger.log("Pairing disabled by test app switch");
+ return;
+ }
+ 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.
+ mLogger.log("Intent received after simulator destroyed, ignoring");
+ return;
+ }
+ BluetoothDevice device = intent.getParcelableExtra(
+ BluetoothDevice.EXTRA_DEVICE);
+ switch (intent.getAction()) {
+ case BluetoothAdapter.ACTION_SCAN_MODE_CHANGED:
+ if (isDiscoverable()) {
+ 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);
+ mLogger.log(
+ "Pairing request, variant=%d, key=%s", variant,
+ key == ERROR ? "(none)" : key);
+
+ // Prevent Bluetooth Settings from getting the pairing request.
+ abortBroadcast();
+
+ 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).
+ 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.
+ mLocalPasskey = key;
+ checkPasskey();
+ } else if (variant == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) {
+ if (mPasskeyEventCallback != null) {
+ mPasskeyEventCallback.onPasskeyRequested(
+ FastPairSimulator.this::enterPassKey);
+ } else {
+ mLogger.log("passkeyEventCallback is not set!");
+ enterPassKey(key);
+ }
+ } else if (variant == BluetoothDevice.PAIRING_VARIANT_CONSENT) {
+ setPasskeyConfirmation(true);
+
+ } else if (variant == BluetoothDevice.PAIRING_VARIANT_PIN) {
+ if (mPasskeyEventCallback != null) {
+ mPasskeyEventCallback.onPasskeyRequested(
+ (int pin) -> {
+ byte[] newPin = convertPinToBytes(
+ String.format(Locale.ENGLISH, "%d", pin));
+ mPairingDevice.setPin(newPin);
+ });
+ }
+ } else {
+ // Reject the pairing request if it's not using the Numeric
+ // Comparison (aka Passkey Confirmation) method.
+ setPasskeyConfirmation(false);
+ }
+ break;
+ case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
+ int bondState =
+ intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
+ BluetoothDevice.BOND_NONE);
+ 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.
+ mAdvertiser.stopAdvertising();
+ // Not discoverable anymore, but still connectable.
+ setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
+ break;
+ case BluetoothDevice.BOND_BONDED:
+ // Once bonded, advertise the account keys.
+ mAdvertiser.startAdvertising(accountKeysServiceData());
+ setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
+
+ // If it is subsequent pair, we need to add paired device here.
+ 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.
+ mAdvertiser.startAdvertising(getServiceData());
+ break;
+ default:
+ break;
+ }
+ break;
+ case BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED:
+ mLogger.log(
+ "Connection state to %s changed to %d",
+ device,
+ intent.getIntExtra(
+ BluetoothAdapter.EXTRA_CONNECTION_STATE,
+ BluetoothAdapter.STATE_DISCONNECTED));
+ break;
+ case BluetoothAdapter.ACTION_STATE_CHANGED:
+ int state = intent.getIntExtra(EXTRA_STATE, -1);
+ mLogger.log("Bluetooth adapter state=%s", state);
+ switch (state) {
+ case STATE_ON:
+ startRfcommServer();
+ break;
+ case STATE_OFF:
+ stopRfcommServer();
+ break;
+ default: // fall out
+ }
+ break;
+ default:
+ mLogger.log(new IllegalArgumentException(intent.toString()),
+ "Received unexpected intent");
+ break;
+ }
+ }
+ };
+
+ @Nullable
+ private byte[] convertPinToBytes(@Nullable String pin) {
+ if (TextUtils.isEmpty(pin)) {
+ return null;
+ }
+ byte[] pinBytes;
+ pinBytes = pin.getBytes(StandardCharsets.UTF_8);
+ if (pinBytes.length <= 0 || pinBytes.length > 16) {
+ return null;
+ }
+ return pinBytes;
+ }
+
+ private final NotifiableGattServlet mPasskeyServlet =
+ new NotifiableGattServlet() {
+ @Override
+ // Simulating deprecated API {@code PasskeyCharacteristic.ID} for testing.
+ @SuppressWarnings("deprecation")
+ public BluetoothGattCharacteristic getBaseCharacteristic() {
+ return new BluetoothGattCharacteristic(
+ PasskeyCharacteristic.CUSTOM_128_BIT_UUID,
+ PROPERTY_WRITE | PROPERTY_INDICATE,
+ PERMISSION_WRITE);
+ }
+
+ @Override
+ public void write(
+ BluetoothGattServerConnection connection, int offset, byte[] value) {
+ 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 {
+ mRemotePasskey = PasskeyCharacteristic.decrypt(
+ PasskeyCharacteristic.Type.SEEKER, mSecret, value);
+ if (mPasskeyEventCallback != null) {
+ mPasskeyEventCallback.onRemotePasskeyReceived(mRemotePasskey);
+ }
+ checkPasskey();
+ } catch (GeneralSecurityException e) {
+ mLogger.log(
+ "Decrypting passkey value %s failed using key %s",
+ base16().encode(value), base16().encode(mSecret));
+ }
+ }
+ };
+
+ private final NotifiableGattServlet mDeviceNameServlet =
+ new NotifiableGattServlet() {
+ @Override
+ // Simulating deprecated API {@code NameCharacteristic.ID} for testing.
+ @SuppressWarnings("deprecation")
+ BluetoothGattCharacteristic getBaseCharacteristic() {
+ return new BluetoothGattCharacteristic(
+ NameCharacteristic.CUSTOM_128_BIT_UUID,
+ PROPERTY_WRITE | PROPERTY_INDICATE,
+ PERMISSION_WRITE);
+ }
+
+ @Override
+ public void write(
+ BluetoothGattServerConnection connection, int offset, byte[] value) {
+ 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.
+ mLogger.log("Got name byte array size = %d", value.length);
+ try {
+ String decryptedDeviceName =
+ NamingEncoder.decodeNamingPacket(mSecret, value);
+ if (decryptedDeviceName != null) {
+ setDeviceName(decryptedDeviceName.getBytes(StandardCharsets.UTF_8));
+ mLogger.log("write device name = %s", decryptedDeviceName);
+ }
+ } catch (GeneralSecurityException e) {
+ mLogger.log(e, "Failed to decrypt device name.");
+ }
+ // For testing to make sure we get the new provider name from simulator.
+ if (mWriteNameCountDown != null) {
+ mLogger.log("finish count down latch to write device name.");
+ mWriteNameCountDown.countDown();
+ }
+ }
+ };
+
+ private Value mBluetoothAddress;
+ private final FastPairAdvertiser mAdvertiser;
+ private final Map<String, BluetoothGattServerHelper> mBluetoothGattServerHelpers =
+ new HashMap<>();
+ private CountDownLatch mIsDiscoverableLatch = new CountDownLatch(1);
+ private ScheduledFuture<?> mRevertDiscoverableFuture;
+ private boolean mShouldFailPairing = false;
+ private boolean mIsDestroyed = false;
+ private boolean mIsAdvertising;
+ @Nullable
+ private String mBleAddress;
+ private BluetoothDevice mPairingDevice;
+ private int mLocalPasskey;
+ private int mRemotePasskey;
+ @Nullable
+ private byte[] mSecret;
+ @Nullable
+ 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[] mOwnerAccountKey;
+ @Nullable
+ private PasskeyConfirmationCallback mPasskeyConfirmationCallback;
+ @Nullable
+ private DeviceNameCallback mDeviceNameCallback;
+ @Nullable
+ private PasskeyEventCallback mPasskeyEventCallback;
+ private final List<BatteryValue> mBatteryValues;
+ private boolean mSuppressBatteryNotification = false;
+ private boolean mSuppressSubsequentPairingNotification = false;
+ HandshakeRequest mHandshakeRequest;
+ @Nullable
+ private CountDownLatch mWriteNameCountDown;
+ private final RfcommServer mRfcommServer = new RfcommServer();
+ 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.mShouldFailPairing = shouldFailPairing;
+ }
+
+ /** Gets the flag for failing paring for debug purpose. */
+ public boolean getShouldFailPairing() {
+ return mShouldFailPairing;
+ }
+
+ /** Clear the battery values, then no battery information is packed when advertising. */
+ public void clearBatteryValues() {
+ mBatteryValues.clear();
+ }
+
+ /** Sets the battery items which will be included in the advertisement packet. */
+ public void setBatteryValues(BatteryValue... 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.mSuppressBatteryNotification = suppressBatteryNotification;
+ }
+
+ /** Sets whether the account key data is within suppress type or not. */
+ public void setSuppressSubsequentPairingNotification(boolean isSuppress) {
+ mSuppressSubsequentPairingNotification = isSuppress;
+ }
+
+ /** Calls this to start advertising after some values are changed. */
+ public void startAdvertising() {
+ 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 && !mUseLogFullEvent) {
+ mRfcommServer.sendFakeEventStreamLoggingMessage(
+ getDeviceName()
+ + " "
+ + getBleAddress()
+ + " send log at "
+ + new SimpleDateFormat("HH:mm:ss:SSS", Locale.US)
+ .format(Calendar.getInstance().getTime()));
+ } else {
+ mRfcommServer.sendFakeEventStreamMessage(eventGroup);
+ }
+ }
+
+ public void setUseLogFullEvent(boolean useLogFullEvent) {
+ this.mUseLogFullEvent = useLogFullEvent;
+ }
+
+ /** An optional way to get advertising status updates. */
+ public interface AdvertisingChangedCallback {
+ /**
+ * Called when we change our BLE advertisement.
+ *
+ * @param isAdvertising the advertising status.
+ */
+ void onAdvertisingChanged(boolean isAdvertising);
+ }
+
+ /** A way for tests to get callbacks when passkey confirmation is invoked. */
+ public interface PasskeyConfirmationCallback {
+ void onPasskeyConfirmation(boolean confirm);
+ }
+
+ /** A way for simulator UI update to get callback when device name is changed. */
+ public interface DeviceNameCallback {
+ void onNameChanged(String deviceName);
+ }
+
+ /**
+ * Callback when there comes a passkey input request from BT service, or receiving remote
+ * device's passkey.
+ */
+ public interface PasskeyEventCallback {
+ void onPasskeyRequested(KeyInputCallback keyInputCallback);
+
+ void onRemotePasskeyReceived(int passkey);
+
+ default void onPasskeyConfirmation(int passkey, Consumer<Boolean> isConfirmed) {
+ }
+ }
+
+ /** Options for the simulator. */
+ public static class Options {
+ private final String mModelId;
+
+ // TODO(b/143117318):Remove this when app-launch type has its own anti-spoofing key.
+ private final String mAdvertisingModelId;
+
+ @Nullable
+ private final String mBluetoothAddress;
+
+ @Nullable
+ private final String mBleAddress;
+
+ private final boolean mDataOnlyConnection;
+
+ private final int mTxPowerLevel;
+
+ private final boolean mEnableNameCharacteristic;
+
+ private final AdvertisingChangedCallback mAdvertisingChangedCallback;
+
+ private final boolean mIncludeTransportDataDescriptor;
+
+ @Nullable
+ private final byte[] mAntiSpoofingPrivateKey;
+
+ private final boolean mUseRandomSaltForAccountKeyRotation;
+
+ private final boolean mBecomeDiscoverable;
+
+ private final boolean mShowsPasskeyConfirmation;
+
+ private final boolean mEnableBeaconActionsCharacteristic;
+
+ private final boolean mRemoveAllDevicesDuringPairing;
+
+ @Nullable
+ private final ByteString mEddystoneIdentityKey;
+
+ private Options(
+ String modelId,
+ String advertisingModelId,
+ @Nullable String bluetoothAddress,
+ @Nullable String bleAddress,
+ boolean dataOnlyConnection,
+ int txPowerLevel,
+ boolean enableNameCharacteristic,
+ AdvertisingChangedCallback advertisingChangedCallback,
+ boolean includeTransportDataDescriptor,
+ @Nullable byte[] antiSpoofingPrivateKey,
+ boolean useRandomSaltForAccountKeyRotation,
+ boolean becomeDiscoverable,
+ boolean showsPasskeyConfirmation,
+ boolean enableBeaconActionsCharacteristic,
+ boolean removeAllDevicesDuringPairing,
+ @Nullable ByteString eddystoneIdentityKey) {
+ this.mModelId = modelId;
+ this.mAdvertisingModelId = advertisingModelId;
+ this.mBluetoothAddress = bluetoothAddress;
+ this.mBleAddress = bleAddress;
+ this.mDataOnlyConnection = dataOnlyConnection;
+ this.mTxPowerLevel = txPowerLevel;
+ this.mEnableNameCharacteristic = enableNameCharacteristic;
+ this.mAdvertisingChangedCallback = advertisingChangedCallback;
+ this.mIncludeTransportDataDescriptor = includeTransportDataDescriptor;
+ this.mAntiSpoofingPrivateKey = antiSpoofingPrivateKey;
+ this.mUseRandomSaltForAccountKeyRotation = useRandomSaltForAccountKeyRotation;
+ this.mBecomeDiscoverable = becomeDiscoverable;
+ this.mShowsPasskeyConfirmation = showsPasskeyConfirmation;
+ this.mEnableBeaconActionsCharacteristic = enableBeaconActionsCharacteristic;
+ this.mRemoveAllDevicesDuringPairing = removeAllDevicesDuringPairing;
+ this.mEddystoneIdentityKey = eddystoneIdentityKey;
+ }
+
+ public String getModelId() {
+ return mModelId;
+ }
+
+ // TODO(b/143117318):Remove this when app-launch type has its own anti-spoofing key.
+ public String getAdvertisingModelId() {
+ return mAdvertisingModelId;
+ }
+
+ @Nullable
+ public String getBluetoothAddress() {
+ return mBluetoothAddress;
+ }
+
+ @Nullable
+ public String getBleAddress() {
+ return mBleAddress;
+ }
+
+ public boolean getDataOnlyConnection() {
+ return mDataOnlyConnection;
+ }
+
+ public int getTxPowerLevel() {
+ return mTxPowerLevel;
+ }
+
+ public boolean getEnableNameCharacteristic() {
+ return mEnableNameCharacteristic;
+ }
+
+ public AdvertisingChangedCallback getAdvertisingChangedCallback() {
+ return mAdvertisingChangedCallback;
+ }
+
+ public boolean getIncludeTransportDataDescriptor() {
+ return mIncludeTransportDataDescriptor;
+ }
+
+ @Nullable
+ public byte[] getAntiSpoofingPrivateKey() {
+ return mAntiSpoofingPrivateKey;
+ }
+
+ public boolean getUseRandomSaltForAccountKeyRotation() {
+ return mUseRandomSaltForAccountKeyRotation;
+ }
+
+ public boolean getBecomeDiscoverable() {
+ return mBecomeDiscoverable;
+ }
+
+ public boolean getShowsPasskeyConfirmation() {
+ return mShowsPasskeyConfirmation;
+ }
+
+ public boolean getEnableBeaconActionsCharacteristic() {
+ return mEnableBeaconActionsCharacteristic;
+ }
+
+ public boolean getRemoveAllDevicesDuringPairing() {
+ return mRemoveAllDevicesDuringPairing;
+ }
+
+ @Nullable
+ public ByteString getEddystoneIdentityKey() {
+ return mEddystoneIdentityKey;
+ }
+
+ /** Converts an instance to a builder. */
+ public Builder toBuilder() {
+ return new Options.Builder(this);
+ }
+
+ /** Constructs a builder. */
+ public static Builder builder() {
+ return new Options.Builder();
+ }
+
+ /** @param modelId Must be a 3-byte hex string. */
+ public static Builder builder(String modelId) {
+ return new Options.Builder()
+ .setModelId(Ascii.toUpperCase(modelId))
+ .setAdvertisingModelId(Ascii.toUpperCase(modelId))
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
+ .setAdvertisingChangedCallback(isAdvertising -> {
+ })
+ .setIncludeTransportDataDescriptor(true)
+ .setUseRandomSaltForAccountKeyRotation(false)
+ .setEnableNameCharacteristic(true)
+ .setDataOnlyConnection(false)
+ .setBecomeDiscoverable(true)
+ .setShowsPasskeyConfirmation(false)
+ .setEnableBeaconActionsCharacteristic(true)
+ .setRemoveAllDevicesDuringPairing(true);
+ }
+
+ /** A builder for {@link Options}. */
+ public static class Builder {
+
+ private String mModelId;
+
+ // TODO(b/143117318):Remove this when app-launch type has its own anti-spoofing key.
+ private String mAdvertisingModelId;
+
+ @Nullable
+ private String mBluetoothAddress;
+
+ @Nullable
+ private String mBleAddress;
+
+ private boolean mDataOnlyConnection;
+
+ private int mTxPowerLevel;
+
+ private boolean mEnableNameCharacteristic;
+
+ private AdvertisingChangedCallback mAdvertisingChangedCallback;
+
+ private boolean mIncludeTransportDataDescriptor;
+
+ @Nullable
+ private byte[] mAntiSpoofingPrivateKey;
+
+ private boolean mUseRandomSaltForAccountKeyRotation;
+
+ private boolean mBecomeDiscoverable;
+
+ private boolean mShowsPasskeyConfirmation;
+
+ private boolean mEnableBeaconActionsCharacteristic;
+
+ private boolean mRemoveAllDevicesDuringPairing;
+
+ @Nullable
+ private ByteString mEddystoneIdentityKey;
+
+ private Builder() {
+ }
+
+ private Builder(Options option) {
+ this.mModelId = option.mModelId;
+ this.mAdvertisingModelId = option.mAdvertisingModelId;
+ this.mBluetoothAddress = option.mBluetoothAddress;
+ this.mBleAddress = option.mBleAddress;
+ this.mDataOnlyConnection = option.mDataOnlyConnection;
+ this.mTxPowerLevel = option.mTxPowerLevel;
+ this.mEnableNameCharacteristic = option.mEnableNameCharacteristic;
+ this.mAdvertisingChangedCallback = option.mAdvertisingChangedCallback;
+ this.mIncludeTransportDataDescriptor = option.mIncludeTransportDataDescriptor;
+ this.mAntiSpoofingPrivateKey = option.mAntiSpoofingPrivateKey;
+ this.mUseRandomSaltForAccountKeyRotation =
+ option.mUseRandomSaltForAccountKeyRotation;
+ this.mBecomeDiscoverable = option.mBecomeDiscoverable;
+ this.mShowsPasskeyConfirmation = option.mShowsPasskeyConfirmation;
+ this.mEnableBeaconActionsCharacteristic = option.mEnableBeaconActionsCharacteristic;
+ this.mRemoveAllDevicesDuringPairing = option.mRemoveAllDevicesDuringPairing;
+ this.mEddystoneIdentityKey = option.mEddystoneIdentityKey;
+ }
+
+ /**
+ * Must be one of the {@code ADVERTISE_TX_POWER_*} levels in {@link AdvertiseSettings}.
+ * Default is HIGH.
+ */
+ public Builder setTxPowerLevel(int txPowerLevel) {
+ this.mTxPowerLevel = txPowerLevel;
+ return this;
+ }
+
+ /**
+ * Must be a 6-byte hex string (optionally with colons).
+ * Default is this device's BT MAC.
+ */
+ public Builder setBluetoothAddress(@Nullable String bluetoothAddress) {
+ this.mBluetoothAddress = bluetoothAddress;
+ return this;
+ }
+
+ public Builder setBleAddress(@Nullable String bleAddress) {
+ this.mBleAddress = bleAddress;
+ return this;
+ }
+
+ /** A boolean to decide if enable name characteristic as simulator characteristic. */
+ public Builder setEnableNameCharacteristic(boolean enable) {
+ this.mEnableNameCharacteristic = enable;
+ return this;
+ }
+
+ /** @see AdvertisingChangedCallback */
+ public Builder setAdvertisingChangedCallback(
+ AdvertisingChangedCallback advertisingChangedCallback) {
+ this.mAdvertisingChangedCallback = advertisingChangedCallback;
+ return this;
+ }
+
+ public Builder setDataOnlyConnection(boolean dataOnlyConnection) {
+ this.mDataOnlyConnection = dataOnlyConnection;
+ return this;
+ }
+
+ /**
+ * Set whether to include the Transport Data descriptor, which has the list of supported
+ * profiles. This is required by the spec, but if we can't get it, we recover gracefully
+ * by assuming support for one of {A2DP, Headset}. Default is true.
+ */
+ public Builder setIncludeTransportDataDescriptor(
+ boolean includeTransportDataDescriptor) {
+ this.mIncludeTransportDataDescriptor = includeTransportDataDescriptor;
+ return this;
+ }
+
+ public Builder setAntiSpoofingPrivateKey(@Nullable byte[] antiSpoofingPrivateKey) {
+ this.mAntiSpoofingPrivateKey = antiSpoofingPrivateKey;
+ return this;
+ }
+
+ public Builder setUseRandomSaltForAccountKeyRotation(
+ boolean useRandomSaltForAccountKeyRotation) {
+ this.mUseRandomSaltForAccountKeyRotation = useRandomSaltForAccountKeyRotation;
+ return this;
+ }
+
+ // TODO(b/143117318):Remove this when app-launch type has its own anti-spoofing key.
+ public Builder setAdvertisingModelId(String modelId) {
+ this.mAdvertisingModelId = modelId;
+ return this;
+ }
+
+ public Builder setBecomeDiscoverable(boolean becomeDiscoverable) {
+ this.mBecomeDiscoverable = becomeDiscoverable;
+ return this;
+ }
+
+ public Builder setShowsPasskeyConfirmation(boolean showsPasskeyConfirmation) {
+ this.mShowsPasskeyConfirmation = showsPasskeyConfirmation;
+ return this;
+ }
+
+ public Builder setEnableBeaconActionsCharacteristic(
+ boolean enableBeaconActionsCharacteristic) {
+ this.mEnableBeaconActionsCharacteristic = enableBeaconActionsCharacteristic;
+ return this;
+ }
+
+ public Builder setRemoveAllDevicesDuringPairing(boolean removeAllDevicesDuringPairing) {
+ this.mRemoveAllDevicesDuringPairing = removeAllDevicesDuringPairing;
+ return this;
+ }
+
+ /**
+ * Non-public because this is required to create a builder. See
+ * {@link Options#builder}.
+ */
+ public Builder setModelId(String modelId) {
+ this.mModelId = modelId;
+ return this;
+ }
+
+ public Builder setEddystoneIdentityKey(@Nullable ByteString eddystoneIdentityKey) {
+ this.mEddystoneIdentityKey = eddystoneIdentityKey;
+ return this;
+ }
+
+ // Custom builder in order to normalize properties. go/autovalue/builders-howto
+ public Options build() {
+ return new Options(
+ Ascii.toUpperCase(mModelId),
+ Ascii.toUpperCase(mAdvertisingModelId),
+ mBluetoothAddress,
+ mBleAddress,
+ mDataOnlyConnection,
+ mTxPowerLevel,
+ mEnableNameCharacteristic,
+ mAdvertisingChangedCallback,
+ mIncludeTransportDataDescriptor,
+ mAntiSpoofingPrivateKey,
+ mUseRandomSaltForAccountKeyRotation,
+ mBecomeDiscoverable,
+ mShowsPasskeyConfirmation,
+ mEnableBeaconActionsCharacteristic,
+ mRemoveAllDevicesDuringPairing,
+ mEddystoneIdentityKey);
+ }
+ }
+ }
+
+ public FastPairSimulator(Context context, Options options) {
+ this(context, options, new Logger(TAG));
+ }
+
+ 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())
+ ? options.getBluetoothAddress()
+ : Settings.Secure.getString(context.getContentResolver(),
+ "bluetooth_address");
+ 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 = mBluetoothAdapter.getAddress();
+ }
+ this.mBluetoothAddress =
+ new Value(BluetoothAddress.decode(bluetoothAddress), ByteOrder.BIG_ENDIAN);
+ this.mBleAddress = options.getBleAddress();
+ this.mAdvertiser = new OreoFastPairAdvertiser(this);
+
+ mFastPairSimulatorDatabase = new FastPairSimulatorDatabase(context);
+
+ byte[] deviceName = getDeviceNameInBytes();
+ mLogger.log(
+ "Provider default device name is %s",
+ deviceName != null ? new String(deviceName, StandardCharsets.UTF_8) : null);
+
+ if (mOptions.getDataOnlyConnection()) {
+ // To get BLE address, we need to start advertising first, and then
+ // {@code#setBleAddress} will be called with BLE address.
+ 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(mBleAddress != null ? mBleAddress : bluetoothAddress);
+ }
+ }
+
+ public void start(String address) {
+ IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
+ filter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST);
+ filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
+ filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
+ mContext.registerReceiver(mBroadcastReceiver, filter);
+
+ BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class);
+ BluetoothGattServerHelper bluetoothGattServerHelper =
+ new BluetoothGattServerHelper(mContext, wrap(bluetoothManager));
+ mBluetoothGattServerHelpers.put(address, bluetoothGattServerHelper);
+
+ if (mOptions.getBecomeDiscoverable()) {
+ try {
+ becomeDiscoverable();
+ } catch (InterruptedException | TimeoutException e) {
+ mLogger.log(e, "Error becoming discoverable");
+ }
+ }
+
+ mAdvertiser.startAdvertising(modelIdServiceData(/* forAdvertising= */ true));
+ startGattServer(bluetoothGattServerHelper);
+ startRfcommServer();
+ scheduleAdvertisingRefresh();
+ }
+
+ /**
+ * Regenerate service data on a fixed interval.
+ * This causes the bloom filter to be refreshed and a different salt to be used for rotation.
+ */
+ @SuppressWarnings("FutureReturnValueIgnored")
+ private void scheduleAdvertisingRefresh() {
+ mExecutor.scheduleAtFixedRate(() -> {
+ if (mIsAdvertising) {
+ mAdvertiser.startAdvertising(getServiceData());
+ }
+ }, ADVERTISING_REFRESH_DELAY_1_MIN, ADVERTISING_REFRESH_DELAY_1_MIN, TimeUnit.MILLISECONDS);
+ }
+
+ public void destroy() {
+ try {
+ mLogger.log("Destroying simulator");
+ mIsDestroyed = true;
+ mContext.unregisterReceiver(mBroadcastReceiver);
+ mAdvertiser.stopAdvertising();
+ for (BluetoothGattServerHelper helper : mBluetoothGattServerHelpers.values()) {
+ helper.close();
+ }
+ stopRfcommServer();
+ 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 mIsDestroyed;
+ }
+
+ @Nullable
+ public String getBluetoothAddress() {
+ return BluetoothAddress.encode(mBluetoothAddress.getBytes(ByteOrder.BIG_ENDIAN));
+ }
+
+ public boolean isAdvertising() {
+ return mIsAdvertising;
+ }
+
+ public void setIsAdvertising(boolean isAdvertising) {
+ if (this.mIsAdvertising != isAdvertising) {
+ this.mIsAdvertising = isAdvertising;
+ mOptions.getAdvertisingChangedCallback().onAdvertisingChanged(isAdvertising);
+ }
+ }
+
+ public void stopAdvertising() {
+ mAdvertiser.stopAdvertising();
+ }
+
+ public void setBleAddress(String bleAddress) {
+ this.mBleAddress = bleAddress;
+ if (mOptions.getDataOnlyConnection()) {
+ mBluetoothAddress = new Value(BluetoothAddress.decode(bleAddress),
+ ByteOrder.BIG_ENDIAN);
+ start(bleAddress);
+ }
+ // When BLE address changes, needs to send BLE address to the client again.
+ sendDeviceBleAddress(bleAddress);
+
+ // 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()) {
+ mAdvertiser.startAdvertising(getServiceData());
+ }
+ }
+
+ @Nullable
+ public String getBleAddress() {
+ 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) {
+ mLogger.log("Set up count down latch to write device name.");
+ mWriteNameCountDown = countDownLatch;
+ }
+
+ public boolean areBeaconActionsNotificationsEnabled() {
+ return mBeaconActionsServlet.areNotificationsEnabled();
+ }
+
+ private abstract class NotifiableGattServlet extends BluetoothGattServlet {
+ private final Map<BluetoothGattServerConnection, Notifier> mConnections = new HashMap<>();
+
+ abstract BluetoothGattCharacteristic getBaseCharacteristic();
+
+ @Override
+ public BluetoothGattCharacteristic getCharacteristic() {
+ // Enabling indication requires the Client Characteristic Configuration descriptor.
+ BluetoothGattCharacteristic characteristic = getBaseCharacteristic();
+ characteristic.addDescriptor(
+ new BluetoothGattDescriptor(
+ Constants.CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID,
+ BluetoothGattDescriptor.PERMISSION_READ
+ | BluetoothGattDescriptor.PERMISSION_WRITE));
+ return characteristic;
+ }
+
+ @Override
+ public void enableNotification(BluetoothGattServerConnection connection, Notifier notifier)
+ throws BluetoothGattException {
+ mLogger.log("Registering notifier for %s", getCharacteristic());
+ mConnections.put(connection, notifier);
+ }
+
+ @Override
+ public void disableNotification(BluetoothGattServerConnection connection, Notifier notifier)
+ throws BluetoothGattException {
+ mLogger.log("Removing notifier for %s", getCharacteristic());
+ mConnections.remove(connection);
+ }
+
+ boolean areNotificationsEnabled() {
+ return !mConnections.isEmpty();
+ }
+
+ void sendNotification(byte[] data) {
+ 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).
+ mExecutor.execute(
+ () -> {
+ for (Map.Entry<BluetoothGattServerConnection, Notifier> entry :
+ mConnections.entrySet()) {
+ try {
+ mLogger.log("Sending notify %s to %s",
+ getCharacteristic(),
+ entry.getKey().getDevice().getAddress());
+ entry.getValue().notify(data);
+ } catch (BluetoothException e) {
+ mLogger.log(
+ e,
+ "Failed to notify (indicate) result of %s to %s",
+ getCharacteristic(),
+ entry.getKey().getDevice().getAddress());
+ }
+ }
+ });
+ }
+ }
+
+ private void startRfcommServer() {
+ mRfcommServer.setRequestHandler(this::handleRfcommServerRequest);
+ mRfcommServer.setStateMonitor(state -> {
+ mLogger.log("RfcommServer is in %s state", state);
+ if (CONNECTED.equals(state)) {
+ sendModelId();
+ sendDeviceBleAddress(mBleAddress);
+ sendFirmwareVersion();
+ sendSessionNonce();
+ }
+ });
+ mRfcommServer.start();
+ }
+
+ private void handleRfcommServerRequest(int eventGroup, int eventCode, byte[] data) {
+ switch (eventGroup) {
+ case EventGroup.DEVICE_VALUE:
+ if (data == null) {
+ break;
+ }
+
+ String deviceValue = base16().encode(data);
+ if (eventCode == DeviceEventCode.DEVICE_CAPABILITY_VALUE) {
+ mLogger.log("Received phone capability: %s", deviceValue);
+ } else if (eventCode == DeviceEventCode.PLATFORM_TYPE_VALUE) {
+ mLogger.log("Received platform type: %s", deviceValue);
+ }
+ break;
+ case EventGroup.DEVICE_ACTION_VALUE:
+ if (eventCode == DeviceActionEventCode.DEVICE_ACTION_RING_VALUE) {
+ 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).
+ mUiThreadHandler.postDelayed(this::sendDeviceRingStoppedAction,
+ 5000);
+ }
+ break;
+ case EventGroup.DEVICE_CONFIGURATION_VALUE:
+ if (eventCode == DeviceConfigurationEventCode.CONFIGURATION_BUFFER_SIZE_VALUE) {
+ mLogger.log(
+ "receive device action with buffer size value, data = %s",
+ base16().encode(data));
+ sendSetBufferActionResponse(data);
+ }
+ break;
+ case EventGroup.DEVICE_CAPABILITY_SYNC_VALUE:
+ if (eventCode == DeviceCapabilitySyncEventCode.REQUEST_CAPABILITY_UPDATE_VALUE) {
+ mLogger.log("receive device capability update request.");
+ sendCapabilitySync();
+ }
+ break;
+ default: // fall out
+ break;
+ }
+ }
+
+ private void stopRfcommServer() {
+ mRfcommServer.stop();
+ mRfcommServer.setRequestHandler(null);
+ mRfcommServer.setStateMonitor(null);
+ }
+
+ private void sendModelId() {
+ 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) {
+ mLogger.log("Send BLE address (%s) to the client", bleAddress);
+ if (bleAddress != null) {
+ mRfcommServer.send(
+ EventGroup.DEVICE_VALUE,
+ DeviceEventCode.DEVICE_BLE_ADDRESS_VALUE,
+ BluetoothAddress.decode(bleAddress));
+ }
+ }
+
+ private void sendFirmwareVersion() {
+ mLogger.log("Send Firmware Version (%s) to the client", mDeviceFirmwareVersion);
+ mRfcommServer.send(
+ EventGroup.DEVICE_VALUE,
+ DeviceEventCode.FIRMWARE_VERSION_VALUE,
+ mDeviceFirmwareVersion.getBytes());
+ }
+
+ private void sendSessionNonce() {
+ mLogger.log("Send SessionNonce (%s) to the client", mDeviceFirmwareVersion);
+ SecureRandom secureRandom = new SecureRandom();
+ mSessionNonce = new byte[SECTION_NONCE_LENGTH];
+ secureRandom.nextBytes(mSessionNonce);
+ mRfcommServer.send(
+ EventGroup.DEVICE_VALUE, DeviceEventCode.SECTION_NONCE_VALUE, mSessionNonce);
+ }
+
+ private void sendDeviceRingActionResponse() {
+ mLogger.log("Send device ring action response to the client");
+ mRfcommServer.send(
+ EventGroup.ACKNOWLEDGEMENT_VALUE,
+ AcknowledgementEventCode.ACKNOWLEDGEMENT_ACK_VALUE,
+ new byte[]{
+ EventGroup.DEVICE_ACTION_VALUE,
+ DeviceActionEventCode.DEVICE_ACTION_RING_VALUE
+ });
+ }
+
+ private void sendSetBufferActionResponse(byte[] data) {
+ boolean hmacPassed = false;
+ for (ByteString accountKey : getAccountKeys()) {
+ try {
+ if (MessageStreamHmacEncoder.verifyHmac(
+ accountKey.toByteArray(), mSessionNonce, data)) {
+ hmacPassed = true;
+ mLogger.log("Buffer size data matches account key %s",
+ base16().encode(accountKey.toByteArray()));
+ break;
+ }
+ } catch (GeneralSecurityException e) {
+ // Ignore.
+ }
+ }
+ if (hmacPassed) {
+ 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[]{
+ EventGroup.DEVICE_CONFIGURATION_VALUE,
+ DeviceConfigurationEventCode.CONFIGURATION_BUFFER_SIZE_VALUE,
+ data[0],
+ data[1],
+ data[2]
+ });
+ } else {
+ mLogger.log("No matched account key for sendSetBufferActionResponse");
+ }
+ }
+
+ private void sendCapabilitySync() {
+ 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[]{
+ 0x00, 0x01, (byte) 0xf4, 0x00, 0x64, 0x00, (byte) 0xc8,
+ 0x01, 0x00, (byte) 0xff, 0x00, 0x01, 0x00, (byte) 0x88,
+ 0x02, 0x01, (byte) 0xff, 0x01, 0x01, 0x01, (byte) 0x88,
+ 0x03, 0x02, (byte) 0xff, 0x02, 0x01, 0x02, (byte) 0x88,
+ 0x04, 0x03, (byte) 0xff, 0x03, 0x01, 0x03, (byte) 0x88
+ });
+ }
+ }
+
+ private void sendDeviceRingStoppedAction() {
+ 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.
+ new byte[]{0x00});
+ }
+
+ private void startGattServer(BluetoothGattServerHelper helper) {
+ BluetoothGattServlet tdsControlPointServlet =
+ new NotifiableGattServlet() {
+ @Override
+ public BluetoothGattCharacteristic getBaseCharacteristic() {
+ return new BluetoothGattCharacteristic(ControlPointCharacteristic.ID,
+ PROPERTY_WRITE | PROPERTY_INDICATE, PERMISSION_WRITE);
+ }
+
+ @Override
+ public void write(
+ BluetoothGattServerConnection connection, int offset, byte[] value)
+ throws BluetoothGattException {
+ mLogger.log("Requested TDS Control Point write, value=%s",
+ base16().encode(value));
+
+ ResultCode resultCode = checkTdsControlPointRequest(value);
+ if (resultCode == ResultCode.SUCCESS) {
+ try {
+ becomeDiscoverable();
+ } catch (TimeoutException | InterruptedException e) {
+ mLogger.log(e, "Failed to become discoverable");
+ resultCode = ResultCode.OPERATION_FAILED;
+ }
+ }
+
+ mLogger.log("Request complete, resultCode=%s", resultCode);
+
+ mLogger.log("Sending TDS Control Point response indication");
+ sendNotification(
+ Bytes.concat(
+ new byte[]{
+ getTdsControlPointOpCode(value),
+ resultCode.mByteValue,
+ },
+ resultCode == ResultCode.SUCCESS
+ ? TDS_CONTROL_POINT_RESPONSE_PARAMETER
+ : new byte[0]));
+ }
+ };
+
+ BluetoothGattServlet brHandoverDataServlet =
+ new BluetoothGattServlet() {
+
+ @Override
+ public BluetoothGattCharacteristic getCharacteristic() {
+ return new BluetoothGattCharacteristic(BrHandoverDataCharacteristic.ID,
+ PROPERTY_READ, PERMISSION_READ);
+ }
+
+ @Override
+ public byte[] read(BluetoothGattServerConnection connection, int offset) {
+ return Bytes.concat(
+ new byte[]{BrHandoverDataCharacteristic.BR_EDR_FEATURES},
+ mBluetoothAddress.getBytes(ByteOrder.LITTLE_ENDIAN),
+ CLASS_OF_DEVICE.getBytes(ByteOrder.LITTLE_ENDIAN));
+ }
+ };
+
+ BluetoothGattServlet bluetoothSigServlet =
+ new BluetoothGattServlet() {
+
+ @Override
+ public BluetoothGattCharacteristic getCharacteristic() {
+ BluetoothGattCharacteristic characteristic =
+ new BluetoothGattCharacteristic(
+ TransportDiscoveryService.BluetoothSigDataCharacteristic.ID,
+ 0 /* no properties */,
+ 0 /* no permissions */);
+
+ if (mOptions.getIncludeTransportDataDescriptor()) {
+ characteristic.addDescriptor(
+ new BluetoothGattDescriptor(
+ TransportDiscoveryService.BluetoothSigDataCharacteristic
+ .BrTransportBlockDataDescriptor.ID,
+ BluetoothGattDescriptor.PERMISSION_READ));
+ }
+ return characteristic;
+ }
+
+ @Override
+ public byte[] readDescriptor(
+ BluetoothGattServerConnection connection,
+ BluetoothGattDescriptor descriptor,
+ int offset)
+ throws BluetoothGattException {
+ return transportDiscoveryData();
+ }
+ };
+
+ BluetoothGattServlet accountKeyServlet =
+ new BluetoothGattServlet() {
+ @Override
+ // Simulating deprecated API {@code AccountKeyCharacteristic.ID} for testing.
+ @SuppressWarnings("deprecation")
+ public BluetoothGattCharacteristic getCharacteristic() {
+ return new BluetoothGattCharacteristic(
+ AccountKeyCharacteristic.CUSTOM_128_BIT_UUID,
+ PROPERTY_WRITE,
+ PERMISSION_WRITE);
+ }
+
+ @Override
+ public void write(
+ BluetoothGattServerConnection connection, int offset, byte[] value) {
+ mLogger.log("Got value from account key servlet: %s",
+ base16().encode(value));
+ try {
+ addAccountKey(AesEcbSingleBlockEncryption.decrypt(mSecret, value),
+ mPairingDevice);
+ } catch (GeneralSecurityException e) {
+ mLogger.log(e, "Failed to decrypt account key.");
+ }
+ mUiThreadHandler.post(
+ () -> mAdvertiser.startAdvertising(accountKeysServiceData()));
+ }
+ };
+
+ BluetoothGattServlet firmwareVersionServlet =
+ new BluetoothGattServlet() {
+ @Override
+ public BluetoothGattCharacteristic getCharacteristic() {
+ return new BluetoothGattCharacteristic(
+ FirmwareVersionCharacteristic.ID, PROPERTY_READ, PERMISSION_READ);
+ }
+
+ @Override
+ public byte[] read(BluetoothGattServerConnection connection, int offset) {
+ return mDeviceFirmwareVersion.getBytes();
+ }
+ };
+
+ BluetoothGattServlet keyBasedPairingServlet =
+ new NotifiableGattServlet() {
+ @Override
+ // Simulating deprecated API {@code KeyBasedPairingCharacteristic.ID} for
+ // testing.
+ @SuppressWarnings("deprecation")
+ public BluetoothGattCharacteristic getBaseCharacteristic() {
+ return new BluetoothGattCharacteristic(
+ KeyBasedPairingCharacteristic.CUSTOM_128_BIT_UUID,
+ PROPERTY_WRITE | PROPERTY_INDICATE,
+ PERMISSION_WRITE);
+ }
+
+ @Override
+ public void write(
+ BluetoothGattServerConnection connection, int offset, byte[] value) {
+ mLogger.log("Requesting key based pairing handshake, value=%s",
+ base16().encode(value));
+
+ mSecret = null;
+ byte[] seekerPublicAddress = null;
+ if (value.length == AES_BLOCK_LENGTH) {
+
+ for (ByteString key : getAccountKeys()) {
+ byte[] candidateSecret = key.toByteArray();
+ try {
+ seekerPublicAddress = handshake(candidateSecret, value);
+ mSecret = candidateSecret;
+ mIsSubsequentPair = true;
+ break;
+ } catch (GeneralSecurityException e) {
+ mLogger.log(e, "Failed to decrypt with %s",
+ base16().encode(candidateSecret));
+ }
+ }
+ } else if (value.length == AES_BLOCK_LENGTH + PUBLIC_KEY_LENGTH
+ && 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(
+ mOptions.getAntiSpoofingPrivateKey())
+ .generateSecret(receivedPublicKey);
+ seekerPublicAddress = handshake(candidateSecret, encryptedRequest);
+ mSecret = candidateSecret;
+ } catch (Exception e) {
+ mLogger.log(
+ e,
+ "Failed to decrypt with anti-spoofing private key %s",
+ base16().encode(mOptions.getAntiSpoofingPrivateKey()));
+ }
+ } else {
+ mLogger.log("Packet length invalid, %d", value.length);
+ return;
+ }
+
+ if (mSecret == null) {
+ mLogger.log("Couldn't find a usable key to decrypt with.");
+ return;
+ }
+
+ 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},
+ 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 (mOptions.getEnableNameCharacteristic()
+ && mHandshakeRequest.requestDeviceName()) {
+ byte[] encryptedResponse =
+ getDeviceNameInBytes() != null ? createEncryptedDeviceName()
+ : new byte[0];
+ mLogger.log(
+ "Sending device name response %s with size %d",
+ base16().encode(encryptedResponse),
+ encryptedResponse.length);
+ mDeviceNameServlet.sendNotification(encryptedResponse);
+ }
+
+ // Disconnects the current connection to allow the following pairing
+ // request. Needs to be on a separate thread to avoid deadlocking and
+ // timing out (waits for a callback from OS, which happens on this
+ // thread).
+ //
+ // Note: The spec does not require you to disconnect from other
+ // devices at this point.
+ // 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.
+ mLogger.log("Skip remove bond, value=%s",
+ mOptions.getRemoveAllDevicesDuringPairing());
+ if (mOptions.getRemoveAllDevicesDuringPairing()
+ && mHandshakeRequest.getType()
+ == HandshakeRequest.Type.KEY_BASED_PAIRING_REQUEST
+ && !mHandshakeRequest.requestRetroactivePair()) {
+ mExecutor.execute(() -> disconnectAllBondedDevices());
+ }
+
+ if (mHandshakeRequest.getType()
+ == HandshakeRequest.Type.KEY_BASED_PAIRING_REQUEST
+ && 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);
+ mExecutor.execute(() -> {
+ mLogger.log("Sending pairing request to %s",
+ seekerPublicAddressString);
+ mBluetoothAdapter.getRemoteDevice(
+ seekerPublicAddressString).createBond();
+ });
+ }
+ } catch (GeneralSecurityException e) {
+ mLogger.log(e, "Failed to notify of static mac address");
+ }
+ }
+
+ @Nullable
+ private byte[] handshake(byte[] key, byte[] encryptedPairingRequest)
+ throws GeneralSecurityException {
+ mHandshakeRequest = new HandshakeRequest(key, encryptedPairingRequest);
+
+ byte[] decryptedAddress = mHandshakeRequest.getVerificationData();
+ if (mBleAddress != null
+ && Arrays.equals(decryptedAddress,
+ BluetoothAddress.decode(mBleAddress))
+ || Arrays.equals(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)
+ + ", "
+ + mBleAddress
+ + ", "
+ + getBluetoothAddress());
+ }
+
+ switch (mHandshakeRequest.getType()) {
+ case KEY_BASED_PAIRING_REQUEST:
+ return handleKeyBasedPairingRequest(mHandshakeRequest);
+ case ACTION_OVER_BLE:
+ return handleActionOverBleRequest(mHandshakeRequest);
+ case UNKNOWN:
+ // continue to throw the exception;
+ }
+ throw new GeneralSecurityException(
+ "Type is not correct: " + mHandshakeRequest.getType());
+ }
+
+ @Nullable
+ private byte[] handleKeyBasedPairingRequest(HandshakeRequest handshakeRequest)
+ throws GeneralSecurityException {
+ if (handshakeRequest.requestDiscoverable()) {
+ mLogger.log("Requested discoverability");
+ setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
+ }
+
+ mLogger.log(
+ "KeyBasedPairing: initialBonding=%s, requestDeviceName=%s, "
+ + "retroactivePair=%s",
+ handshakeRequest.requestProviderInitialBonding(),
+ handshakeRequest.requestDeviceName(),
+ handshakeRequest.requestRetroactivePair());
+
+ byte[] seekerPublicAddress = null;
+ if (handshakeRequest.requestProviderInitialBonding()
+ || handshakeRequest.requestRetroactivePair()) {
+ seekerPublicAddress = handshakeRequest.getSeekerPublicAddress();
+ mLogger.log(
+ "Seeker sends BR/EDR address %s to provider",
+ BluetoothAddress.encode(seekerPublicAddress));
+ }
+
+ if (handshakeRequest.requestRetroactivePair()) {
+ if (mBluetoothAdapter.getRemoteDevice(
+ seekerPublicAddress).getBondState()
+ != BluetoothDevice.BOND_BONDED) {
+ throw new GeneralSecurityException(
+ "Address (BR/EDR) is not bonded: "
+ + BluetoothAddress.encode(seekerPublicAddress));
+ }
+ }
+
+ return seekerPublicAddress;
+ }
+
+ @Nullable
+ private byte[] handleActionOverBleRequest(HandshakeRequest handshakeRequest) {
+ // TODO(wollohchou): implement action over ble request.
+ if (handshakeRequest.requestDeviceAction()) {
+ mLogger.log("Requesting action over BLE, device action");
+ } else if (handshakeRequest.requestFollowedByAdditionalData()) {
+ mLogger.log(
+ "Requesting action over BLE, followed by additional data, "
+ + "type:%s",
+ handshakeRequest.getAdditionalDataType());
+ } else {
+ mLogger.log("Requesting action over BLE");
+ }
+ return null;
+ }
+
+ /**
+ * @return The encrypted device name from provider for seeker to use.
+ */
+ private byte[] createEncryptedDeviceName() throws GeneralSecurityException {
+ byte[] deviceName = getDeviceNameInBytes();
+ String providerName = new String(deviceName, StandardCharsets.UTF_8);
+ mLogger.log(
+ "Sending handshake response for device name %s with size %d",
+ providerName, deviceName.length);
+ return NamingEncoder.encodeNamingPacket(mSecret, providerName);
+ }
+ };
+
+ mBeaconActionsServlet =
+ new NotifiableGattServlet() {
+ private static final int GATT_ERROR_UNAUTHENTICATED = 0x80;
+ private static final int GATT_ERROR_INVALID_VALUE = 0x81;
+ private static final int NONCE_LENGTH = 8;
+ private static final int ONE_TIME_AUTH_KEY_OFFSET = 2;
+ private static final int ONE_TIME_AUTH_KEY_LENGTH = 8;
+ private static final int IDENTITY_KEY_LENGTH = 32;
+ private static final byte TRANSMISSION_POWER = 0;
+
+ private final SecureRandom mRandom = new SecureRandom();
+ private final MessageDigest mSha256;
+ @Nullable
+ private byte[] mLastNonce;
+ @Nullable
+ private ByteString mIdentityKey = mOptions.getEddystoneIdentityKey();
+
+ {
+ try {
+ mSha256 = MessageDigest.getInstance("SHA-256");
+ mSha256.reset();
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException(
+ "System missing SHA-256 implementation.", e);
+ }
+ }
+
+ @Override
+ // Simulating deprecated API {@code BeaconActionsCharacteristic.ID} for testing.
+ @SuppressWarnings("deprecation")
+ public BluetoothGattCharacteristic getBaseCharacteristic() {
+ return new BluetoothGattCharacteristic(
+ BeaconActionsCharacteristic.CUSTOM_128_BIT_UUID,
+ PROPERTY_READ | PROPERTY_WRITE | PROPERTY_NOTIFY,
+ PERMISSION_READ | PERMISSION_WRITE);
+ }
+
+ @Override
+ public byte[] read(BluetoothGattServerConnection connection, int offset) {
+ mLastNonce = new byte[NONCE_LENGTH];
+ mRandom.nextBytes(mLastNonce);
+ return mLastNonce;
+ }
+
+ @Override
+ public void write(
+ BluetoothGattServerConnection connection, int offset, byte[] value)
+ throws BluetoothGattException {
+ mLogger.log("Got value from beacon actions servlet: %s",
+ base16().encode(value));
+ if (value.length == 0) {
+ mLogger.log("Packet length invalid, %d", value.length);
+ throw new BluetoothGattException("Packet length invalid",
+ GATT_ERROR_INVALID_VALUE);
+ }
+ switch (value[0]) {
+ case BeaconActionType.READ_BEACON_PARAMETERS:
+ handleReadBeaconParameters(value);
+ break;
+ case BeaconActionType.READ_PROVISIONING_STATE:
+ handleReadProvisioningState(value);
+ break;
+ case BeaconActionType.SET_EPHEMERAL_IDENTITY_KEY:
+ handleSetEphemeralIdentityKey(value);
+ break;
+ case BeaconActionType.CLEAR_EPHEMERAL_IDENTITY_KEY:
+ case BeaconActionType.READ_EPHEMERAL_IDENTITY_KEY:
+ case BeaconActionType.RING:
+ case BeaconActionType.READ_RINGING_STATE:
+ throw new BluetoothGattException(
+ "Unimplemented beacon action",
+ BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
+ default:
+ throw new BluetoothGattException(
+ "Unknown beacon action",
+ BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
+ }
+ }
+
+ private boolean verifyAccountKeyToken(byte[] value, boolean ownerOnly)
+ throws BluetoothGattException {
+ if (value.length < ONE_TIME_AUTH_KEY_LENGTH + ONE_TIME_AUTH_KEY_OFFSET) {
+ mLogger.log("Packet length invalid, %d", value.length);
+ throw new BluetoothGattException(
+ "Packet length invalid", GATT_ERROR_INVALID_VALUE);
+ }
+ byte[] hashedAccountKey =
+ Arrays.copyOfRange(
+ value,
+ ONE_TIME_AUTH_KEY_OFFSET,
+ ONE_TIME_AUTH_KEY_LENGTH + ONE_TIME_AUTH_KEY_OFFSET);
+ if (mLastNonce == null) {
+ throw new BluetoothGattException(
+ "Nonce wasn't set", GATT_ERROR_UNAUTHENTICATED);
+ }
+ if (ownerOnly) {
+ ByteString accountKey = getOwnerAccountKey();
+ if (accountKey != null) {
+ mSha256.update(accountKey.toByteArray());
+ mSha256.update(mLastNonce);
+ return Arrays.equals(
+ hashedAccountKey,
+ Arrays.copyOf(mSha256.digest(), ONE_TIME_AUTH_KEY_LENGTH));
+ }
+ } else {
+ Set<ByteString> accountKeys = getAccountKeys();
+ for (ByteString accountKey : accountKeys) {
+ mSha256.update(accountKey.toByteArray());
+ mSha256.update(mLastNonce);
+ if (Arrays.equals(
+ hashedAccountKey,
+ Arrays.copyOf(mSha256.digest(),
+ ONE_TIME_AUTH_KEY_LENGTH))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private int getBeaconClock() {
+ return (int) TimeUnit.MILLISECONDS.toSeconds(SystemClock.elapsedRealtime());
+ }
+
+ private ByteString fromBytes(byte... bytes) {
+ return ByteString.copyFrom(bytes);
+ }
+
+ private byte[] intToByteArray(int value) {
+ byte[] data = new byte[4];
+ data[3] = (byte) value;
+ data[2] = (byte) (value >>> 8);
+ data[1] = (byte) (value >>> 16);
+ data[0] = (byte) (value >>> 24);
+ return data;
+ }
+
+ private void handleReadBeaconParameters(byte[] value)
+ throws BluetoothGattException {
+ if (!verifyAccountKeyToken(value, /* ownerOnly= */ false)) {
+ throw new BluetoothGattException(
+ "failed to authenticate account key",
+ GATT_ERROR_UNAUTHENTICATED);
+ }
+ sendNotification(
+ fromBytes(
+ (byte) BeaconActionType.READ_BEACON_PARAMETERS,
+ (byte) 5 /* data length */,
+ TRANSMISSION_POWER)
+ .concat(ByteString.copyFrom(
+ intToByteArray(getBeaconClock())))
+ .toByteArray());
+ }
+
+ private void handleReadProvisioningState(byte[] value)
+ throws BluetoothGattException {
+ if (!verifyAccountKeyToken(value, /* ownerOnly= */ false)) {
+ throw new BluetoothGattException(
+ "failed to authenticate account key",
+ GATT_ERROR_UNAUTHENTICATED);
+ }
+ byte flags = 0;
+ if (verifyAccountKeyToken(value, /* ownerOnly= */ true)) {
+ flags |= (byte) (1 << 1);
+ }
+ if (mIdentityKey == null) {
+ sendNotification(
+ fromBytes(
+ (byte) BeaconActionType.READ_PROVISIONING_STATE,
+ (byte) 1 /* data length */,
+ flags)
+ .toByteArray());
+ } else {
+ flags |= (byte) 1;
+ sendNotification(
+ fromBytes(
+ (byte) BeaconActionType.READ_PROVISIONING_STATE,
+ (byte) 21 /* data length */,
+ flags)
+ .concat(
+ E2eeCalculator.computeE2eeEid(
+ mIdentityKey, /* exponent= */ 10,
+ getBeaconClock()))
+ .toByteArray());
+ }
+ }
+
+ private void handleSetEphemeralIdentityKey(byte[] value)
+ throws BluetoothGattException {
+ if (!verifyAccountKeyToken(value, /* ownerOnly= */ true)) {
+ throw new BluetoothGattException(
+ "failed to authenticate owner account key",
+ GATT_ERROR_UNAUTHENTICATED);
+ }
+ if (value.length
+ != ONE_TIME_AUTH_KEY_LENGTH + ONE_TIME_AUTH_KEY_OFFSET
+ + IDENTITY_KEY_LENGTH) {
+ mLogger.log("Packet length invalid, %d", value.length);
+ throw new BluetoothGattException("Packet length invalid",
+ GATT_ERROR_INVALID_VALUE);
+ }
+ if (mIdentityKey != null) {
+ throw new BluetoothGattException(
+ "Device is already provisioned as Eddystone",
+ GATT_ERROR_UNAUTHENTICATED);
+ }
+ mIdentityKey = Crypto.aesEcbNoPaddingDecrypt(
+ ByteString.copyFrom(mOwnerAccountKey),
+ ByteString.copyFrom(value)
+ .substring(ONE_TIME_AUTH_KEY_LENGTH
+ + ONE_TIME_AUTH_KEY_OFFSET));
+ }
+ };
+
+ ServiceConfig fastPairServiceConfig =
+ new ServiceConfig()
+ .addCharacteristic(accountKeyServlet)
+ .addCharacteristic(keyBasedPairingServlet)
+ .addCharacteristic(mPasskeyServlet)
+ .addCharacteristic(firmwareVersionServlet);
+ if (mOptions.getEnableBeaconActionsCharacteristic()) {
+ fastPairServiceConfig.addCharacteristic(mBeaconActionsServlet);
+ }
+
+ BluetoothGattServerConfig config =
+ new BluetoothGattServerConfig()
+ .addService(
+ TransportDiscoveryService.ID,
+ new ServiceConfig()
+ .addCharacteristic(tdsControlPointServlet)
+ .addCharacteristic(brHandoverDataServlet)
+ .addCharacteristic(bluetoothSigServlet))
+ .addService(
+ FastPairService.ID,
+ mOptions.getEnableNameCharacteristic()
+ ? fastPairServiceConfig.addCharacteristic(
+ mDeviceNameServlet)
+ : fastPairServiceConfig);
+
+ mLogger.log(
+ "Starting GATT server, support name characteristic %b",
+ mOptions.getEnableNameCharacteristic());
+ try {
+ helper.open(config);
+ } catch (BluetoothException e) {
+ mLogger.log(e, "Error starting GATT server");
+ }
+ }
+
+ /** Callback for passkey/pin input. */
+ public interface KeyInputCallback {
+ void onKeyInput(int key);
+ }
+
+ public void enterPassKey(int passkey) {
+ mLogger.log("enterPassKey called with passkey %d.", passkey);
+ mPairingDevice.setPairingConfirmation(true);
+ }
+
+ 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 (mLocalPasskey == 0 || mRemotePasskey == 0) {
+ mLogger.log(
+ "Skipping passkey check, missing local (%s) or remote (%s).",
+ mLocalPasskey, mRemotePasskey);
+ return;
+ }
+
+ // Regardless of whether it matches, send our (encrypted) passkey to the seeker.
+ sendPasskeyToRemoteDevice(mLocalPasskey);
+
+ 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);
+ }
+ }
+
+ private void sendPasskeyToRemoteDevice(int passkey) {
+ try {
+ mPasskeyServlet.sendNotification(
+ PasskeyCharacteristic.encrypt(
+ PasskeyCharacteristic.Type.PROVIDER, mSecret, passkey));
+ } catch (GeneralSecurityException e) {
+ mLogger.log(e, "Failed to encrypt passkey response.");
+ }
+ }
+
+ public void setFirmwareVersion(String versionNumber) {
+ mDeviceFirmwareVersion = versionNumber;
+ }
+
+ public void setDynamicBufferSize(boolean support) {
+ if (mSupportDynamicBufferSize != support) {
+ mSupportDynamicBufferSize = support;
+ sendCapabilitySync();
+ }
+ }
+
+ @VisibleForTesting
+ void setPasskeyConfirmationCallback(PasskeyConfirmationCallback callback) {
+ this.mPasskeyConfirmationCallback = callback;
+ }
+
+ public void setDeviceNameCallback(DeviceNameCallback callback) {
+ this.mDeviceNameCallback = callback;
+ }
+
+ public void setPasskeyEventCallback(PasskeyEventCallback passkeyEventCallback) {
+ this.mPasskeyEventCallback = passkeyEventCallback;
+ }
+
+ private void setPasskeyConfirmation(boolean confirm) {
+ mPairingDevice.setPairingConfirmation(confirm);
+ if (mPasskeyConfirmationCallback != null) {
+ mPasskeyConfirmationCallback.onPasskeyConfirmation(confirm);
+ }
+ mLocalPasskey = 0;
+ mRemotePasskey = 0;
+ }
+
+ private void becomeDiscoverable() throws InterruptedException, TimeoutException {
+ setDiscoverable(true);
+ }
+
+ public void cancelDiscovery() throws InterruptedException, TimeoutException {
+ setDiscoverable(false);
+ }
+
+ private void setDiscoverable(boolean discoverable)
+ throws InterruptedException, TimeoutException {
+ 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()) {
+ mIsDiscoverableLatch.countDown();
+ }
+ if (mIsDiscoverableLatch.await(BECOME_DISCOVERABLE_TIMEOUT_SEC, TimeUnit.SECONDS)) {
+ mLogger.log("Successfully became switched discoverable mode %s", discoverable);
+ } else {
+ throw new TimeoutException();
+ }
+ }
+
+ private void setScanMode(int scanMode) {
+ if (mRevertDiscoverableFuture != null) {
+ mRevertDiscoverableFuture.cancel(false /* may interrupt if running */);
+ }
+
+ mLogger.log("Setting scan mode to %s", scanModeToString(scanMode));
+ try {
+ Method method = mBluetoothAdapter.getClass().getMethod("setScanMode", Integer.TYPE);
+ method.invoke(mBluetoothAdapter, scanMode);
+
+ if (scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
+ mRevertDiscoverableFuture =
+ mExecutor.schedule(() -> setScanMode(SCAN_MODE_CONNECTABLE),
+ SCAN_MODE_REFRESH_SEC, TimeUnit.SECONDS);
+ }
+ } catch (Exception e) {
+ mLogger.log(e, "Error setting scan mode to %d", scanMode);
+ }
+ }
+
+ public static String scanModeToString(int scanMode) {
+ switch (scanMode) {
+ case SCAN_MODE_CONNECTABLE_DISCOVERABLE:
+ return "DISCOVERABLE";
+ case SCAN_MODE_CONNECTABLE:
+ return "CONNECTABLE";
+ case SCAN_MODE_NONE:
+ return "NOT CONNECTABLE";
+ default:
+ return "UNKNOWN(" + scanMode + ")";
+ }
+ }
+
+ private ResultCode checkTdsControlPointRequest(byte[] request) {
+ if (request.length < 2) {
+ 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) {
+ 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) {
+ mLogger.log(
+ new IllegalArgumentException(),
+ "Expected Bluetooth SIG organization ID (0x01), got %d",
+ request[1]);
+ return ResultCode.UNSUPPORTED_ORGANIZATION_ID;
+ }
+ return ResultCode.SUCCESS;
+ }
+
+ private static byte getTdsControlPointOpCode(byte[] request) {
+ return request.length < 1 ? 0x00 : request[0];
+ }
+
+ private boolean isDiscoverable() {
+ 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 ? 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);
+ }
+ return modelIdPacket;
+ }
+
+ private byte[] accountKeysServiceData() {
+ try {
+ return concat(new byte[]{0x00}, generateBloomFilterFields());
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("Unable to build bloom filter.", e);
+ }
+ }
+
+ private byte[] transportDiscoveryData() {
+ byte[] transportData = SUPPORTED_SERVICES_LTV;
+ return Bytes.concat(
+ new byte[]{BLUETOOTH_SIG_ORGANIZATION_ID},
+ new byte[]{tdsFlags(isDiscoverable() ? TransportState.ON : TransportState.OFF)},
+ new byte[]{(byte) transportData.length},
+ transportData);
+ }
+
+ private byte[] generateBloomFilterFields() throws NoSuchAlgorithmException {
+ Set<ByteString> accountKeys = getAccountKeys();
+ if (accountKeys.isEmpty()) {
+ return new byte[0];
+ }
+ BloomFilter bloomFilter =
+ new BloomFilter(
+ new byte[(int) (1.2 * accountKeys.size()) + 3],
+ new FastPairBloomFilterHasher());
+ 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 = !mBatteryValues.isEmpty();
+ byte[] salt;
+ if (mOptions.getUseRandomSaltForAccountKeyRotation() || advertisingBatteryValues) {
+ salt = new byte[1];
+ new SecureRandom().nextBytes(salt);
+ mLogger.log("Using random salt %s for bloom filter", base16().encode(salt));
+ } else {
+ salt = BluetoothAddress.decode(address);
+ mLogger.log("Using address %s for bloom filter", address);
+ }
+
+ // To prevent tampering, account filter shall be slightly modified to include battery data
+ // when the battery values are included in the advertisement. Normally, when building the
+ // account filter, a value V is produce by combining the account key with a salt. Instead,
+ // when battery values are also being advertised, it be constructed as follows:
+ // - the first 16 bytes are account key.
+ // - the next bytes are the salt.
+ // - the remaining bytes are the battery data.
+ byte[] saltAndBatteryData =
+ advertisingBatteryValues ? concat(salt, generateBatteryData()) : salt;
+
+ for (ByteString accountKey : accountKeys) {
+ bloomFilter.add(concat(accountKey.toByteArray(), saltAndBatteryData));
+ }
+ byte[] packet = generateAccountKeyData(bloomFilter);
+ 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.
+ : packet;
+ }
+
+ /**
+ * Creates a new field for the packet.
+ *
+ * The header is formatted 0xLLLLTTTT where LLLL is the
+ * length of the field and TTTT is the type (0 for bloom filter, 1 for salt).
+ */
+ private byte[] createField(byte header, byte[] value) {
+ return concat(new byte[]{header}, value);
+ }
+
+ public int getTxPower() {
+ return mOptions.getTxPowerLevel();
+ }
+
+ @Nullable
+ byte[] getServiceData() {
+ byte[] packet =
+ isDiscoverable()
+ ? modelIdServiceData(/* forAdvertising= */ true)
+ : !getAccountKeys().isEmpty() ? accountKeysServiceData() : null;
+ return addBatteryValues(packet);
+ }
+
+ @Nullable
+ private byte[] addBatteryValues(byte[] packet) {
+ if (mBatteryValues.isEmpty() || packet == null) {
+ return packet;
+ }
+
+ return concat(packet, generateBatteryData());
+ }
+
+ private byte[] generateBatteryData() {
+ // Byte 0: Battery length and type, first 4 bits are the number of battery values, second
+ // 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[mBatteryValues.size() + 1];
+ batteryData[0] = (byte) (mBatteryValues.size() << 4
+ | (mSuppressBatteryNotification ? 0b0100 : 0b0011));
+
+ int batteryValueIndex = 1;
+ for (BatteryValue batteryValue : mBatteryValues) {
+ batteryData[batteryValueIndex++] =
+ (byte)
+ ((batteryValue.mCharging ? 0b10000000 : 0b00000000)
+ | (0b01111111 & batteryValue.mLevel));
+ }
+
+ return batteryData;
+ }
+
+ private byte[] generateAccountKeyData(BloomFilter bloomFilter) {
+ // Byte 0: length and type, first 4 bits are the length of bloom filter, second 4 are the
+ // type which indicating the subsequent pairing notification is suppressed or not.
+ // The following bytes are the data of bloom filter.
+ byte[] filterBytes = bloomFilter.asBytes();
+ byte lengthAndType = (byte) (filterBytes.length << 4
+ | (mSuppressSubsequentPairingNotification ? 0b0010 : 0b0000));
+ mLogger.log(
+ "Generate bloom filter with suppress subsequent pairing notification:%b",
+ mSuppressSubsequentPairingNotification);
+ return createField(lengthAndType, filterBytes);
+ }
+
+ /** Disconnects all bonded devices. */
+ public void disconnectAllBondedDevices() {
+ for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) {
+ if (device.getBluetoothClass().getMajorDeviceClass() == Major.PHONE) {
+ removeBond(device);
+ }
+ }
+ }
+
+ public void disconnect(BluetoothProfile profile, BluetoothDevice device) {
+ device.disconnect();
+ }
+
+ public void removeBond(BluetoothDevice device) {
+ device.removeBond();
+ }
+
+ public void resetAccountKeys() {
+ mFastPairSimulatorDatabase.setAccountKeys(new HashSet<>());
+ mFastPairSimulatorDatabase.setFastPairSeekerDevices(new HashSet<>());
+ mAccountKey = null;
+ mOwnerAccountKey = null;
+ mLogger.log("Remove all account keys");
+ }
+
+ public void addAccountKey(byte[] key) {
+ addAccountKey(key, /* device= */ null);
+ }
+
+ private void addAccountKey(byte[] key, @Nullable BluetoothDevice device) {
+ mAccountKey = key;
+ if (mOwnerAccountKey == null) {
+ mOwnerAccountKey = key;
+ }
+
+ 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 mFastPairSimulatorDatabase.getAccountKeys();
+ }
+
+ /** Get the latest account key. */
+ @Nullable
+ public ByteString getAccountKey() {
+ if (mAccountKey == null) {
+ return null;
+ }
+ return ByteString.copyFrom(mAccountKey);
+ }
+
+ /** Get the owner account key (the first account key registered). */
+ @Nullable
+ public ByteString getOwnerAccountKey() {
+ if (mOwnerAccountKey == null) {
+ return null;
+ }
+ return ByteString.copyFrom(mOwnerAccountKey);
+ }
+
+ public void resetDeviceName() {
+ mFastPairSimulatorDatabase.setLocalDeviceName(null);
+ // Trigger simulator to update device name text view.
+ if (mDeviceNameCallback != null) {
+ mDeviceNameCallback.onNameChanged(getDeviceName());
+ }
+ }
+
+ // This method is used in test case with default name in provider.
+ public void setDeviceName(String deviceName) {
+ setDeviceName(deviceName.getBytes(StandardCharsets.UTF_8));
+ }
+
+ private void setDeviceName(@Nullable byte[] deviceName) {
+ mFastPairSimulatorDatabase.setLocalDeviceName(deviceName);
+
+ mLogger.log("Save device name : %s", getDeviceName());
+ // Trigger simulator to update device name text view.
+ if (mDeviceNameCallback != null) {
+ mDeviceNameCallback.onNameChanged(getDeviceName());
+ }
+ }
+
+ @Nullable
+ private byte[] getDeviceNameInBytes() {
+ return mFastPairSimulatorDatabase.getLocalDeviceName();
+ }
+
+ @Nullable
+ public String getDeviceName() {
+ String providerDeviceName =
+ getDeviceNameInBytes() != null
+ ? new String(getDeviceNameInBytes(), StandardCharsets.UTF_8)
+ : null;
+ mLogger.log("get device name = %s", providerDeviceName);
+ return providerDeviceName;
+ }
+
+ /**
+ * Bit index: Description - Value
+ *
+ * <ul>
+ * <li>0-1: Role - 0b10 (Provider only)
+ * <li>2: Transport Data Incomplete: 0 (false)
+ * <li>3-4: Transport State (0b00: Off, 0b01: On, 0b10: Temporarily Unavailable)
+ * <li>5-7: Reserved for future use
+ * </ul>
+ */
+ private static byte tdsFlags(TransportState transportState) {
+ return (byte) (0b00000010 & (transportState.mByteValue << 3));
+ }
+
+ /** Detailed information about battery value. */
+ public static class BatteryValue {
+ boolean mCharging;
+
+ // The range is 0 ~ 100, and -1 represents the battery level is unknown.
+ int mLevel;
+
+ public BatteryValue(boolean charging, int level) {
+ this.mCharging = charging;
+ this.mLevel = level;
+ }
+ }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulatorDatabase.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulatorDatabase.java
new file mode 100644
index 0000000..cbe39ff
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulatorDatabase.java
@@ -0,0 +1,264 @@
+/*
+ * 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;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.annotation.Nullable;
+
+import com.google.protobuf.ByteString;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+/** Stores fast pair related information for each paired device */
+public class FastPairSimulatorDatabase {
+
+ private static final String SHARED_PREF_NAME =
+ "android.nearby.fastpair.provider.fastpairsimulator";
+ private static final String KEY_DEVICE_NAME = "DEVICE_NAME";
+ private static final String KEY_ACCOUNT_KEYS = "ACCOUNT_KEYS";
+ private static final int MAX_NUMBER_OF_ACCOUNT_KEYS = 8;
+
+ // [for SASS]
+ private static final String KEY_FAST_PAIR_SEEKER_DEVICE = "FAST_PAIR_SEEKER_DEVICE";
+
+ private final SharedPreferences mSharedPreferences;
+
+ public FastPairSimulatorDatabase(Context context) {
+ mSharedPreferences = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
+ }
+
+ /** Adds single account key. */
+ public void addAccountKey(byte[] accountKey) {
+ if (mSharedPreferences == null) {
+ return;
+ }
+
+ Set<ByteString> accountKeys = new HashSet<>(getAccountKeys());
+ if (accountKeys.size() >= MAX_NUMBER_OF_ACCOUNT_KEYS) {
+ Set<ByteString> removedKeys = new HashSet<>();
+ int removedCount = accountKeys.size() - MAX_NUMBER_OF_ACCOUNT_KEYS + 1;
+ for (ByteString key : accountKeys) {
+ if (removedKeys.size() == removedCount) {
+ break;
+ }
+ removedKeys.add(key);
+ }
+
+ accountKeys.removeAll(removedKeys);
+ }
+
+ // Just make sure the newest key will not be removed.
+ accountKeys.add(ByteString.copyFrom(accountKey));
+ setAccountKeys(accountKeys);
+ }
+
+ /** Sets account keys, overrides all. */
+ public void setAccountKeys(Set<ByteString> accountKeys) {
+ if (mSharedPreferences == null) {
+ return;
+ }
+
+ Set<String> keys = new HashSet<>();
+ for (ByteString item : accountKeys) {
+ keys.add(base16().encode(item.toByteArray()));
+ }
+
+ mSharedPreferences.edit().putStringSet(KEY_ACCOUNT_KEYS, keys).apply();
+ }
+
+ /** Gets all account keys. */
+ public Set<ByteString> getAccountKeys() {
+ if (mSharedPreferences == null) {
+ return 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) {
+ accountKeys.add(ByteString.copyFrom(base16().decode(key)));
+ }
+
+ return accountKeys;
+ }
+
+ /** Sets local device name. */
+ public void setLocalDeviceName(byte[] deviceName) {
+ if (mSharedPreferences == null) {
+ return;
+ }
+
+ String humanReadableName = deviceName != null ? new String(deviceName, UTF_8) : null;
+ if (humanReadableName == null) {
+ mSharedPreferences.edit().remove(KEY_DEVICE_NAME).apply();
+ } else {
+ mSharedPreferences.edit().putString(KEY_DEVICE_NAME, humanReadableName).apply();
+ }
+ }
+
+ /** Gets local device name. */
+ @Nullable
+ public byte[] getLocalDeviceName() {
+ if (mSharedPreferences == null) {
+ return null;
+ }
+
+ String deviceName = mSharedPreferences.getString(KEY_DEVICE_NAME, null);
+ return deviceName != null ? deviceName.getBytes(UTF_8) : null;
+ }
+
+ /**
+ * [for SASS] Adds seeker device info. <a
+ * href="http://go/smart-audio-source-switching-design">Sass design doc</a>
+ */
+ public void addFastPairSeekerDevice(@Nullable BluetoothDevice device, byte[] accountKey) {
+ if (mSharedPreferences == null) {
+ return;
+ }
+
+ if (device == null) {
+ return;
+ }
+
+ // When hitting size limitation, choose the existing items to delete.
+ Set<FastPairSeekerDevice> fastPairSeekerDevices = getFastPairSeekerDevices();
+ if (fastPairSeekerDevices.size() > MAX_NUMBER_OF_ACCOUNT_KEYS) {
+ int removedCount = fastPairSeekerDevices.size() - MAX_NUMBER_OF_ACCOUNT_KEYS + 1;
+ Set<FastPairSeekerDevice> removedFastPairDevices = new HashSet<>();
+ for (FastPairSeekerDevice fastPairDevice : fastPairSeekerDevices) {
+ if (removedFastPairDevices.size() == removedCount) {
+ break;
+ }
+ removedFastPairDevices.add(fastPairDevice);
+ }
+ fastPairSeekerDevices.removeAll(removedFastPairDevices);
+ }
+
+ fastPairSeekerDevices.add(new FastPairSeekerDevice(device, accountKey));
+ setFastPairSeekerDevices(fastPairSeekerDevices);
+ }
+
+ /** [for SASS] Sets all seeker device info, overrides all. */
+ public void setFastPairSeekerDevices(Set<FastPairSeekerDevice> fastPairSeekerDeviceSet) {
+ if (mSharedPreferences == null) {
+ return;
+ }
+
+ Set<String> rawStringSet = new HashSet<>();
+ for (FastPairSeekerDevice item : fastPairSeekerDeviceSet) {
+ rawStringSet.add(item.toRawString());
+ }
+
+ mSharedPreferences.edit().putStringSet(KEY_FAST_PAIR_SEEKER_DEVICE, rawStringSet).apply();
+ }
+
+ /** [for SASS] Gets all seeker device info. */
+ public Set<FastPairSeekerDevice> getFastPairSeekerDevices() {
+ if (mSharedPreferences == null) {
+ return new HashSet<>();
+ }
+
+ Set<FastPairSeekerDevice> fastPairSeekerDevices = new HashSet<>();
+ Set<String> rawStringSet =
+ mSharedPreferences.getStringSet(KEY_FAST_PAIR_SEEKER_DEVICE, new HashSet<>());
+ for (String rawString : rawStringSet) {
+ FastPairSeekerDevice fastPairDevice = FastPairSeekerDevice.fromRawString(rawString);
+ if (fastPairDevice == null) {
+ continue;
+ }
+ fastPairSeekerDevices.add(fastPairDevice);
+ }
+
+ return fastPairSeekerDevices;
+ }
+
+ /** Defines data structure for the paired Fast Pair device. */
+ public static class FastPairSeekerDevice {
+ private static final int INDEX_DEVICE = 0;
+ private static final int INDEX_ACCOUNT_KEY = 1;
+
+ private final BluetoothDevice mDevice;
+ private final byte[] mAccountKey;
+
+ private FastPairSeekerDevice(BluetoothDevice device, byte[] accountKey) {
+ this.mDevice = device;
+ this.mAccountKey = accountKey;
+ }
+
+ public BluetoothDevice getBluetoothDevice() {
+ return mDevice;
+ }
+
+ public byte[] getAccountKey() {
+ return mAccountKey;
+ }
+
+ public String toRawString() {
+ return String.format("%s,%s", mDevice, base16().encode(mAccountKey));
+ }
+
+ /** Decodes the raw string if possible. */
+ @Nullable
+ public static FastPairSeekerDevice fromRawString(String rawString) {
+ BluetoothDevice device = null;
+ byte[] accountKey = null;
+ int step = INDEX_DEVICE;
+
+ StringTokenizer tokenizer = new StringTokenizer(rawString, ",");
+ while (tokenizer.hasMoreElements()) {
+ boolean shouldStop = false;
+ String token = tokenizer.nextToken();
+ switch (step) {
+ case INDEX_DEVICE:
+ try {
+ device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(token);
+ } catch (IllegalArgumentException e) {
+ device = null;
+ }
+ break;
+ case INDEX_ACCOUNT_KEY:
+ accountKey = base16().decode(token);
+ if (accountKey.length != 16) {
+ accountKey = null;
+ }
+ break;
+ default:
+ shouldStop = true;
+ }
+
+ if (shouldStop) {
+ break;
+ }
+ step++;
+ }
+ if (device != null && accountKey != null) {
+ return new FastPairSeekerDevice(device, accountKey);
+ }
+ return null;
+ }
+ }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/HandshakeRequest.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/HandshakeRequest.java
new file mode 100644
index 0000000..9cfffd8
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/HandshakeRequest.java
@@ -0,0 +1,157 @@
+/*
+ * 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;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.decrypt;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.BLUETOOTH_ADDRESS_LENGTH;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.ADDITIONAL_DATA_CHARACTERISTIC;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.DEVICE_ACTION;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.PROVIDER_INITIATES_BONDING;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_DEVICE_NAME;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_DISCOVERABLE;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_RETROACTIVE_PAIR;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.ADDITIONAL_DATA_TYPE_INDEX;
+
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AdditionalDataCharacteristic.AdditionalDataType;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request;
+
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+/**
+ * A wrapper for Fast Pair Provider to access decoded handshake request from the Seeker.
+ *
+ * @see {go/fast-pair-early-spec-handshake}
+ */
+public class HandshakeRequest {
+
+ /**
+ * 16 bytes data: 1-byte for type, 1-byte for flags, 6-bytes for provider's BLE address, 8 bytes
+ * optional data.
+ *
+ * @see {go/fast-pair-spec-handshake-message1}
+ */
+ private final byte[] mDecryptedMessage;
+
+ /** Enumerates the handshake message types. */
+ public enum Type {
+ KEY_BASED_PAIRING_REQUEST(Request.TYPE_KEY_BASED_PAIRING_REQUEST),
+ ACTION_OVER_BLE(Request.TYPE_ACTION_OVER_BLE),
+ UNKNOWN((byte) 0xFF);
+
+ private final byte mValue;
+
+ Type(byte type) {
+ mValue = type;
+ }
+
+ public byte getValue() {
+ return mValue;
+ }
+
+ public static Type valueOf(byte value) {
+ for (Type type : Type.values()) {
+ if (type.getValue() == value) {
+ return type;
+ }
+ }
+ return UNKNOWN;
+ }
+ }
+
+ public HandshakeRequest(byte[] key, byte[] encryptedPairingRequest)
+ throws GeneralSecurityException {
+ 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.
+ */
+ public Type getType() {
+ return Type.valueOf(mDecryptedMessage[Request.TYPE_INDEX]);
+ }
+
+ /**
+ * Gets verification data of this handshake request.
+ * Currently, we use Provider's BLE address.
+ */
+ public byte[] getVerificationData() {
+ return Arrays.copyOfRange(
+ mDecryptedMessage,
+ Request.VERIFICATION_DATA_INDEX,
+ Request.VERIFICATION_DATA_INDEX + Request.VERIFICATION_DATA_LENGTH);
+ }
+
+ /** Gets Seeker's public address of the handshake request. */
+ public byte[] getSeekerPublicAddress() {
+ return Arrays.copyOfRange(
+ mDecryptedMessage,
+ Request.SEEKER_PUBLIC_ADDRESS_INDEX,
+ Request.SEEKER_PUBLIC_ADDRESS_INDEX + BLUETOOTH_ADDRESS_LENGTH);
+ }
+
+ /** Checks whether the Seeker request discoverability from flags byte. */
+ public boolean requestDiscoverable() {
+ return (getFlags() & REQUEST_DISCOVERABLE) != 0;
+ }
+
+ /**
+ * Checks whether the Seeker requests that the Provider shall initiate bonding from flags byte.
+ */
+ public boolean requestProviderInitialBonding() {
+ return (getFlags() & PROVIDER_INITIATES_BONDING) != 0;
+ }
+
+ /** Checks whether the Seeker requests that the Provider shall notify the existing name. */
+ public boolean requestDeviceName() {
+ return (getFlags() & REQUEST_DEVICE_NAME) != 0;
+ }
+
+ /** Checks whether this is for retroactively writing account key. */
+ public boolean requestRetroactivePair() {
+ return (getFlags() & REQUEST_RETROACTIVE_PAIR) != 0;
+ }
+
+ /** Gets the flags of this handshake request. */
+ private byte getFlags() {
+ return mDecryptedMessage[Request.FLAGS_INDEX];
+ }
+
+ /** Checks whether the Seeker requests a device action. */
+ public boolean requestDeviceAction() {
+ return (getFlags() & DEVICE_ACTION) != 0;
+ }
+
+ /**
+ * 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. */
+ @AdditionalDataType
+ public int getAdditionalDataType() {
+ if (!requestFollowedByAdditionalData()
+ || mDecryptedMessage.length <= ADDITIONAL_DATA_TYPE_INDEX) {
+ return AdditionalDataType.UNKNOWN;
+ }
+ return mDecryptedMessage[ADDITIONAL_DATA_TYPE_INDEX];
+ }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/OreoFastPairAdvertiser.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/OreoFastPairAdvertiser.java
new file mode 100644
index 0000000..bc0cdfe
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/OreoFastPairAdvertiser.java
@@ -0,0 +1,163 @@
+/*
+ * 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;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.AdvertisingSet;
+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.ParcelUuid;
+
+import androidx.annotation.Nullable;
+
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Locale;
+
+/** Fast Pair advertiser taking advantage of new Android Oreo advertising features. */
+public final class OreoFastPairAdvertiser implements FastPairAdvertiser {
+ private static final String TAG = "OreoFastPairAdvertiser";
+ private final Logger mLogger = new Logger(TAG);
+
+ private final FastPairSimulator mSimulator;
+ private final BluetoothLeAdvertiser mAdvertiser;
+ private final AdvertisingSetCallback mAdvertisingSetCallback;
+ private AdvertisingSet mAdvertisingSet;
+
+ public OreoFastPairAdvertiser(FastPairSimulator simulator) {
+ this.mSimulator = simulator;
+ this.mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
+ this.mAdvertisingSetCallback = new AdvertisingSetCallback() {
+
+ @Override
+ public void onAdvertisingSetStarted(
+ AdvertisingSet set, int txPower, int status) {
+ if (status == AdvertisingSetCallback.ADVERTISE_SUCCESS) {
+ mLogger.log("Advertising succeeded, advertising at %s dBm", txPower);
+ simulator.setIsAdvertising(true);
+ mAdvertisingSet = set;
+ mAdvertisingSet.getOwnAddress();
+ } else {
+ mLogger.log(
+ new IllegalStateException(),
+ "Advertising failed, error code=%d", status);
+ }
+ }
+
+ @Override
+ public void onAdvertisingDataSet(AdvertisingSet set, int status) {
+ if (status != AdvertisingSetCallback.ADVERTISE_SUCCESS) {
+ mLogger.log(
+ new IllegalStateException(),
+ "Updating advertisement failed, error code=%d",
+ status);
+ stopAdvertising();
+ }
+ }
+
+ // Callback for AdvertisingSet.getOwnAddress().
+ @Override
+ public void onOwnAddressRead(
+ AdvertisingSet set, int addressType, String address) {
+ if (!address.equals(simulator.getBleAddress())) {
+ mLogger.log(
+ "Read own BLE address=%s at %s",
+ address,
+ new SimpleDateFormat("HH:mm:ss:SSS", Locale.US)
+ .format(Calendar.getInstance().getTime()));
+ // Implicitly start the advertising once BLE address callback arrived.
+ simulator.setBleAddress(address);
+ }
+ }
+ };
+ }
+
+ @Override
+ public void startAdvertising(@Nullable byte[] serviceData) {
+ // To be informed that BLE address is rotated, we need to polling query it asynchronously.
+ if (mAdvertisingSet != null) {
+ mAdvertisingSet.getOwnAddress();
+ }
+
+ if (mSimulator.isDestroyed()) {
+ return;
+ }
+
+ if (serviceData == null) {
+ mLogger.log("Service data is null, stop advertising");
+ stopAdvertising();
+ return;
+ }
+
+ AdvertiseData data =
+ new AdvertiseData.Builder()
+ .addServiceData(new ParcelUuid(FastPairService.ID), serviceData)
+ .setIncludeTxPowerLevel(true)
+ .build();
+
+ mLogger.log("Advertising FE2C service data=%s", base16().encode(serviceData));
+
+ if (mAdvertisingSet != null) {
+ mAdvertisingSet.setAdvertisingData(data);
+ return;
+ }
+
+ stopAdvertising();
+ AdvertisingSetParameters parameters =
+ new AdvertisingSetParameters.Builder()
+ .setLegacyMode(true)
+ .setConnectable(true)
+ .setScannable(true)
+ .setInterval(AdvertisingSetParameters.INTERVAL_LOW)
+ .setTxPowerLevel(convertAdvertiseSettingsTxPower(mSimulator.getTxPower()))
+ .build();
+ mAdvertiser.startAdvertisingSet(parameters, data, null, null, null,
+ mAdvertisingSetCallback);
+ }
+
+ private static int convertAdvertiseSettingsTxPower(int txPower) {
+ switch (txPower) {
+ case AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW:
+ return AdvertisingSetParameters.TX_POWER_ULTRA_LOW;
+ case AdvertiseSettings.ADVERTISE_TX_POWER_LOW:
+ return AdvertisingSetParameters.TX_POWER_LOW;
+ case AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM:
+ return AdvertisingSetParameters.TX_POWER_MEDIUM;
+ default:
+ return AdvertisingSetParameters.TX_POWER_HIGH;
+ }
+ }
+
+ @Override
+ public void stopAdvertising() {
+ if (mSimulator.isDestroyed()) {
+ return;
+ }
+
+ mAdvertiser.stopAdvertisingSet(mAdvertisingSetCallback);
+ mAdvertisingSet = null;
+ mSimulator.setIsAdvertising(false);
+ }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothController.kt b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothController.kt
new file mode 100644
index 0000000..0cc0c92
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothController.kt
@@ -0,0 +1,276 @@
+/*
+ * 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.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.utils.Logger
+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 mLogger = Logger(TAG)
+ 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) {
+ bluetoothAdapter.ioCapability = ioCapabilityClassic
+ bluetoothAdapter.leIoCapability = ioCapabilityBLE
+
+ // Toggling airplane mode on/off to restart Bluetooth stack and reset the BLE.
+ try {
+ Settings.Global.putInt(
+ context.contentResolver,
+ Settings.Global.AIRPLANE_MODE_ON,
+ TURN_AIRPLANE_MODE_ON
+ )
+ } catch (expectedOnNonCustomAndroid: SecurityException) {
+ mLogger.log(
+ expectedOnNonCustomAndroid,
+ "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) {
+ mLogger.log(
+ expectedOnNonCustomAndroid,
+ "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) {}
+ },
+ BluetoothProfile.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) {
+ 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
+ }
+ mLogger.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
+ )
+ mLogger.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
+ )
+ mLogger.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 {
+ private const val TAG = "BluetoothController"
+
+ private const val TURN_AIRPLANE_MODE_OFF = 0
+ private const val TURN_AIRPLANE_MODE_ON = 1
+ }
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConfig.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConfig.java
new file mode 100644
index 0000000..3cacd55
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConfig.java
@@ -0,0 +1,151 @@
+/*
+ * 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.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
+
+import androidx.annotation.Nullable;
+
+import com.android.server.nearby.common.bluetooth.BluetoothConsts;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.UUID;
+
+/** Configuration of a GATT server. */
+@TargetApi(18)
+public class BluetoothGattServerConfig {
+ private final Map<UUID, ServiceConfig> mServiceConfigs = new HashMap<UUID, ServiceConfig>();
+
+ @Nullable
+ private BluetoothGattServerHelper.Listener mServerlistener = null;
+
+ public BluetoothGattServerConfig addService(UUID uuid, ServiceConfig serviceConfig) {
+ mServiceConfigs.put(uuid, serviceConfig);
+ return this;
+ }
+
+ public BluetoothGattServerConfig setServerConnectionListener(
+ BluetoothGattServerHelper.Listener listener) {
+ mServerlistener = listener;
+ return this;
+ }
+
+ @Nullable
+ public BluetoothGattServerHelper.Listener getServerListener() {
+ return mServerlistener;
+ }
+
+ /**
+ * Adds a service and a characteristic to indicate that the server has dynamic services.
+ * This is a workaround for b/21587710.
+ * 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);
+ }
+ });
+ return addService(BluetoothConsts.SERVICE_DYNAMIC_SERVICE, serviceConfig);
+ }
+
+ public List<BluetoothGattService> getBluetoothGattServices() {
+ List<BluetoothGattService> result = new ArrayList<BluetoothGattService>();
+ for (Entry<UUID, ServiceConfig> serviceEntry : mServiceConfigs.entrySet()) {
+ UUID serviceUuid = serviceEntry.getKey();
+ ServiceConfig serviceConfig = serviceEntry.getValue();
+ if (serviceUuid == null || serviceConfig == null) {
+ // This is not supposed to happen
+ throw new IllegalStateException();
+ }
+ BluetoothGattService gattService = new BluetoothGattService(serviceUuid,
+ BluetoothGattService.SERVICE_TYPE_PRIMARY);
+ for (Entry<BluetoothGattCharacteristic, BluetoothGattServlet> servletEntry :
+ serviceConfig.getServlets().entrySet()) {
+ BluetoothGattCharacteristic characteristic = servletEntry.getKey();
+ if (characteristic == null) {
+ // This is not supposed to happen
+ throw new IllegalStateException();
+ }
+ gattService.addCharacteristic(characteristic);
+ }
+ result.add(gattService);
+ }
+ return result;
+ }
+
+ public List<UUID> getAdvertisedUuids() {
+ List<UUID> result = new ArrayList<UUID>();
+ for (Entry<UUID, ServiceConfig> serviceEntry : mServiceConfigs.entrySet()) {
+ UUID serviceUuid = serviceEntry.getKey();
+ ServiceConfig serviceConfig = serviceEntry.getValue();
+ if (serviceUuid == null || serviceConfig == null) {
+ // This is not supposed to happen
+ throw new IllegalStateException();
+ }
+ if (serviceConfig.isAdvertised()) {
+ result.add(serviceUuid);
+ }
+ }
+ return result;
+ }
+
+ public Map<BluetoothGattCharacteristic, BluetoothGattServlet> getServlets() {
+ Map<BluetoothGattCharacteristic, BluetoothGattServlet> result =
+ new HashMap<BluetoothGattCharacteristic, BluetoothGattServlet>();
+ for (ServiceConfig serviceConfig : mServiceConfigs.values()) {
+ result.putAll(serviceConfig.getServlets());
+ }
+ return result;
+ }
+
+ /** Configuration of a GATT service. */
+ public static class ServiceConfig {
+ private final Map<BluetoothGattCharacteristic, BluetoothGattServlet> mServlets =
+ new HashMap<BluetoothGattCharacteristic, BluetoothGattServlet>();
+ private boolean mAdvertise = false;
+
+ public ServiceConfig addCharacteristic(BluetoothGattServlet servlet) {
+ mServlets.put(servlet.getCharacteristic(), servlet);
+ return this;
+ }
+
+ public ServiceConfig setAdvertise(boolean advertise) {
+ mAdvertise = advertise;
+ return this;
+ }
+
+ public Map<BluetoothGattCharacteristic, BluetoothGattServlet> getServlets() {
+ return mServlets;
+ }
+
+ public boolean isAdvertised() {
+ return mAdvertise;
+ }
+ }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConnection.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConnection.java
new file mode 100644
index 0000000..fae6951
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerConnection.java
@@ -0,0 +1,468 @@
+/*
+ * 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.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+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.BluetoothOperationExecutor;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.io.BaseEncoding;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Connection to a bluetooth LE device over Gatt.
+ */
+@TargetApi(18)
+public class BluetoothGattServerConnection implements Closeable {
+ @SuppressWarnings("unused")
+ private static final String TAG = BluetoothGattServerConnection.class.getSimpleName();
+
+ /** See {@link BluetoothGattDescriptor#DISABLE_NOTIFICATION_VALUE}. */
+ private static final short DISABLE_NOTIFICATION_VALUE = 0x0000;
+
+ /** See {@link BluetoothGattDescriptor#ENABLE_NOTIFICATION_VALUE}. */
+ private static final short ENABLE_NOTIFICATION_VALUE = 0x0001;
+
+ /** See {@link BluetoothGattDescriptor#ENABLE_INDICATION_VALUE}. */
+ private static final short ENABLE_INDICATION_VALUE = 0x0002;
+
+ /** Default MTU when value is unknown. */
+ public static final int DEFAULT_MTU = 23;
+
+ @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 {
+ NOTIFICATION,
+ INDICATION
+ }
+
+ /** BT operation types that can be in flight. */
+ public enum OperationType {
+ SEND_NOTIFICATION
+ }
+
+ private final Map<ScopedKey, Object> mContextValues = new HashMap<ScopedKey, Object>();
+ private final List<Listener> mCloseListeners = new ArrayList<Listener>();
+
+ private final BluetoothGattServerHelper mBluetoothGattServerHelper;
+ private final BluetoothDevice mBluetoothDevice;
+
+ @VisibleForTesting
+ BluetoothOperationExecutor mBluetoothOperationScheduler =
+ new BluetoothOperationExecutor(1);
+
+ /** Stores pending writes. For each UUID, we store an offset and a byte[] of data. */
+ @VisibleForTesting
+ final Map<BluetoothGattServlet, SortedMap<Integer, byte[]>> mQueuedCharacteristicWrites =
+ new HashMap<BluetoothGattServlet, SortedMap<Integer, byte[]>>();
+
+ @VisibleForTesting
+ final Map<BluetoothGattCharacteristic, Notifier> mRegisteredNotifications =
+ new HashMap<BluetoothGattCharacteristic, Notifier>();
+
+ private final Map<BluetoothGattCharacteristic, BluetoothGattServlet> mServlets;
+
+ public BluetoothGattServerConnection(
+ BluetoothGattServerHelper bluetoothGattServerHelper,
+ BluetoothDevice device,
+ BluetoothGattServerConfig serverConfig) {
+ mBluetoothGattServerHelper = bluetoothGattServerHelper;
+ mBluetoothDevice = device;
+ mServlets = serverConfig.getServlets();
+ }
+
+ public void setContextValue(Object scope, String key, @Nullable Object value) {
+ mContextValues.put(new ScopedKey(scope, key), value);
+ }
+
+ @Nullable
+ public Object getContextValue(Object scope, String key) {
+ return mContextValues.get(new ScopedKey(scope, key));
+ }
+
+ public BluetoothDevice getDevice() {
+ return mBluetoothDevice;
+ }
+
+ public int getMtu() {
+ return DEFAULT_MTU;
+ }
+
+ public int getMaxDataPacketSize() {
+ // Per BT specs (3.2.9), only MTU - 3 bytes can be used to transmit data
+ return getMtu() - 3;
+ }
+
+ public void addCloseListener(Listener listener) {
+ synchronized (mCloseListeners) {
+ mCloseListeners.add(listener);
+ }
+ }
+
+ public void removeCloseListener(Listener listener) {
+ synchronized (mCloseListeners) {
+ mCloseListeners.remove(listener);
+ }
+ }
+
+ 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()),
+ BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
+ }
+ return servlet;
+ }
+
+ public byte[] readCharacteristic(int offset, BluetoothGattCharacteristic characteristic)
+ throws BluetoothGattException {
+ return getServlet(characteristic).read(this, offset);
+ }
+
+ 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.",
+ value.length,
+ offset,
+ BluetoothGattUtils.toString(characteristic),
+ mBluetoothDevice,
+ preparedWrite));
+ BluetoothGattServlet servlet = getServlet(characteristic);
+ if (preparedWrite) {
+ SortedMap<Integer, byte[]> bytePackets = mQueuedCharacteristicWrites.get(servlet);
+ if (bytePackets == null) {
+ bytePackets = new TreeMap<Integer, byte[]>();
+ mQueuedCharacteristicWrites.put(servlet, bytePackets);
+ }
+ bytePackets.put(offset, value);
+ return;
+ }
+
+ Log.d(TAG, servlet.toString());
+ servlet.write(this, offset, value);
+ }
+
+ public byte[] readDescriptor(int offset, BluetoothGattDescriptor descriptor)
+ throws BluetoothGattException {
+ BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
+ if (characteristic == null) {
+ throw new BluetoothGattException(String.format(
+ "Descriptor %s not associated with a characteristics!",
+ BluetoothGattUtils.toString(descriptor)), BluetoothGatt.GATT_FAILURE);
+ }
+ return getServlet(characteristic).readDescriptor(this, descriptor, offset);
+ }
+
+ public void writeDescriptor(
+ BluetoothGattDescriptor descriptor,
+ 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.",
+ value.length,
+ offset,
+ BluetoothGattUtils.toString(descriptor),
+ mBluetoothDevice,
+ preparedWrite));
+ if (preparedWrite) {
+ throw new BluetoothGattException(
+ String.format("Prepare write not supported for descriptor %s.", descriptor),
+ BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
+ }
+
+ BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
+ if (characteristic == null) {
+ throw new BluetoothGattException(String.format(
+ "Descriptor %s not associated with a characteristics!",
+ BluetoothGattUtils.toString(descriptor)), BluetoothGatt.GATT_FAILURE);
+ }
+ BluetoothGattServlet servlet = getServlet(characteristic);
+ if (descriptor.getUuid().equals(
+ ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION)) {
+ handleCharacteristicConfigurationChange(characteristic, servlet, offset, value);
+ return;
+ }
+ servlet.writeDescriptor(this, descriptor, offset, value);
+ }
+
+ private void handleCharacteristicConfigurationChange(
+ 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),
+ 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);
+ }
+
+ boolean notificationRegistered = mRegisteredNotifications.containsKey(characteristic);
+ Notifier notifier;
+ switch (toShort(value)) {
+ case ENABLE_NOTIFICATION_VALUE:
+ if (!notificationRegistered) {
+ notifier = new Notifier() {
+ @Override
+ public void notify(byte[] data) throws BluetoothException {
+ sendNotification(characteristic, NotificationType.NOTIFICATION, data);
+ }
+ };
+ mRegisteredNotifications.put(characteristic, notifier);
+ servlet.enableNotification(this, notifier);
+ }
+ break;
+ case ENABLE_INDICATION_VALUE:
+ if (!notificationRegistered) {
+ notifier = new Notifier() {
+ @Override
+ public void notify(byte[] data) throws BluetoothException {
+ sendNotification(characteristic, NotificationType.INDICATION, data);
+ }
+ };
+ mRegisteredNotifications.put(characteristic, notifier);
+ servlet.enableNotification(this, notifier);
+ }
+ break;
+ case DISABLE_NOTIFICATION_VALUE:
+ // Note: this disables notifications or indications.
+ if (notificationRegistered) {
+ notifier = mRegisteredNotifications.remove(characteristic);
+ if (notifier == null) {
+ return; // this is not supposed to happen
+ }
+ servlet.disableNotification(this, notifier);
+ }
+ break;
+ 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);
+ }
+ }
+
+ private static short toShort(byte[] value) {
+ Preconditions.checkNotNull(value);
+ Preconditions.checkArgument(value.length == 2, "Length should be 2 bytes.");
+
+ return (short) ((value[0] & 0x00FF) | (value[1] << 8));
+ }
+
+ public void executeWrite(boolean execute) throws BluetoothGattException {
+ if (!execute) {
+ mQueuedCharacteristicWrites.clear();
+ return;
+ }
+
+ try {
+ for (Entry<BluetoothGattServlet, SortedMap<Integer, byte[]>> queuedWrite :
+ mQueuedCharacteristicWrites.entrySet()) {
+ BluetoothGattServlet servlet = queuedWrite.getKey();
+ SortedMap<Integer, byte[]> chunks = queuedWrite.getValue();
+ if (servlet == null || chunks == null) {
+ // This is not supposed to happen
+ throw new IllegalStateException();
+ }
+ assembleByteChunksAndHandle(servlet, chunks);
+ }
+ } finally {
+ mQueuedCharacteristicWrites.clear();
+ }
+ }
+
+ /**
+ * 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>
+ *
+ * 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
+ * </code>
+ *
+ * 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 {
+ ByteArrayOutputStream assembledRequest = new ByteArrayOutputStream();
+ Integer startWritingAtOffset = 0;
+
+ while (!byteChunks.isEmpty()) {
+ 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);
+ }
+
+ // If we have a hole, then write what we've already assembled and start assembling a new
+ // long write
+ if (offset.intValue() > startWritingAtOffset + assembledRequest.size()) {
+ servlet.write(this, startWritingAtOffset.intValue(),
+ assembledRequest.toByteArray());
+ startWritingAtOffset = offset;
+ assembledRequest.reset();
+ }
+
+ try {
+ byte[] dataChunk = byteChunks.remove(offset);
+ if (dataChunk == null) {
+ // This is not supposed to happen
+ throw new IllegalStateException();
+ }
+ assembledRequest.write(dataChunk);
+ } catch (IOException e) {
+ throw new BluetoothGattException("Error assembling request",
+ BluetoothGatt.GATT_FAILURE);
+ }
+ }
+
+ // If there is anything to write, write it
+ if (assembledRequest.size() > 0) {
+ Preconditions.checkNotNull(startWritingAtOffset); // should never be null at this point
+ servlet.write(this, startWritingAtOffset.intValue(), assembledRequest.toByteArray());
+ }
+ }
+
+ private void sendNotification(final BluetoothGattCharacteristic characteristic,
+ final NotificationType notificationType, final byte[] data)
+ throws BluetoothException {
+ mBluetoothOperationScheduler.execute(
+ new Operation<Void>(OperationType.SEND_NOTIFICATION) {
+ @Override
+ public void run() throws BluetoothException {
+ mBluetoothGattServerHelper.sendNotification(mBluetoothDevice,
+ characteristic,
+ data,
+ notificationType == NotificationType.INDICATION ? true : false);
+ }
+ },
+ OPERATION_TIMEOUT);
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ mBluetoothGattServerHelper.closeConnection(mBluetoothDevice);
+ } catch (BluetoothException e) {
+ throw new IOException("Failed to close connection", e);
+ }
+ }
+
+ public void notifyNotificationSent(int status) {
+ mBluetoothOperationScheduler.notifyCompletion(
+ new Operation<Void>(OperationType.SEND_NOTIFICATION), status);
+ }
+
+ public void onClose() {
+ synchronized (mCloseListeners) {
+ for (Listener listener : mCloseListeners) {
+ listener.onClose();
+ }
+ }
+ }
+
+ /** Scope/key pair to use to reference contextual values. */
+ private static class ScopedKey {
+ private final Object mScope;
+ private final String mKey;
+
+ ScopedKey(Object scope, String key) {
+ mScope = scope;
+ mKey = key;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (!(o instanceof ScopedKey)) {
+ return false;
+ }
+ ScopedKey other = (ScopedKey) o;
+ return other.mScope.equals(mScope) && other.mKey.equals(mKey);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mScope, mKey);
+ }
+ }
+
+ /** Listener to be notified when the connection closes. */
+ public interface Listener {
+ void onClose();
+ }
+
+ /** Notifier to notify data over notification or indication. */
+ public interface Notifier {
+ void notify(byte[] data) throws BluetoothException;
+ }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerHelper.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerHelper.java
new file mode 100644
index 0000000..9339e14
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServerHelper.java
@@ -0,0 +1,449 @@
+/*
+ * 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.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.os.Build.VERSION_CODES;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.BluetoothGattException;
+import com.android.server.nearby.common.bluetooth.testability.VersionProvider;
+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.util.BluetoothOperationExecutor;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
+
+import com.google.common.base.Preconditions;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Helper for simplifying operations on {@link BluetoothGattServer}.
+ */
+@TargetApi(18)
+public class BluetoothGattServerHelper {
+ private static final String TAG = BluetoothGattServerHelper.class.getSimpleName();
+
+ @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. */
+ public enum OperationType {
+ ADD_SERVICE,
+ CLOSE_CONNECTION,
+ START_ADVERTISING
+ }
+
+ private final Object mOperationLock = new Object();
+ @VisibleForTesting
+ final BluetoothGattServerCallback mGattServerCallback =
+ new GattServerCallback();
+ @VisibleForTesting
+ BluetoothOperationExecutor mBluetoothOperationScheduler =
+ new BluetoothOperationExecutor(MAX_PARALLEL_OPERATIONS);
+
+ private final Context mContext;
+ private final BluetoothManager mBluetoothManager;
+ private final VersionProvider mVersionProvider;
+
+ @Nullable
+ @VisibleForTesting
+ volatile BluetoothGattServerConfig mServerConfig = null;
+
+ @Nullable
+ @VisibleForTesting
+ volatile BluetoothGattServer mBluetoothGattServer = null;
+
+ @VisibleForTesting
+ final ConcurrentMap<BluetoothDevice, BluetoothGattServerConnection>
+ mConnections = new ConcurrentHashMap<BluetoothDevice, BluetoothGattServerConnection>();
+
+ public BluetoothGattServerHelper(Context context, BluetoothManager bluetoothManager) {
+ this(
+ Preconditions.checkNotNull(context),
+ Preconditions.checkNotNull(bluetoothManager),
+ new VersionProvider()
+ );
+ }
+
+ @VisibleForTesting
+ BluetoothGattServerHelper(
+ Context context, BluetoothManager bluetoothManager, VersionProvider versionProvider) {
+ mContext = context;
+ mBluetoothManager = bluetoothManager;
+ mVersionProvider = versionProvider;
+ }
+
+ @Nullable
+ public BluetoothGattServerConfig getConfig() {
+ return mServerConfig;
+ }
+
+ public void open(final BluetoothGattServerConfig gattServerConfig) throws BluetoothException {
+ synchronized (mOperationLock) {
+ Preconditions.checkState(mBluetoothGattServer == null, "Gatt server is already open.");
+ final BluetoothGattServer server =
+ mBluetoothManager.openGattServer(mContext, mGattServerCallback);
+ if (server == null) {
+ throw new BluetoothException(
+ "Failed to open the GATT server, openGattServer returned null.");
+ }
+
+ try {
+ for (final BluetoothGattService service :
+ gattServerConfig.getBluetoothGattServices()) {
+ if (service == null) {
+ continue;
+ }
+ mBluetoothOperationScheduler.execute(
+ new Operation<Void>(OperationType.ADD_SERVICE, service) {
+ @Override
+ public void run() throws BluetoothException {
+ boolean success = server.addService(service);
+ if (!success) {
+ throw new BluetoothException("Fails on adding service");
+ }
+ }
+ }, OPERATION_TIMEOUT_MILLIS);
+ }
+ mBluetoothGattServer = server;
+ mServerConfig = gattServerConfig;
+ } catch (BluetoothException e) {
+ server.close();
+ throw e;
+ }
+ }
+ }
+
+ public boolean isOpen() {
+ synchronized (mOperationLock) {
+ return mBluetoothGattServer != null;
+ }
+ }
+
+ public void close() {
+ synchronized (mOperationLock) {
+ BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
+ if (bluetoothGattServer == null) {
+ return;
+ }
+ bluetoothGattServer.close();
+ mBluetoothGattServer = null;
+ }
+ }
+
+ private BluetoothGattServerConnection getConnectionByDevice(BluetoothDevice device)
+ throws BluetoothGattException {
+ BluetoothGattServerConnection bluetoothLeConnection = mConnections.get(device);
+ if (bluetoothLeConnection == null) {
+ throw new BluetoothGattException(
+ String.format("Received operation on an unknown device: %s", device),
+ BluetoothGatt.GATT_FAILURE);
+ }
+ return bluetoothLeConnection;
+ }
+
+ public void sendNotification(
+ BluetoothDevice device,
+ BluetoothGattCharacteristic characteristic,
+ byte[] data,
+ boolean confirm)
+ throws BluetoothException {
+ Log.d(TAG, String.format("Sending a %s of %d bytes on characteristics %s on device %s.",
+ confirm ? "indication" : "notification",
+ data.length,
+ characteristic.getUuid(),
+ device));
+ synchronized (mOperationLock) {
+ BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
+ if (bluetoothGattServer == null) {
+ throw new BluetoothException("Server is not open.");
+ }
+ BluetoothGattCharacteristic clonedCharacteristic =
+ BluetoothGattUtils.clone(characteristic);
+ clonedCharacteristic.setValue(data);
+ bluetoothGattServer.notifyCharacteristicChanged(device, clonedCharacteristic, confirm);
+ }
+ }
+
+ public void closeConnection(final BluetoothDevice bluetoothDevice) throws BluetoothException {
+ final BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
+ if (bluetoothGattServer == null) {
+ throw new BluetoothException("Server is not open.");
+ }
+ int connectionSate =
+ mBluetoothManager.getConnectionState(bluetoothDevice, BluetoothProfile.GATT);
+ if (connectionSate != BluetoothGatt.STATE_CONNECTED) {
+ return;
+ }
+ mBluetoothOperationScheduler.execute(
+ new Operation<Void>(OperationType.CLOSE_CONNECTION) {
+ @Override
+ public void run() throws BluetoothException {
+ bluetoothGattServer.cancelConnection(bluetoothDevice);
+ }
+ },
+ OPERATION_TIMEOUT_MILLIS);
+ }
+
+ private class GattServerCallback extends BluetoothGattServerCallback {
+ @Override
+ public void onServiceAdded(int status, BluetoothGattService service) {
+ mBluetoothOperationScheduler.notifyCompletion(
+ new Operation<Void>(OperationType.ADD_SERVICE, service), status);
+ }
+
+ @Override
+ public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
+ BluetoothGattServerConfig serverConfig = mServerConfig;
+ BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
+ BluetoothGattServerConnection bluetoothLeConnection;
+ if (serverConfig == null || bluetoothGattServer == null) {
+ return;
+ }
+ switch (newState) {
+ case BluetoothGattServer.STATE_CONNECTED:
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.e(TAG, String.format("Connection to %s failed: %s", device,
+ BluetoothGattUtils.getMessageForStatusCode(status)));
+ return;
+ }
+ 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));
+ return;
+ }
+
+ BluetoothGattServerConnection connection = new BluetoothGattServerConnection(
+ BluetoothGattServerHelper.this,
+ device,
+ serverConfig);
+ if (serverConfig.getServerListener() != null) {
+ serverConfig.getServerListener().onConnection(connection);
+ }
+ 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
+ // 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.
+ // 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);
+ if (!success) {
+ Log.w(TAG, String.format(
+ "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)));
+ }
+ bluetoothLeConnection = mConnections.remove(device);
+ if (bluetoothLeConnection != null) {
+ // Disconnect the server, required after connecting to it.
+ bluetoothGattServer.cancelConnection(device);
+ bluetoothLeConnection.onClose();
+ }
+ mBluetoothOperationScheduler.notifyCompletion(
+ new Operation<Void>(OperationType.CLOSE_CONNECTION), status);
+ break;
+ default:
+ Log.e(TAG, String.format("Unexpected connection state: %d", newState));
+ }
+ }
+
+ @Override
+ public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
+ BluetoothGattCharacteristic characteristic) {
+ BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
+ if (bluetoothGattServer == null) {
+ return;
+ }
+ try {
+ byte[] value =
+ getConnectionByDevice(device).readCharacteristic(offset, characteristic);
+ 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 offset %d",
+ BluetoothGattUtils.toString(characteristic),
+ device,
+ offset),
+ e);
+ bluetoothGattServer.sendResponse(
+ device, requestId, e.getGattErrorCode(), offset, null);
+ }
+ }
+
+ @Override
+ public void onCharacteristicWriteRequest(BluetoothDevice device,
+ int requestId,
+ BluetoothGattCharacteristic characteristic,
+ boolean preparedWrite,
+ boolean responseNeeded,
+ int offset,
+ byte[] value) {
+ BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
+ if (bluetoothGattServer == null) {
+ return;
+ }
+ try {
+ getConnectionByDevice(device).writeCharacteristic(characteristic,
+ preparedWrite,
+ offset,
+ value);
+ if (responseNeeded) {
+ bluetoothGattServer.sendResponse(
+ device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
+ }
+ } catch (BluetoothGattException e) {
+ Log.e(TAG,
+ String.format(
+ "Could not write %s on device %s at offset %d",
+ BluetoothGattUtils.toString(characteristic),
+ device,
+ offset),
+ e);
+ bluetoothGattServer.sendResponse(
+ device, requestId, e.getGattErrorCode(), offset, null);
+ }
+ }
+
+ @Override
+ public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset,
+ BluetoothGattDescriptor descriptor) {
+ BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
+ if (bluetoothGattServer == null) {
+ return;
+ }
+ try {
+ byte[] value = getConnectionByDevice(device).readDescriptor(offset, descriptor);
+ 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",
+ BluetoothGattUtils.toString(descriptor),
+ device,
+ offset),
+ e);
+ bluetoothGattServer.sendResponse(
+ device, requestId, e.getGattErrorCode(), offset, null);
+ }
+ }
+
+ @Override
+ public void onDescriptorWriteRequest(BluetoothDevice device,
+ int requestId,
+ BluetoothGattDescriptor descriptor,
+ boolean preparedWrite,
+ boolean responseNeeded,
+ int offset,
+ byte[] value) {
+ BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
+ if (bluetoothGattServer == null) {
+ return;
+ }
+ try {
+ getConnectionByDevice(device)
+ .writeDescriptor(descriptor, preparedWrite, offset, value);
+ if (responseNeeded) {
+ bluetoothGattServer.sendResponse(
+ device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
+ }
+ Log.d(TAG, "Operation onDescriptorWriteRequest successful.");
+ } catch (BluetoothGattException e) {
+ Log.e(TAG,
+ String.format(
+ "Could not write %s on device %s at %d",
+ BluetoothGattUtils.toString(descriptor),
+ device,
+ offset),
+ e);
+ bluetoothGattServer.sendResponse(
+ device, requestId, e.getGattErrorCode(), offset, null);
+ }
+ }
+
+ @Override
+ public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
+ BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
+ if (bluetoothGattServer == null) {
+ return;
+ }
+ try {
+ getConnectionByDevice(device).executeWrite(execute);
+ 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);
+ }
+ }
+
+ @Override
+ public void onNotificationSent(BluetoothDevice device, int 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, "An error occurred when receiving onNotificationSent: " + e);
+ }
+ }
+ }
+
+ /** Listener for {@link BluetoothGattServerHelper}'s events. */
+ public interface Listener {
+ /** Called when a new connection to the server is established. */
+ void onConnection(BluetoothGattServerConnection connection);
+ }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServlet.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServlet.java
new file mode 100644
index 0000000..e25e223
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattServlet.java
@@ -0,0 +1,71 @@
+/*
+ * 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;
+
+/** Servlet to handle GATT operations on a characteristic. */
+@TargetApi(18)
+public abstract class BluetoothGattServlet {
+ public byte[] read(BluetoothGattServerConnection connection,
+ @SuppressWarnings("unused") int offset) throws BluetoothGattException {
+ throw new BluetoothGattException("Read not supported.",
+ BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
+ }
+
+ public void write(BluetoothGattServerConnection connection,
+ @SuppressWarnings("unused") int offset, @SuppressWarnings("unused") byte[] value)
+ throws BluetoothGattException {
+ throw new BluetoothGattException("Write not supported.",
+ BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
+ }
+
+ public byte[] readDescriptor(BluetoothGattServerConnection connection,
+ BluetoothGattDescriptor descriptor, @SuppressWarnings("unused") int offset)
+ throws BluetoothGattException {
+ throw new BluetoothGattException("Read not supported.",
+ BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
+ }
+
+ public void writeDescriptor(BluetoothGattServerConnection connection,
+ BluetoothGattDescriptor descriptor,
+ @SuppressWarnings("unused") int offset, @SuppressWarnings("unused") byte[] value)
+ throws BluetoothGattException {
+ throw new BluetoothGattException("Write not supported.",
+ BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
+ }
+
+ public void enableNotification(BluetoothGattServerConnection connection, Notifier notifier)
+ throws BluetoothGattException {
+ throw new BluetoothGattException("Notification not supported.",
+ BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
+ }
+
+ public void disableNotification(BluetoothGattServerConnection connection, Notifier notifier)
+ throws BluetoothGattException {
+ throw new BluetoothGattException("Notification not supported.",
+ BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
+ }
+
+ public abstract BluetoothGattCharacteristic getCharacteristic();
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattUtils.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattUtils.java
new file mode 100644
index 0000000..7ac26ee
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothGattUtils.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.fastpair.provider.bluetooth;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+
+import androidx.annotation.Nullable;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+
+/**
+ * Utils for Gatt profile.
+ */
+public class BluetoothGattUtils {
+
+ /**
+ * Returns a string message for a BluetoothGatt status codes.
+ */
+ public static String getMessageForStatusCode(int statusCode) {
+ switch (statusCode) {
+ case BluetoothGatt.GATT_SUCCESS:
+ return "GATT_SUCCESS";
+ case BluetoothGatt.GATT_FAILURE:
+ return "GATT_FAILURE";
+ case BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION:
+ return "GATT_INSUFFICIENT_AUTHENTICATION";
+ case BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION:
+ return "GATT_INSUFFICIENT_AUTHORIZATION";
+ case BluetoothGatt.GATT_INSUFFICIENT_ENCRYPTION:
+ return "GATT_INSUFFICIENT_ENCRYPTION";
+ case BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH:
+ return "GATT_INVALID_ATTRIBUTE_LENGTH";
+ case BluetoothGatt.GATT_INVALID_OFFSET:
+ return "GATT_INVALID_OFFSET";
+ case BluetoothGatt.GATT_READ_NOT_PERMITTED:
+ return "GATT_READ_NOT_PERMITTED";
+ case BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED:
+ return "GATT_REQUEST_NOT_SUPPORTED";
+ case BluetoothGatt.GATT_WRITE_NOT_PERMITTED:
+ return "GATT_WRITE_NOT_PERMITTED";
+ case BluetoothGatt.GATT_CONNECTION_CONGESTED:
+ return "GATT_CONNECTION_CONGESTED";
+ default:
+ return "Unknown error code";
+ }
+ }
+
+ /** Clones a {@link BluetoothGattCharacteristic} so the value can be changed thread-safely. */
+ public static BluetoothGattCharacteristic clone(BluetoothGattCharacteristic characteristic)
+ throws BluetoothException {
+ BluetoothGattCharacteristic result = new BluetoothGattCharacteristic(
+ characteristic.getUuid(),
+ characteristic.getProperties(), characteristic.getPermissions());
+ try {
+ Field instanceIdField = BluetoothGattCharacteristic.class.getDeclaredField("mInstance");
+ Field serviceField = BluetoothGattCharacteristic.class.getDeclaredField("mService");
+ Field descriptorField = BluetoothGattCharacteristic.class.getDeclaredField(
+ "mDescriptors");
+ instanceIdField.setAccessible(true);
+ serviceField.setAccessible(true);
+ descriptorField.setAccessible(true);
+ instanceIdField.set(result, instanceIdField.get(characteristic));
+ serviceField.set(result, serviceField.get(characteristic));
+ descriptorField.set(result, descriptorField.get(characteristic));
+ byte[] value = characteristic.getValue();
+ if (value != null) {
+ result.setValue(Arrays.copyOf(value, value.length));
+ }
+ result.setWriteType(characteristic.getWriteType());
+ } catch (NoSuchFieldException e) {
+ throw new BluetoothException("Cannot clone characteristic.", e);
+ } catch (IllegalAccessException e) {
+ throw new BluetoothException("Cannot clone characteristic.", e);
+ } catch (IllegalArgumentException e) {
+ throw new BluetoothException("Cannot clone characteristic.", e);
+ }
+ return result;
+ }
+
+ /** Creates a user-readable string from a {@link BluetoothGattDescriptor}. */
+ public static String toString(@Nullable BluetoothGattDescriptor descriptor) {
+ if (descriptor == null) {
+ return "null descriptor";
+ }
+ return String.format("descriptor %s on %s",
+ descriptor.getUuid(),
+ toString(descriptor.getCharacteristic()));
+ }
+
+ /** Creates a user-readable string from a {@link BluetoothGattCharacteristic}. */
+ public static String toString(@Nullable BluetoothGattCharacteristic characteristic) {
+ if (characteristic == null) {
+ return "null characteristic";
+ }
+ return String.format("characteristic %s on %s",
+ characteristic.getUuid(),
+ toString(characteristic.getService()));
+ }
+
+ /** Creates a user-readable string from a {@link BluetoothGattService}. */
+ public static String toString(@Nullable BluetoothGattService service) {
+ if (service == null) {
+ return "null service";
+ }
+ return String.format("service %s", service.getUuid());
+ }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothManager.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothManager.java
new file mode 100644
index 0000000..bf241f1
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothManager.java
@@ -0,0 +1,80 @@
+/*
+ * 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.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;
+
+/**
+ * Mockable wrapper of {@link android.bluetooth.BluetoothManager}.
+ */
+public class BluetoothManager {
+
+ private android.bluetooth.BluetoothManager mWrappedInstance;
+
+ private BluetoothManager(android.bluetooth.BluetoothManager instance) {
+ mWrappedInstance = instance;
+ }
+
+ /**
+ * See {@link android.bluetooth.BluetoothManager#openGattServer(Context,
+ * android.bluetooth.BluetoothGattServerCallback)}.
+ */
+ @Nullable
+ public BluetoothGattServer openGattServer(Context context,
+ BluetoothGattServerCallback callback) {
+ return BluetoothGattServer.wrap(
+ mWrappedInstance.openGattServer(context, callback.unwrap()));
+ }
+
+ /**
+ * See {@link android.bluetooth.BluetoothManager#getConnectionState(
+ *android.bluetooth.BluetoothDevice, int)}.
+ */
+ public int getConnectionState(BluetoothDevice device, int profile) {
+ return mWrappedInstance.getConnectionState(device.unwrap(), profile);
+ }
+
+ /** See {@link android.bluetooth.BluetoothManager#getConnectedDevices(int)}. */
+ public List<BluetoothDevice> getConnectedDevices(int profile) {
+ List<android.bluetooth.BluetoothDevice> devices = mWrappedInstance.getConnectedDevices(
+ profile);
+ List<BluetoothDevice> wrappedDevices = new ArrayList<>(devices.size());
+ for (android.bluetooth.BluetoothDevice device : devices) {
+ wrappedDevices.add(BluetoothDevice.wrap(device));
+ }
+ return wrappedDevices;
+ }
+
+ /** See {@link android.bluetooth.BluetoothManager#getAdapter()}. */
+ public BluetoothAdapter getAdapter() {
+ return BluetoothAdapter.wrap(mWrappedInstance.getAdapter());
+ }
+
+ public static BluetoothManager wrap(android.bluetooth.BluetoothManager bluetoothManager) {
+ return new BluetoothManager(bluetoothManager);
+ }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/RfcommServer.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/RfcommServer.java
new file mode 100644
index 0000000..9ed95ac
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/RfcommServer.java
@@ -0,0 +1,419 @@
+/*
+ * 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 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.fastpair.provider.EventStreamProtocol;
+import android.nearby.fastpair.provider.utils.Logger;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Listens for a rfcomm client to connect and supports both sending messages to the client and
+ * receiving messages from the client.
+ */
+public class RfcommServer {
+ private static final String TAG = "RfcommServer";
+ 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 mControllerExecutor = Executors.newSingleThreadExecutor();
+
+ private final ExecutorService mSendMessageExecutor = Executors.newSingleThreadExecutor();
+ private final ExecutorService mReceiveMessageExecutor = Executors.newSingleThreadExecutor();
+
+ @Nullable
+ private BluetoothServerSocket mServerSocket;
+ @Nullable
+ private BluetoothSocket mSocket;
+
+ private State mState = STOPPED;
+ private boolean mIsStopRequested = false;
+
+ @Nullable
+ private RequestHandler mRequestHandler;
+
+ @Nullable
+ private CountDownLatch mCountDownLatch;
+ @Nullable
+ private StateMonitor mStateMonitor;
+
+ /**
+ * Manages RfcommServer status.
+ *
+ * <pre>{@code
+ * +------------------------------------------------+
+ * +-------------------------------+ |
+ * v | |
+ * +---------+ +----------+ +-----+-----+ +-----+-----+
+ * | STOPPED +--> | STARTING +--> | ACCEPTING +--> | CONNECTED |
+ * +---------+ +-----+----+ +-------+---+ +-----+-----+
+ * ^ | ^ v |
+ * +---------------+ +---+--------+ |
+ * | RESTARTING | <-------+
+ * +------------+
+ * }</pre>
+ *
+ * If Stop action is not requested, the server will restart forever. Otherwise, go stopped.
+ */
+ public enum State {
+ STOPPED,
+ STARTING,
+ RESTARTING,
+ ACCEPTING,
+ CONNECTED,
+ }
+
+ /** Starts the rfcomm server. */
+ public void start() {
+ runInControllerExecutor(this::startServer);
+ }
+
+ private void startServer() {
+ log("Start RfcommServer");
+
+ if (!mState.equals(STOPPED)) {
+ log("Server is not stopped, skip start request.");
+ return;
+ }
+ updateState(STARTING);
+ mIsStopRequested = false;
+
+ startAccept();
+ }
+
+ private void restartServer() {
+ log("Restart RfcommServer");
+ updateState(RESTARTING);
+ startAccept();
+ }
+
+ private void startAccept() {
+ try {
+ // Gets server socket in controller thread for stop() API.
+ mServerSocket =
+ BluetoothAdapter.getDefaultAdapter()
+ .listenUsingRfcommWithServiceRecord(
+ FAST_PAIR_RFCOMM_SERVICE_NAME, FAST_PAIR_RFCOMM_UUID);
+ } catch (IOException e) {
+ log("Create service record failed, stop server");
+ stopServer();
+ return;
+ }
+
+ updateState(ACCEPTING);
+ new Thread(() -> accept(mServerSocket)).start();
+ }
+
+ private void accept(BluetoothServerSocket serverSocket) {
+ triggerCountdownLatch();
+
+ try {
+ BluetoothSocket socket = serverSocket.accept();
+ serverSocket.close();
+
+ runInControllerExecutor(() -> startListen(socket));
+ } catch (IOException e) {
+ log("IOException when accepting new connection");
+ runInControllerExecutor(() -> handleAcceptException(serverSocket));
+ }
+ }
+
+ private void handleAcceptException(BluetoothServerSocket serverSocket) {
+ if (mIsStopRequested) {
+ stopServer();
+ } else {
+ closeServerSocket(serverSocket);
+ restartServer();
+ }
+ }
+
+ private void startListen(BluetoothSocket bluetoothSocket) {
+ if (mIsStopRequested) {
+ closeSocket(bluetoothSocket);
+ stopServer();
+ return;
+ }
+
+ updateState(CONNECTED);
+ // Sets method parameter to global socket for stop() API.
+ this.mSocket = bluetoothSocket;
+ new Thread(() -> listen(bluetoothSocket)).start();
+ }
+
+ private void listen(BluetoothSocket bluetoothSocket) {
+ triggerCountdownLatch();
+
+ try {
+ DataInputStream dataInputStream = new DataInputStream(bluetoothSocket.getInputStream());
+ while (true) {
+ int eventGroup = dataInputStream.readUnsignedByte();
+ int eventCode = dataInputStream.readUnsignedByte();
+ int additionalLength = dataInputStream.readUnsignedShort();
+
+ byte[] data = new byte[additionalLength];
+ if (additionalLength > 0) {
+ int count = 0;
+ do {
+ count += dataInputStream.read(data, count, additionalLength - count);
+ } while (count < additionalLength);
+ }
+
+ if (mRequestHandler != null) {
+ // In order not to block listening thread, use different thread to dispatch
+ // message.
+ mReceiveMessageExecutor.execute(
+ () -> {
+ mRequestHandler.handleRequest(eventGroup, eventCode, data);
+ triggerCountdownLatch();
+ });
+ }
+ }
+ } catch (IOException e) {
+ log(
+ String.format(
+ "IOException when listening to %s",
+ bluetoothSocket.getRemoteDevice().getAddress()));
+ runInControllerExecutor(() -> handleListenException(bluetoothSocket));
+ }
+ }
+
+ private void handleListenException(BluetoothSocket bluetoothSocket) {
+ if (mIsStopRequested) {
+ stopServer();
+ } else {
+ closeSocket(bluetoothSocket);
+ restartServer();
+ }
+ }
+
+ public void sendFakeEventStreamMessage(EventStreamProtocol.EventGroup eventGroup) {
+ switch (eventGroup) {
+ case BLUETOOTH:
+ send(EventStreamProtocol.EventGroup.BLUETOOTH_VALUE,
+ EventStreamProtocol.BluetoothEventCode.BLUETOOTH_ENABLE_SILENCE_MODE_VALUE,
+ new byte[0]);
+ break;
+ case LOGGING:
+ 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});
+ break;
+ default: // fall out
+ }
+ }
+
+ public void sendFakeEventStreamLoggingMessage(@Nullable String logContent) {
+ send(EventStreamProtocol.EventGroup.LOGGING_VALUE,
+ EventStreamProtocol.LoggingEventCode.LOG_SAVE_TO_BUFFER_VALUE,
+ logContent != null ? logContent.getBytes(UTF_8) : new byte[0]);
+ }
+
+ public void send(int eventGroup, int eventCode, byte[] data) {
+ runInControllerExecutor(
+ () -> {
+ if (!CONNECTED.equals(mState)) {
+ log("Server is not in CONNECTED state, skip send request");
+ return;
+ }
+ BluetoothSocket bluetoothSocket = this.mSocket;
+ mSendMessageExecutor.execute(() -> {
+ String address = bluetoothSocket.getRemoteDevice().getAddress();
+ try {
+ DataOutputStream dataOutputStream =
+ new DataOutputStream(bluetoothSocket.getOutputStream());
+ dataOutputStream.writeByte(eventGroup);
+ dataOutputStream.writeByte(eventCode);
+ dataOutputStream.writeShort(data.length);
+ if (data.length > 0) {
+ dataOutputStream.write(data);
+ }
+ dataOutputStream.flush();
+ log(
+ String.format(
+ "Send message to %s: %s, %s, %s.",
+ address, eventGroup, eventCode, data.length));
+ } catch (IOException e) {
+ log(
+ String.format(
+ "Failed to send message to %s: %s, %s, %s.",
+ address, eventGroup, eventCode, data.length),
+ e);
+ }
+ });
+ });
+ }
+
+ /** Stops the rfcomm server. */
+ public void stop() {
+ runInControllerExecutor(() -> {
+ log("Stop RfcommServer");
+
+ if (STOPPED.equals(mState)) {
+ log("Server is stopped, skip stop request.");
+ return;
+ }
+
+ if (mIsStopRequested) {
+ log("Stop is already requested, skip stop request.");
+ return;
+ }
+ mIsStopRequested = true;
+
+ if (ACCEPTING.equals(mState)) {
+ closeServerSocket(mServerSocket);
+ }
+
+ if (CONNECTED.equals(mState)) {
+ closeSocket(mSocket);
+ }
+ });
+ }
+
+ private void stopServer() {
+ updateState(STOPPED);
+ triggerCountdownLatch();
+ }
+
+ private void updateState(State newState) {
+ log(String.format("Change state from %s to %s", mState, newState));
+ if (mStateMonitor != null) {
+ mStateMonitor.onStateChanged(newState);
+ }
+ mState = newState;
+ }
+
+ private void closeServerSocket(BluetoothServerSocket serverSocket) {
+ try {
+ if (serverSocket != null) {
+ log(String.format("Close server socket: %s", serverSocket));
+ serverSocket.close();
+ }
+ } catch (IOException | NullPointerException e) {
+ // NullPointerException is used to skip robolectric test failure.
+ // In unit test, different virtual devices are set up in different threads, calling
+ // ServerSocket.close() in wrong thread will result in NullPointerException since there
+ // is no corresponding service record.
+ // TODO(hylo): Remove NullPointerException when the solution is submitted to test cases.
+ log("Failed to stop server", e);
+ }
+ }
+
+ private void closeSocket(BluetoothSocket socket) {
+ try {
+ if (socket != null && socket.isConnected()) {
+ log(String.format("Close socket: %s", socket.getRemoteDevice().getAddress()));
+ socket.close();
+ }
+ } catch (IOException e) {
+ log(String.format("IOException when close socket %s",
+ socket.getRemoteDevice().getAddress()));
+ }
+ }
+
+ private void runInControllerExecutor(Runnable runnable) {
+ mControllerExecutor.execute(runnable);
+ }
+
+ private void log(String message) {
+ mLogger.log("Server=%s, %s", FAST_PAIR_RFCOMM_SERVICE_NAME, message);
+ }
+
+ private void log(String message, Throwable e) {
+ mLogger.log(e, "Server=%s, %s", FAST_PAIR_RFCOMM_SERVICE_NAME, message);
+ }
+
+ private void triggerCountdownLatch() {
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ /** Interface to handle incoming request from clients. */
+ public interface RequestHandler {
+ void handleRequest(int eventGroup, int eventCode, byte[] data);
+ }
+
+ public void setRequestHandler(@Nullable RequestHandler requestHandler) {
+ this.mRequestHandler = requestHandler;
+ }
+
+ /** A state monitor to send signal when state is changed. */
+ public interface StateMonitor {
+ void onStateChanged(State state);
+ }
+
+ public void setStateMonitor(@Nullable StateMonitor stateMonitor) {
+ this.mStateMonitor = stateMonitor;
+ }
+
+ @VisibleForTesting
+ void setCountDownLatch(@Nullable CountDownLatch countDownLatch) {
+ this.mCountDownLatch = countDownLatch;
+ }
+
+ @VisibleForTesting
+ void setIsStopRequested(boolean isStopRequested) {
+ this.mIsStopRequested = isStopRequested;
+ }
+
+ @VisibleForTesting
+ void simulateAcceptIOException() {
+ runInControllerExecutor(() -> {
+ if (ACCEPTING.equals(mState)) {
+ closeServerSocket(mServerSocket);
+ }
+ });
+ }
+
+ @VisibleForTesting
+ void simulateListenIOException() {
+ runInControllerExecutor(() -> {
+ if (CONNECTED.equals(mState)) {
+ closeSocket(mSocket);
+ }
+ });
+ }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/Crypto.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/Crypto.java
new file mode 100644
index 0000000..0aa4f6e
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/Crypto.java
@@ -0,0 +1,70 @@
+/*
+ * 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.crypto;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+import android.annotation.SuppressLint;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.protobuf.ByteString;
+
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+
+/** Cryptography utilities for ephemeral IDs. */
+public final class Crypto {
+ private static final int AES_BLOCK_SIZE = 16;
+ private static final ImmutableSet<Integer> VALID_AES_KEY_SIZES = ImmutableSet.of(16, 24, 32);
+ private static final String AES_ECB_NOPADDING_ENCRYPTION_ALGO = "AES/ECB/NoPadding";
+ private static final String AES_ENCRYPTION_ALGO = "AES";
+
+ /** Encrypts the provided data with the provided key using the AES/ECB/NoPadding algorithm. */
+ public static ByteString aesEcbNoPaddingEncrypt(ByteString key, ByteString data) {
+ return aesEcbOperation(key, data, Cipher.ENCRYPT_MODE);
+ }
+
+ /** Decrypts the provided data with the provided key using the AES/ECB/NoPadding algorithm. */
+ public static ByteString aesEcbNoPaddingDecrypt(ByteString key, ByteString data) {
+ return aesEcbOperation(key, data, Cipher.DECRYPT_MODE);
+ }
+
+ @SuppressLint("GetInstance")
+ private static ByteString aesEcbOperation(ByteString key, ByteString data, int operation) {
+ checkArgument(VALID_AES_KEY_SIZES.contains(key.size()));
+ checkArgument(data.size() % AES_BLOCK_SIZE == 0);
+ try {
+ Cipher aesCipher = Cipher.getInstance(AES_ECB_NOPADDING_ENCRYPTION_ALGO);
+ SecretKeySpec secretKeySpec = new SecretKeySpec(key.toByteArray(), AES_ENCRYPTION_ALGO);
+ aesCipher.init(operation, secretKeySpec);
+ ByteBuffer output = ByteBuffer.allocate(data.size());
+ checkState(aesCipher.doFinal(data.asReadOnlyByteBuffer(), output) == data.size());
+ output.rewind();
+ return ByteString.copyFrom(output);
+ } catch (GeneralSecurityException e) {
+ // Should never happen.
+ throw new AssertionError(e);
+ }
+ }
+
+ private Crypto() {
+ }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/E2eeCalculator.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/E2eeCalculator.java
new file mode 100644
index 0000000..794c19d
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/crypto/E2eeCalculator.java
@@ -0,0 +1,188 @@
+/*
+ * 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.crypto;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Verify;
+import com.google.common.primitives.Bytes;
+import com.google.common.primitives.Ints;
+import com.google.protobuf.ByteString;
+
+import java.math.BigInteger;
+import java.security.spec.ECFieldFp;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.EllipticCurve;
+import java.util.Collections;
+
+/** Provides methods for calculating E2EE EIDs and E2E encryption/decryption based on E2EE EIDs. */
+public final class E2eeCalculator {
+
+ private static final byte[] TEMP_KEY_PADDING_1 =
+ Bytes.toArray(Collections.nCopies(11, (byte) 0xFF));
+ private static final byte[] TEMP_KEY_PADDING_2 = new byte[11];
+ private static final ECParameterSpec CURVE_SPEC = getCurveSpec();
+ private static final BigInteger P = ((ECFieldFp) CURVE_SPEC.getCurve().getField()).getP();
+ private static final BigInteger TWO = new BigInteger("2");
+ private static final BigInteger THREE = new BigInteger("3");
+ private static final int E2EE_EID_IDENTITY_KEY_SIZE = 32;
+ 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))}.
+ *
+ * <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)
+ * @return E2EE EID value.
+ */
+ public static ByteString computeE2eeEid(
+ ByteString identityKey, int exponent, int deviceClockSeconds) {
+ return computePublicKey(computePrivateKey(identityKey, exponent, deviceClockSeconds));
+ }
+
+ private static ByteString computePublicKey(BigInteger privateKey) {
+ return getXCoordinateBytes(toPoint(privateKey));
+ }
+
+ private static BigInteger computePrivateKey(
+ ByteString identityKey, int exponent, int deviceClockSeconds) {
+ Preconditions.checkArgument(
+ Preconditions.checkNotNull(identityKey).size() == E2EE_EID_IDENTITY_KEY_SIZE);
+ Preconditions.checkArgument(exponent >= 0 && exponent < 16);
+
+ byte[] exponentByte = new byte[]{(byte) exponent};
+ byte[] paddedCounter = Ints.toByteArray((deviceClockSeconds >>> exponent) << exponent);
+ byte[] data =
+ Bytes.concat(
+ TEMP_KEY_PADDING_1,
+ exponentByte,
+ paddedCounter,
+ TEMP_KEY_PADDING_2,
+ exponentByte,
+ paddedCounter);
+
+ byte[] rTag =
+ Crypto.aesEcbNoPaddingEncrypt(identityKey, ByteString.copyFrom(data)).toByteArray();
+ return new BigInteger(1, rTag).mod(CURVE_SPEC.getOrder());
+ }
+
+ private static ECPoint toPoint(BigInteger privateKey) {
+ return multiplyPoint(CURVE_SPEC.getGenerator(), privateKey);
+ }
+
+ private static ByteString getXCoordinateBytes(ECPoint point) {
+ 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.
+ Verify.verify(
+ unalignedBytes.length <= E2EE_EID_SIZE
+ || (unalignedBytes.length == E2EE_EID_SIZE + 1 && unalignedBytes[0] == 0));
+
+ byte[] bytes;
+ if (unalignedBytes.length < E2EE_EID_SIZE) {
+ bytes = new byte[E2EE_EID_SIZE];
+ System.arraycopy(
+ 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);
+ } else { // unalignedBytes.length == GattE2EE_EID_SIZE
+ bytes = unalignedBytes;
+ }
+ return ByteString.copyFrom(bytes);
+ }
+
+ /** Returns a secp160r1 curve spec. */
+ private static ECParameterSpec getCurveSpec() {
+ final BigInteger p = new BigInteger("ffffffffffffffffffffffffffffffff7fffffff", 16);
+ final BigInteger n = new BigInteger("0100000000000000000001f4c8f927aed3ca752257", 16);
+ final BigInteger a = new BigInteger("ffffffffffffffffffffffffffffffff7ffffffc", 16);
+ final BigInteger b = new BigInteger("1c97befc54bd7a8b65acf89f81d4d4adc565fa45", 16);
+ final BigInteger gx = new BigInteger("4a96b5688ef573284664698968c38bb913cbfc82", 16);
+ final BigInteger gy = new BigInteger("23a628553168947d59dcc912042351377ac5fb32", 16);
+ final int h = 1;
+ ECFieldFp fp = new ECFieldFp(p);
+ EllipticCurve spec = new EllipticCurve(fp, a, b);
+ ECPoint g = new ECPoint(gx, gy);
+ return new ECParameterSpec(spec, g, n, h);
+ }
+
+ /** Returns the scalar multiplication result of k*p in Fp. */
+ private static ECPoint multiplyPoint(ECPoint p, BigInteger k) {
+ ECPoint r = ECPoint.POINT_INFINITY;
+ ECPoint s = p;
+ BigInteger kModP = k.mod(P);
+ int length = kModP.bitLength();
+ for (int i = 0; i <= length - 1; i++) {
+ if (kModP.mod(TWO).byteValue() == 1) {
+ r = addPoint(r, s);
+ }
+ s = doublePoint(s);
+ kModP = kModP.divide(TWO);
+ }
+ return r;
+ }
+
+ /** Returns the point addition r+s in Fp. */
+ private static ECPoint addPoint(ECPoint r, ECPoint s) {
+ if (r.equals(s)) {
+ return doublePoint(r);
+ } else if (r.equals(ECPoint.POINT_INFINITY)) {
+ return s;
+ } else if (s.equals(ECPoint.POINT_INFINITY)) {
+ return r;
+ }
+ BigInteger slope =
+ r.getAffineY()
+ .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 y = s.getAffineY().negate().mod(P);
+ y = y.add(slope.multiply(s.getAffineX().subtract(x))).mod(P);
+ return new ECPoint(x, y);
+ }
+
+ /** Returns the point doubling 2*r in Fp. */
+ private static ECPoint doublePoint(ECPoint r) {
+ if (r.equals(ECPoint.POINT_INFINITY)) {
+ return r;
+ }
+ BigInteger slope = r.getAffineX().pow(2).multiply(THREE);
+ 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);
+ return new ECPoint(x, y);
+ }
+
+ private E2eeCalculator() {
+ }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/utils/Logger.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/utils/Logger.java
new file mode 100644
index 0000000..794f100
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/utils/Logger.java
@@ -0,0 +1,50 @@
+/*
+ * 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.utils;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.google.errorprone.annotations.FormatMethod;
+
+/**
+ * The base context for a logging statement.
+ */
+public class Logger {
+ private final String mString;
+
+ public Logger(String tag) {
+ this.mString = tag;
+ }
+
+ @FormatMethod
+ public void log(String message, Object... objects) {
+ log(null, message, objects);
+ }
+
+ /** Logs to the console. */
+ @FormatMethod
+ public void log(@Nullable Throwable exception, String message, Object... objects) {
+ if (exception == null) {
+ Log.i(mString, String.format(message, objects));
+ } else {
+ Log.w(mString, String.format(message, objects));
+ Log.w(mString, String.format("Cause: %s", exception));
+ }
+ }
+}
diff --git a/nearby/tests/multidevices/clients/test_support/snippet_helper/Android.bp b/nearby/tests/multidevices/clients/test_support/snippet_helper/Android.bp
new file mode 100644
index 0000000..697c88d
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/snippet_helper/Android.bp
@@ -0,0 +1,24 @@
+// 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_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/test_support/snippet_helper/src/com/google/android/mobly/snippet/util/SnippetEventHelper.kt b/nearby/tests/multidevices/clients/test_support/snippet_helper/src/com/google/android/mobly/snippet/util/SnippetEventHelper.kt
new file mode 100644
index 0000000..0dbcb57
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/snippet_helper/src/com/google/android/mobly/snippet/util/SnippetEventHelper.kt
@@ -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 com.google.android.mobly.snippet.util
+
+import android.os.Bundle
+import com.google.android.mobly.snippet.event.EventCache
+import com.google.android.mobly.snippet.event.SnippetEvent
+
+/**
+ * Posts an {@link SnippetEvent} to the event cache with data bundle [fill] by the given function.
+ *
+ * This is a helper function to make your client side codes more concise. Sample usage:
+ * ```
+ * postSnippetEvent(callbackId, "onReceiverFound") {
+ * putLong("discoveryTimeMs", discoveryTimeMs)
+ * putBoolean("isKnown", isKnown)
+ * }
+ * ```
+ *
+ * @param callbackId the callbackId passed to the {@link
+ * com.google.android.mobly.snippet.rpc.AsyncRpc} method.
+ * @param eventName the name of the event.
+ * @param fill the function to fill the data bundle.
+ */
+fun postSnippetEvent(callbackId: String, eventName: String, fill: Bundle.() -> Unit) {
+ val eventData = Bundle().apply(fill)
+ val snippetEvent = SnippetEvent(callbackId, eventName).apply { data.putAll(eventData) }
+ EventCache.getInstance().postEvent(snippetEvent)
+}
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/test_support/snippet_helper/tests/src/com/google/android/mobly/snippet/util/SnippetEventHelperTest.kt b/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/src/com/google/android/mobly/snippet/util/SnippetEventHelperTest.kt
new file mode 100644
index 0000000..641ab82
--- /dev/null
+++ b/nearby/tests/multidevices/clients/test_support/snippet_helper/tests/src/com/google/android/mobly/snippet/util/SnippetEventHelperTest.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.mobly.snippet.util
+
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.android.mobly.snippet.event.EventSnippet
+import com.google.android.mobly.snippet.util.Log
+import com.google.common.truth.Truth.assertThat
+import org.json.JSONObject
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+/** Robolectric tests for SnippetEventHelper.kt. */
+@RunWith(RobolectricTestRunner::class)
+class SnippetEventHelperTest {
+
+ @Test
+ fun testPostSnippetEvent_withDataBundle_writesEventCache() {
+ val testCallbackId = "test_1234"
+ val testEventName = "onTestEvent"
+ val testBundleDataStrKey = "testStrKey"
+ val testBundleDataStrValue = "testStrValue"
+ val testBundleDataIntKey = "testIntKey"
+ val testBundleDataIntValue = 777
+ val eventSnippet = EventSnippet()
+ Log.initLogTag(InstrumentationRegistry.getInstrumentation().context)
+
+ postSnippetEvent(testCallbackId, testEventName) {
+ putString(testBundleDataStrKey, testBundleDataStrValue)
+ putInt(testBundleDataIntKey, testBundleDataIntValue)
+ }
+
+ val event = eventSnippet.eventWaitAndGet(testCallbackId, testEventName, null)
+ assertThat(event.getJSONObject("data").toString())
+ .isEqualTo(
+ JSONObject()
+ .put(testBundleDataIntKey, testBundleDataIntValue)
+ .put(testBundleDataStrKey, testBundleDataStrValue)
+ .toString()
+ )
+ }
+}
diff --git a/nearby/tests/multidevices/host/Android.bp b/nearby/tests/multidevices/host/Android.bp
new file mode 100644
index 0000000..ff795e8
--- /dev/null
+++ b/nearby/tests/multidevices/host/Android.bp
@@ -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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Run the tests: atest -v CtsNearbyMultiDevicesTestSuite
+// Check go/run-nearby-mainline-e2e for more details.
+python_test_host {
+ name: "CtsNearbyMultiDevicesTestSuite",
+ main: "suite_main.py",
+ srcs: ["*.py"],
+ libs: ["NearbyMultiDevicesHostHelper"],
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+ test_options: {
+ unit_test: false,
+ },
+ data: [
+ // Package the snippet with the Mobly test.
+ ":NearbyMultiDevicesClientsSnippets",
+ // Package the data provider with the Mobly test.
+ ":NearbyFastPairSeekerDataProvider",
+ // Package the JSON metadata with the Mobly test.
+ "test_data/**/*",
+ ],
+}
+
+python_library_host {
+ name: "NearbyMultiDevicesHostHelper",
+ srcs: ["test_helper/*.py"],
+}
diff --git a/nearby/tests/multidevices/host/AndroidTest.xml b/nearby/tests/multidevices/host/AndroidTest.xml
new file mode 100644
index 0000000..5926cc1
--- /dev/null
+++ b/nearby/tests/multidevices/host/AndroidTest.xml
@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Config for CTS Nearby Mainline multi devices end-to-end test suite">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="wifi" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_secondary_user" />
+
+ <device name="device1">
+ <!-- For coverage to work, the APK should not be uninstalled until after coverage is pulled.
+ So it's a lot easier to install APKs outside the python code.
+ -->
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+ <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+ <option name="remount-system" value="true" />
+ <option name="push" value="NearbyMultiDevicesClientsSnippets.apk->/system/app/NearbyMultiDevicesClientsSnippets/NearbyMultiDevicesClientsSnippets.apk" />
+ <option name="push" value="NearbyFastPairSeekerDataProvider.apk->/system/app/NearbyFastPairSeekerDataProvider/NearbyFastPairSeekerDataProvider.apk" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RebootTargetPreparer" />
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
+ <option name="run-command" value="wm dismiss-keyguard" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.PythonVirtualenvPreparer">
+ <!-- Any python dependencies can be specified and will be installed with pip -->
+ <!-- TODO(b/225958696): Import python dependencies -->
+ <option name="dep-module" value="mobly" />
+ <option name="dep-module" value="retry" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <option name="force-skip-system-props" value="true" /> <!-- avoid restarting device -->
+ <option name="screen-always-on" value="on" />
+ <!-- List permissions requested by the APK: aapt d permissions <PATH_TO_YOUR_APK> -->
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.BLUETOOTH" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_ADMIN" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_ADVERTISE" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_CONNECT" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_PRIVILEGED" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_SCAN" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.INTERNET" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.GET_ACCOUNTS" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.WRITE_SECURE_SETTINGS" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.REORDER_TASKS" />
+ </target_preparer>
+ </device>
+ <device name="device2">
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+ <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+ <option name="remount-system" value="true" />
+ <option name="push" value="NearbyMultiDevicesClientsSnippets.apk->/system/app/NearbyMultiDevicesClientsSnippets/NearbyMultiDevicesClientsSnippets.apk" />
+ <option name="push" value="NearbyFastPairSeekerDataProvider.apk->/system/app/NearbyFastPairSeekerDataProvider/NearbyFastPairSeekerDataProvider.apk" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RebootTargetPreparer" />
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
+ <option name="run-command" value="wm dismiss-keyguard" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <option name="force-skip-system-props" value="true" /> <!-- avoid restarting device -->
+ <option name="screen-always-on" value="on" />
+ <!-- List permissions requested by the APK: aapt d permissions <PATH_TO_YOUR_APK> -->
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.BLUETOOTH" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_ADMIN" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_ADVERTISE" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_CONNECT" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_PRIVILEGED" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.BLUETOOTH_SCAN" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.INTERNET" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.GET_ACCOUNTS" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.WRITE_SECURE_SETTINGS" />
+ <option
+ name="run-command"
+ value="pm grant android.nearby.multidevices android.permission.REORDER_TASKS" />
+ </target_preparer>
+ </device>
+
+ <test class="com.android.tradefed.testtype.mobly.MoblyBinaryHostTest">
+ <!-- The mobly-par-file-name should match the module name -->
+ <option name="mobly-par-file-name" value="CtsNearbyMultiDevicesTestSuite" />
+ <!-- Timeout limit in milliseconds for all test cases of the python binary -->
+ <option name="mobly-test-timeout" value="60000" />
+ </test>
+</configuration>
+
diff --git a/nearby/tests/multidevices/host/initial_pairing_test.py b/nearby/tests/multidevices/host/initial_pairing_test.py
new file mode 100644
index 0000000..1a49045
--- /dev/null
+++ b/nearby/tests/multidevices/host/initial_pairing_test.py
@@ -0,0 +1,62 @@
+# 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.
+
+"""CTS-V Nearby Mainline Fast Pair end-to-end test case: initial pairing test."""
+
+from test_helper import constants
+from test_helper import fast_pair_base_test
+
+# The model ID to simulate on provider side.
+PROVIDER_SIMULATOR_MODEL_ID = constants.DEFAULT_MODEL_ID
+# The public key to simulate as registered headsets.
+PROVIDER_SIMULATOR_ANTI_SPOOFING_KEY = constants.DEFAULT_ANTI_SPOOFING_KEY
+# The anti-spoof key device metadata JSON file for data provider at seeker side.
+PROVIDER_SIMULATOR_KDM_JSON_FILE = constants.DEFAULT_KDM_JSON_FILE
+
+# Time in seconds for events waiting.
+SETUP_TIMEOUT_SEC = constants.SETUP_TIMEOUT_SEC
+BECOME_DISCOVERABLE_TIMEOUT_SEC = constants.BECOME_DISCOVERABLE_TIMEOUT_SEC
+START_ADVERTISING_TIMEOUT_SEC = constants.START_ADVERTISING_TIMEOUT_SEC
+HALF_SHEET_POPUP_TIMEOUT_SEC = constants.HALF_SHEET_POPUP_TIMEOUT_SEC
+MANAGE_ACCOUNT_DEVICE_TIMEOUT_SEC = constants.AVERAGE_PAIRING_TIMEOUT_SEC * 2
+
+
+class InitialPairingTest(fast_pair_base_test.FastPairBaseTest):
+ """Fast Pair initial pairing test."""
+
+ def setup_test(self) -> None:
+ super().setup_test()
+ self._provider.start_model_id_advertising(PROVIDER_SIMULATOR_MODEL_ID,
+ PROVIDER_SIMULATOR_ANTI_SPOOFING_KEY)
+ self._provider.wait_for_discoverable_mode(BECOME_DISCOVERABLE_TIMEOUT_SEC)
+ self._provider.wait_for_advertising_start(START_ADVERTISING_TIMEOUT_SEC)
+ self._seeker.put_anti_spoof_key_device_metadata(PROVIDER_SIMULATOR_MODEL_ID,
+ PROVIDER_SIMULATOR_KDM_JSON_FILE)
+ self._seeker.set_fast_pair_scan_enabled(True)
+
+ # TODO(b/214015364): Remove Bluetooth bound on both sides ("Forget device").
+ def teardown_test(self) -> None:
+ self._seeker.set_fast_pair_scan_enabled(False)
+ self._provider.teardown_provider_simulator()
+ self._seeker.dismiss_halfsheet()
+ super().teardown_test()
+
+ def test_seeker_initial_pair_provider(self) -> None:
+ self._seeker.wait_and_assert_halfsheet_showed(
+ timeout_seconds=HALF_SHEET_POPUP_TIMEOUT_SEC,
+ expected_model_id=PROVIDER_SIMULATOR_MODEL_ID)
+ self._seeker.start_pairing()
+ self._seeker.wait_and_assert_account_device(
+ get_account_key_from_provider=self._provider.get_latest_received_account_key,
+ timeout_seconds=MANAGE_ACCOUNT_DEVICE_TIMEOUT_SEC)
diff --git a/nearby/tests/multidevices/host/seeker_discover_provider_test.py b/nearby/tests/multidevices/host/seeker_discover_provider_test.py
new file mode 100644
index 0000000..6356595
--- /dev/null
+++ b/nearby/tests/multidevices/host/seeker_discover_provider_test.py
@@ -0,0 +1,52 @@
+# 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.
+
+"""CTS-V Nearby Mainline Fast Pair end-to-end test case: seeker can discover the provider."""
+
+from test_helper import constants
+from test_helper import fast_pair_base_test
+
+# The model ID to simulate on provider side.
+PROVIDER_SIMULATOR_MODEL_ID = constants.DEFAULT_MODEL_ID
+# The public key to simulate as registered headsets.
+PROVIDER_SIMULATOR_ANTI_SPOOFING_KEY = constants.DEFAULT_ANTI_SPOOFING_KEY
+
+# Time in seconds for events waiting.
+BECOME_DISCOVERABLE_TIMEOUT_SEC = constants.BECOME_DISCOVERABLE_TIMEOUT_SEC
+START_ADVERTISING_TIMEOUT_SEC = constants.START_ADVERTISING_TIMEOUT_SEC
+SCAN_TIMEOUT_SEC = constants.SCAN_TIMEOUT_SEC
+
+
+class SeekerDiscoverProviderTest(fast_pair_base_test.FastPairBaseTest):
+ """Fast Pair seeker discover provider test."""
+
+ def setup_test(self) -> None:
+ super().setup_test()
+ self._provider.start_model_id_advertising(
+ PROVIDER_SIMULATOR_MODEL_ID, PROVIDER_SIMULATOR_ANTI_SPOOFING_KEY)
+ self._provider.wait_for_discoverable_mode(BECOME_DISCOVERABLE_TIMEOUT_SEC)
+ self._provider.wait_for_advertising_start(START_ADVERTISING_TIMEOUT_SEC)
+ self._seeker.start_scan()
+
+ def teardown_test(self) -> None:
+ self._seeker.stop_scan()
+ self._provider.teardown_provider_simulator()
+ super().teardown_test()
+
+ def test_seeker_start_scanning_find_provider(self) -> None:
+ provider_ble_mac_address = self._provider.get_ble_mac_address()
+ self._seeker.wait_and_assert_provider_found(
+ timeout_seconds=SCAN_TIMEOUT_SEC,
+ expected_model_id=PROVIDER_SIMULATOR_MODEL_ID,
+ expected_ble_mac_address=provider_ble_mac_address)
diff --git a/nearby/tests/multidevices/host/seeker_show_halfsheet_test.py b/nearby/tests/multidevices/host/seeker_show_halfsheet_test.py
new file mode 100644
index 0000000..f6561e5
--- /dev/null
+++ b/nearby/tests/multidevices/host/seeker_show_halfsheet_test.py
@@ -0,0 +1,56 @@
+# 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.
+
+"""CTS-V Nearby Mainline Fast Pair end-to-end test case: seeker show half sheet UI."""
+
+from test_helper import constants
+from test_helper import fast_pair_base_test
+
+# The model ID to simulate on provider side.
+PROVIDER_SIMULATOR_MODEL_ID = constants.DEFAULT_MODEL_ID
+# The public key to simulate as registered headsets.
+PROVIDER_SIMULATOR_ANTI_SPOOFING_KEY = constants.DEFAULT_ANTI_SPOOFING_KEY
+# The anti-spoof key device metadata JSON file for data provider at seeker side.
+PROVIDER_SIMULATOR_KDM_JSON_FILE = constants.DEFAULT_KDM_JSON_FILE
+
+# Time in seconds for events waiting.
+SETUP_TIMEOUT_SEC = constants.SETUP_TIMEOUT_SEC
+BECOME_DISCOVERABLE_TIMEOUT_SEC = constants.BECOME_DISCOVERABLE_TIMEOUT_SEC
+START_ADVERTISING_TIMEOUT_SEC = constants.START_ADVERTISING_TIMEOUT_SEC
+HALF_SHEET_POPUP_TIMEOUT_SEC = constants.HALF_SHEET_POPUP_TIMEOUT_SEC
+
+
+class SeekerShowHalfSheetTest(fast_pair_base_test.FastPairBaseTest):
+ """Fast Pair seeker show half sheet UI test."""
+
+ def setup_test(self) -> None:
+ super().setup_test()
+ self._provider.start_model_id_advertising(PROVIDER_SIMULATOR_MODEL_ID,
+ PROVIDER_SIMULATOR_ANTI_SPOOFING_KEY)
+ self._provider.wait_for_discoverable_mode(BECOME_DISCOVERABLE_TIMEOUT_SEC)
+ self._provider.wait_for_advertising_start(START_ADVERTISING_TIMEOUT_SEC)
+ self._seeker.put_anti_spoof_key_device_metadata(PROVIDER_SIMULATOR_MODEL_ID,
+ PROVIDER_SIMULATOR_KDM_JSON_FILE)
+ self._seeker.set_fast_pair_scan_enabled(True)
+
+ def teardown_test(self) -> None:
+ self._seeker.set_fast_pair_scan_enabled(False)
+ self._provider.teardown_provider_simulator()
+ self._seeker.dismiss_halfsheet()
+ super().teardown_test()
+
+ def test_seeker_show_half_sheet(self) -> None:
+ self._seeker.wait_and_assert_halfsheet_showed(
+ timeout_seconds=HALF_SHEET_POPUP_TIMEOUT_SEC,
+ expected_model_id=PROVIDER_SIMULATOR_MODEL_ID)
diff --git a/nearby/tests/multidevices/host/suite_main.py b/nearby/tests/multidevices/host/suite_main.py
new file mode 100644
index 0000000..4f5d48c
--- /dev/null
+++ b/nearby/tests/multidevices/host/suite_main.py
@@ -0,0 +1,41 @@
+# 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.
+
+"""The entry point for Nearby Mainline multi devices end-to-end test suite."""
+
+import logging
+import sys
+
+from mobly import suite_runner
+
+import initial_pairing_test
+import seeker_discover_provider_test
+import seeker_show_halfsheet_test
+
+_BOOTSTRAP_LOGGING_FILENAME = '/tmp/nearby_multi_devices_test_suite_log.txt'
+_TEST_CLASSES_LIST = [
+ seeker_discover_provider_test.SeekerDiscoverProviderTest,
+ seeker_show_halfsheet_test.SeekerShowHalfSheetTest,
+ initial_pairing_test.InitialPairingTest,
+]
+
+
+def _valid_argument(arg: str) -> bool:
+ return arg.startswith(('--config', '-c', '--tests', '--test_case'))
+
+
+if __name__ == '__main__':
+ logging.basicConfig(filename=_BOOTSTRAP_LOGGING_FILENAME, level=logging.INFO)
+ suite_runner.run_suite(argv=[arg for arg in sys.argv if _valid_argument(arg)],
+ test_classes=_TEST_CLASSES_LIST)
diff --git a/nearby/tests/multidevices/host/test_data/fastpair/pixelbuds-a_account_devicemeta_json.txt b/nearby/tests/multidevices/host/test_data/fastpair/pixelbuds-a_account_devicemeta_json.txt
new file mode 100644
index 0000000..d3deb40
--- /dev/null
+++ b/nearby/tests/multidevices/host/test_data/fastpair/pixelbuds-a_account_devicemeta_json.txt
@@ -0,0 +1,47 @@
+[
+ {
+ "account_key": "BPy5AaSyMfrFvMNgr6f7GA==",
+ "sha256_account_key_public_address": "jNGRz+Ni6ZuLd8hVF3lmGoJnF5byXBUyVi9CmnrF1so=",
+ "fast_pair_device_metadata": {
+ "image_url": "https:\/\/lh3.googleusercontent.com\/2PffmZiopo2AjT8sshX0Se3jV-91cp4yOCIay2bBvZqKoKGVy5B4uyzdHsde6UrUSGaoCqV-h4edd5ZljA4oSGc",
+ "intent_uri": "intent:#Intent;action=com.google.android.gms.nearby.discovery%3AACTION_MAGIC_PAIR;package=com.google.android.gms;component=com.google.android.gms\/.nearby.discovery.service.DiscoveryService;S.com.google.android.gms.nearby.discovery%3AEXTRA_COMPANION_APP=com.google.android.apps.wearables.maestro.companion;end",
+ "ble_tx_power": 0,
+ "trigger_distance": 0,
+ "device_type": 0,
+ "left_bud_url": "",
+ "right_bud_url": "",
+ "case_url": "",
+ "initial_notification_description": "Tap to pair. Earbuds will be tied to %s",
+ "initial_notification_description_no_account": "Tap to pair with this device",
+ "initial_pairing_description": "Pixel Buds A-Series will appear on devices linked with ericth.nearby.dogfood@gmail.com",
+ "connect_success_companion_app_installed": "Your device is ready to be set up",
+ "connect_success_companion_app_not_installed": "Download the device app on Google Play to see all available features",
+ "subsequent_pairing_description": "Connect %s to this phone",
+ "retroactive_pairing_description": "Save device to %s for faster pairing to your other devices",
+ "wait_launch_companion_app_description": "This will take a few moments",
+ "fail_connect_go_to_settings_description": "Try manually pairing to the device by going to Settings"
+ },
+ "fast_pair_discovery_item": {
+ "id": "",
+ "mac_address": "",
+ "action_url": "intent:#Intent;action=com.google.android.gms.nearby.discovery%3AACTION_MAGIC_PAIR;package=com.google.android.gms;component=com.google.android.gms\/.nearby.discovery.service.DiscoveryService;S.com.google.android.gms.nearby.discovery%3AEXTRA_COMPANION_APP=com.google.android.apps.wearables.maestro.companion;end",
+ "device_name": "",
+ "title": "Pixel Buds A-Series",
+ "description": "Tap to pair with this device",
+ "display_url": "",
+ "last_observation_timestamp_millis": 0,
+ "first_observation_timestamp_millis": 0,
+ "state": 1,
+ "action_url_type": 2,
+ "rssi": 0,
+ "pending_app_install_timestamp_millis": 0,
+ "tx_power": 0,
+ "app_name": "",
+ "package_name": "",
+ "trigger_id": "",
+ "icon_png": "",
+ "icon_fife_url": "https:\/\/lh3.googleusercontent.com\/2PffmZiopo2AjT8sshX0Se3jV-91cp4yOCIay2bBvZqKoKGVy5B4uyzdHsde6UrUSGaoCqV-h4edd5ZljA4oSGc",
+ "authentication_public_key_secp256r1": "z+grhW8lWVA34JUQhXOxMrk1WqVy+VpEDd2K+01ZJvS6KdV0OUg7FRMzq+ITuOqKO\/2TIRKEAEfMKdyk2Ob1Vw=="
+ }
+ }
+]
\ No newline at end of file
diff --git a/nearby/tests/multidevices/host/test_data/fastpair/pixelbuds-a_antispoofkey_devicemeta_json.txt b/nearby/tests/multidevices/host/test_data/fastpair/pixelbuds-a_antispoofkey_devicemeta_json.txt
new file mode 100644
index 0000000..3611b03
--- /dev/null
+++ b/nearby/tests/multidevices/host/test_data/fastpair/pixelbuds-a_antispoofkey_devicemeta_json.txt
@@ -0,0 +1,28 @@
+{
+ "anti_spoofing_public_key_str": "z+grhW8lWVA34JUQhXOxMrk1WqVy+VpEDd2K+01ZJvS6KdV0OUg7FRMzq+ITuOqKO\/2TIRKEAEfMKdyk2Ob1Vw==",
+ "fast_pair_device_metadata": {
+ "image_url": "https:\/\/lh3.googleusercontent.com\/2PffmZiopo2AjT8sshX0Se3jV-91cp4yOCIay2bBvZqKoKGVy5B4uyzdHsde6UrUSGaoCqV-h4edd5ZljA4oSGc",
+ "intent_uri": "intent:#Intent;action=com.google.android.gms.nearby.discovery%3AACTION_MAGIC_PAIR;package=com.google.android.gms;component=com.google.android.gms\/.nearby.discovery.service.DiscoveryService;S.com.google.android.gms.nearby.discovery%3AEXTRA_COMPANION_APP=com.google.android.apps.wearables.maestro.companion;end",
+ "ble_tx_power": -11,
+ "trigger_distance": 0.6000000238418579,
+ "device_type": 7,
+ "name": "Pixel Buds A-Series",
+ "left_bud_url": "https:\/\/lh3.googleusercontent.com\/O8SVJ5E7CXUkpkym7ibZbp6wypuO7HaTFcslT_FjmEzJX4KHoIY_kzLTdK2kwJXiDBgg8cC__sG-JJ5aVnQtFjQ",
+ "right_bud_url": "https:\/\/lh3.googleusercontent.com\/X_FsRmEKH_fgKzvopyrlyWJAdczRel42Tih7p9-e-U48gBTaggGVQx70K27TzlqIaqYVuaNpTnGoUsKIgiy4WA",
+ "case_url": "https:\/\/lh3.googleusercontent.com\/mNZ7CGplQSpZhoY79jXDQU4B65eY2f0SndnYZLk1PSm8zKTYeRU7REmrLL_pptD6HpVI2F_oQ6xhhtZKOvB8EQ",
+ "initial_notification_description": "Tap to pair. Earbuds will be tied to %s",
+ "initial_notification_description_no_account": "Tap to pair with this device",
+ "open_companion_app_description": "Tap to finish setup",
+ "update_companion_app_description": "Tap to update device settings and finish setup",
+ "download_companion_app_description": "Tap to download device app on Google Play and see all features",
+ "unable_to_connect_title": "Unable to connect",
+ "unable_to_connect_description": "Try manually pairing to the device",
+ "initial_pairing_description": "%s will appear on devices linked with %s",
+ "connect_success_companion_app_installed": "Your device is ready to be set up",
+ "connect_success_companion_app_not_installed": "Download the device app on Google Play to see all available features",
+ "subsequent_pairing_description": "Connect %s to this phone",
+ "retroactive_pairing_description": "Save device to %s for faster pairing to your other devices",
+ "wait_launch_companion_app_description": "This will take a few moments",
+ "fail_connect_go_to_settings_description": "Try manually pairing to the device by going to Settings"
+ }
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/host/test_data/fastpair/simulator_account_devicemeta_json.txt b/nearby/tests/multidevices/host/test_data/fastpair/simulator_account_devicemeta_json.txt
new file mode 100644
index 0000000..ed60860
--- /dev/null
+++ b/nearby/tests/multidevices/host/test_data/fastpair/simulator_account_devicemeta_json.txt
@@ -0,0 +1,47 @@
+[
+ {
+ "account_key": "BPy5AaSyMfrFvMNgr6f7GA==",
+ "sha256_account_key_public_address": "jNGRz+Ni6ZuLd8hVF3lmGoJnF5byXBUyVi9CmnrF1so=",
+ "fast_pair_device_metadata": {
+ "ble_tx_power": 0,
+ "case_url": "",
+ "connect_success_companion_app_installed": "Your device is ready to be set up",
+ "connect_success_companion_app_not_installed": "Download the device app on Google Play to see all available features",
+ "device_type": 0,
+ "fail_connect_go_to_settings_description": "Try manually pairing to the device by going to Settings",
+ "image_url": "https://lh3.googleusercontent.com/2PffmZiopo2AjT8sshX0Se3jV-91cp4yOCIay2bBvZqKoKGVy5B4uyzdHsde6UrUSGaoCqV-h4edd5ZljA4oSGc",
+ "initial_notification_description": "Tap to pair. Earbuds will be tied to %s",
+ "initial_notification_description_no_account": "Tap to pair with this device",
+ "initial_pairing_description": "Pixel Buds A-Series will appear on devices linked with ericth.nearby.dogfood@gmail.com",
+ "intent_uri": "intent:#Intent;action=com.google.android.gms.nearby.discovery%3AACTION_MAGIC_PAIR;package=com.google.android.gms;component=com.google.android.gms/.nearby.discovery.service.DiscoveryService;S.com.google.android.gms.nearby.discovery%3AEXTRA_COMPANION_APP=com.google.android.apps.wearables.maestro.companion;end",
+ "left_bud_url": "",
+ "retroactive_pairing_description": "Save device to %s for faster pairing to your other devices",
+ "right_bud_url": "",
+ "subsequent_pairing_description": "Connect %s to this phone",
+ "trigger_distance": 0,
+ "wait_launch_companion_app_description": "This will take a few moments"
+ },
+ "fast_pair_discovery_item": {
+ "action_url": "intent:#Intent;action=com.google.android.gms.nearby.discovery%3AACTION_MAGIC_PAIR;package=com.google.android.gms;component=com.google.android.gms/.nearby.discovery.service.DiscoveryService;S.com.google.android.gms.nearby.discovery%3AEXTRA_COMPANION_APP=com.google.android.apps.wearables.maestro.companion;end",
+ "action_url_type": 2,
+ "app_name": "",
+ "authentication_public_key_secp256r1": "z+grhW8lWVA34JUQhXOxMrk1WqVy+VpEDd2K+01ZJvS6KdV0OUg7FRMzq+ITuOqKO/2TIRKEAEfMKdyk2Ob1Vw==",
+ "description": "Tap to pair with this device",
+ "device_name": "",
+ "display_url": "",
+ "first_observation_timestamp_millis": 0,
+ "icon_fife_url": "https://lh3.googleusercontent.com/2PffmZiopo2AjT8sshX0Se3jV-91cp4yOCIay2bBvZqKoKGVy5B4uyzdHsde6UrUSGaoCqV-h4edd5ZljA4oSGc",
+ "icon_png": "",
+ "id": "",
+ "last_observation_timestamp_millis": 0,
+ "mac_address": "",
+ "package_name": "",
+ "pending_app_install_timestamp_millis": 0,
+ "rssi": 0,
+ "state": 1,
+ "title": "Pixel Buds A-Series",
+ "trigger_id": "",
+ "tx_power": 0
+ }
+ }
+]
\ No newline at end of file
diff --git a/nearby/tests/multidevices/host/test_data/fastpair/simulator_antispoofkey_devicemeta_json.txt b/nearby/tests/multidevices/host/test_data/fastpair/simulator_antispoofkey_devicemeta_json.txt
new file mode 100644
index 0000000..fc9706a
--- /dev/null
+++ b/nearby/tests/multidevices/host/test_data/fastpair/simulator_antispoofkey_devicemeta_json.txt
@@ -0,0 +1,28 @@
+{
+ "anti_spoofing_public_key_str": "sjp\/AOS7+VnTCaueeWorjdeJ8Nc32EOmpe\/QRhzY9+cMNELU1QA3jzgvUXdWW73nl6+EN01eXtLBu2Fw9CGmfA==",
+ "fast_pair_device_metadata": {
+ "ble_tx_power": -10,
+ "case_url": "https://lh3.googleusercontent.com/mNZ7CGplQSpZhoY79jXDQU4B65eY2f0SndnYZLk1PSm8zKTYeRU7REmrLL_pptD6HpVI2F_oQ6xhhtZKOvB8EQ",
+ "connect_success_companion_app_installed": "Your device is ready to be set up",
+ "connect_success_companion_app_not_installed": "Download the device app on Google Play to see all available features",
+ "device_type": 7,
+ "download_companion_app_description": "Tap to download device app on Google Play and see all features",
+ "fail_connect_go_to_settings_description": "Try manually pairing to the device by going to Settings",
+ "image_url": "https://lh3.googleusercontent.com/THpAzISZGa5F86cMsBcTPhRWefBPc5dorBxWdOPCGvbFg6ZMHUjFuE-4kbLuoLoIMHf3Fd8jUvvcxnjp_Q",
+ "initial_notification_description": "Tap to pair. Earbuds will be tied to %s",
+ "initial_notification_description_no_account": "Tap to pair with this device",
+ "initial_pairing_description": "%s will appear on devices linked with %s",
+ "intent_uri": "intent:#Intent;action=com.google.android.gms.nearby.discovery%3AACTION_MAGIC_PAIR;package=com.google.android.gms;component=com.google.android.gms/.nearby.discovery.service.DiscoveryService;S.com.google.android.gms.nearby.discovery%3AEXTRA_COMPANION_APP=com.google.android.testapp;end",
+ "left_bud_url": "https://lh3.googleusercontent.com/O8SVJ5E7CXUkpkym7ibZbp6wypuO7HaTFcslT_FjmEzJX4KHoIY_kzLTdK2kwJXiDBgg8cC__sG-JJ5aVnQtFjQ",
+ "name": "Fast Pair Provider Simulator",
+ "open_companion_app_description": "Tap to finish setup",
+ "retroactive_pairing_description": "Save device to %s for faster pairing to your other devices",
+ "right_bud_url": "https://lh3.googleusercontent.com/X_FsRmEKH_fgKzvopyrlyWJAdczRel42Tih7p9-e-U48gBTaggGVQx70K27TzlqIaqYVuaNpTnGoUsKIgiy4WA",
+ "subsequent_pairing_description": "Connect %s to this phone",
+ "trigger_distance": 0.6000000238418579,
+ "unable_to_connect_description": "Try manually pairing to the device",
+ "unable_to_connect_title": "Unable to connect",
+ "update_companion_app_description": "Tap to update device settings and finish setup",
+ "wait_launch_companion_app_description": "This will take a few moments"
+ }
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/host/test_helper/__init__.py b/nearby/tests/multidevices/host/test_helper/__init__.py
new file mode 100644
index 0000000..b0cae91
--- /dev/null
+++ b/nearby/tests/multidevices/host/test_helper/__init__.py
@@ -0,0 +1,13 @@
+# 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.
diff --git a/nearby/tests/multidevices/host/test_helper/constants.py b/nearby/tests/multidevices/host/test_helper/constants.py
new file mode 100644
index 0000000..342be8f
--- /dev/null
+++ b/nearby/tests/multidevices/host/test_helper/constants.py
@@ -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.
+
+# Default model ID to simulate on provider side.
+DEFAULT_MODEL_ID = '00000c'
+
+# Default public key to simulate as registered headsets.
+DEFAULT_ANTI_SPOOFING_KEY = 'Cbj9eCJrTdDgSYxLkqtfADQi86vIaMvxJsQ298sZYWE='
+
+# Default anti-spoof Key Device Metadata JSON file for data provider at seeker side.
+DEFAULT_KDM_JSON_FILE = 'simulator_antispoofkey_devicemeta_json.txt'
+
+# Time in seconds for events waiting according to Fast Pair certification guidelines:
+# https://developers.google.com/nearby/fast-pair/certification-guideline
+SETUP_TIMEOUT_SEC = 5
+BECOME_DISCOVERABLE_TIMEOUT_SEC = 10
+START_ADVERTISING_TIMEOUT_SEC = 5
+SCAN_TIMEOUT_SEC = 5
+HALF_SHEET_POPUP_TIMEOUT_SEC = 5
+AVERAGE_PAIRING_TIMEOUT_SEC = 12
+
+# The phone to simulate Fast Pair provider (like headphone) needs changes in Android system:
+# 1. System permission check removal
+# 2. Adjusts Bluetooth profile configurations
+# The build fingerprint of the custom ROM for Fast Pair provider simulator.
+FAST_PAIR_PROVIDER_SIMULATOR_BUILD_FINGERPRINT = (
+ 'google/bramble/bramble:Tiramisu/MASTER/eng.hylo.20211019.091550:userdebug/dev-keys')
diff --git a/nearby/tests/multidevices/host/test_helper/event_helper.py b/nearby/tests/multidevices/host/test_helper/event_helper.py
new file mode 100644
index 0000000..8abf05c
--- /dev/null
+++ b/nearby/tests/multidevices/host/test_helper/event_helper.py
@@ -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.
+
+"""This is a shared library to help handling Mobly event waiting logic."""
+
+import time
+from typing import Callable
+
+from mobly.controllers.android_device_lib import callback_handler
+from mobly.controllers.android_device_lib import snippet_event
+
+# Abbreviations for common use type
+CallbackHandler = callback_handler.CallbackHandler
+SnippetEvent = snippet_event.SnippetEvent
+
+# Type definition for the callback functions to make code formatted nicely
+OnReceivedCallback = Callable[[SnippetEvent, int], bool]
+OnWaitingCallback = Callable[[int], None]
+OnMissedCallback = Callable[[], None]
+
+
+def wait_callback_event(callback_event_handler: CallbackHandler,
+ event_name: str, timeout_seconds: int,
+ on_received: OnReceivedCallback,
+ on_waiting: OnWaitingCallback,
+ on_missed: OnMissedCallback) -> None:
+ """Waits until the matched event has been received or timeout.
+
+ Here we keep waitAndGet for event callback from EventSnippet.
+ We loop until over timeout_seconds instead of directly
+ waitAndGet(timeout=teardown_timeout_seconds). Because there is
+ MAX_TIMEOUT limitation in callback_handler of Mobly.
+
+ Args:
+ callback_event_handler: Mobly callback events handler.
+ event_name: the specific name of the event to wait.
+ timeout_seconds: the number of seconds to wait before giving up.
+ on_received: calls when event received, return false to keep waiting.
+ on_waiting: calls when waitAndGet timeout.
+ on_missed: calls when giving up.
+ """
+ start_time = time.perf_counter()
+ deadline = start_time + timeout_seconds
+ while time.perf_counter() < deadline:
+ remaining_time_sec = min(callback_handler.DEFAULT_TIMEOUT,
+ deadline - time.perf_counter())
+ try:
+ event = callback_event_handler.waitAndGet(
+ event_name, timeout=remaining_time_sec)
+ except callback_handler.TimeoutError:
+ elapsed_time = int(time.perf_counter() - start_time)
+ on_waiting(elapsed_time)
+ else:
+ elapsed_time = int(time.perf_counter() - start_time)
+ if on_received(event, elapsed_time):
+ break
+ else:
+ on_missed()
diff --git a/nearby/tests/multidevices/host/test_helper/fast_pair_base_test.py b/nearby/tests/multidevices/host/test_helper/fast_pair_base_test.py
new file mode 100644
index 0000000..8b84839
--- /dev/null
+++ b/nearby/tests/multidevices/host/test_helper/fast_pair_base_test.py
@@ -0,0 +1,75 @@
+# 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.
+
+"""Base for all Nearby Mainline Fast Pair end-to-end test cases."""
+
+from typing import List, Tuple
+
+from mobly import base_test
+from mobly import signals
+from mobly.controllers import android_device
+
+from test_helper import constants
+from test_helper import fast_pair_provider_simulator
+from test_helper import fast_pair_seeker
+
+# Abbreviations for common use type.
+AndroidDevice = android_device.AndroidDevice
+FastPairProviderSimulator = fast_pair_provider_simulator.FastPairProviderSimulator
+FastPairSeeker = fast_pair_seeker.FastPairSeeker
+REQUIRED_BUILD_FINGERPRINT = constants.FAST_PAIR_PROVIDER_SIMULATOR_BUILD_FINGERPRINT
+
+
+class FastPairBaseTest(base_test.BaseTestClass):
+ """Base class for all Nearby Mainline Fast Pair end-to-end classes to inherit."""
+
+ _duts: List[AndroidDevice]
+ _provider: FastPairProviderSimulator
+ _seeker: FastPairSeeker
+
+ def setup_class(self) -> None:
+ super().setup_class()
+ self._duts = self.register_controller(android_device, min_number=2)
+
+ provider_ad, seeker_ad = self._check_devices_supported()
+ self._provider = FastPairProviderSimulator(provider_ad)
+ self._seeker = FastPairSeeker(seeker_ad)
+ self._provider.load_snippet()
+ self._seeker.load_snippet()
+
+ def setup_test(self) -> None:
+ super().setup_test()
+ self._provider.setup_provider_simulator(constants.SETUP_TIMEOUT_SEC)
+
+ def teardown_test(self) -> None:
+ super().teardown_test()
+ # Create per-test excepts of logcat.
+ for dut in self._duts:
+ dut.services.create_output_excerpts_all(self.current_test_info)
+
+ def _check_devices_supported(self) -> Tuple[AndroidDevice, AndroidDevice]:
+ # Assume the 1st phone is provider, the 2nd one is seeker.
+ provider_ad, seeker_ad = self._duts[:2]
+
+ for ad in self._duts:
+ if ad.build_info['build_fingerprint'] == REQUIRED_BUILD_FINGERPRINT:
+ if ad != provider_ad:
+ provider_ad, seeker_ad = seeker_ad, provider_ad
+ break
+ else:
+ raise signals.TestAbortClass(
+ f'None of phones has custom ROM ({REQUIRED_BUILD_FINGERPRINT}) for Fast Pair '
+ f'provider simulator. Skip all the test cases!')
+
+ return provider_ad, seeker_ad
diff --git a/nearby/tests/multidevices/host/test_helper/fast_pair_provider_simulator.py b/nearby/tests/multidevices/host/test_helper/fast_pair_provider_simulator.py
new file mode 100644
index 0000000..d6484fb
--- /dev/null
+++ b/nearby/tests/multidevices/host/test_helper/fast_pair_provider_simulator.py
@@ -0,0 +1,190 @@
+# 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.
+
+"""Fast Pair provider simulator role."""
+
+from mobly import asserts
+from mobly.controllers import android_device
+from mobly.controllers.android_device_lib import snippet_event
+import retry
+from typing import Optional
+
+from test_helper import event_helper
+
+# The package name of the provider simulator snippet.
+FP_PROVIDER_SIMULATOR_SNIPPETS_PACKAGE = 'android.nearby.multidevices'
+
+# Events reported from the provider simulator snippet.
+ON_A2DP_SINK_PROFILE_CONNECT_EVENT = 'onA2DPSinkProfileConnected'
+ON_SCAN_MODE_CHANGE_EVENT = 'onScanModeChange'
+ON_ADVERTISING_CHANGE_EVENT = 'onAdvertisingChange'
+
+# Target scan mode.
+DISCOVERABLE_MODE = 'DISCOVERABLE'
+
+# Abbreviations for common use type.
+AndroidDevice = android_device.AndroidDevice
+SnippetEvent = snippet_event.SnippetEvent
+wait_for_event = event_helper.wait_callback_event
+
+
+class FastPairProviderSimulator:
+ """A proxy for provider simulator snippet on the device."""
+
+ def __init__(self, ad: AndroidDevice) -> None:
+ self._ad = ad
+ self._ad.debug_tag = 'FastPairProviderSimulator'
+ self._provider_status_callback = None
+
+ def load_snippet(self) -> None:
+ """Starts the provider simulator snippet and connects.
+
+ Raises:
+ SnippetError: Illegal load operations are attempted.
+ """
+ self._ad.load_snippet(
+ name='fp', package=FP_PROVIDER_SIMULATOR_SNIPPETS_PACKAGE)
+
+ def setup_provider_simulator(self, timeout_seconds: int) -> None:
+ """Sets up the Fast Pair provider simulator.
+
+ Args:
+ timeout_seconds: The number of seconds to wait before giving up.
+ """
+ setup_status_callback = self._ad.fp.setupProviderSimulator()
+
+ def _on_a2dp_sink_profile_connect_event_received(_, elapsed_time: int) -> bool:
+ self._ad.log.info('Provider simulator connected to A2DP sink in %d seconds.',
+ elapsed_time)
+ return True
+
+ def _on_a2dp_sink_profile_connect_event_waiting(elapsed_time: int) -> None:
+ self._ad.log.info(
+ 'Still waiting "%s" event callback from provider side '
+ 'after %d seconds...', ON_A2DP_SINK_PROFILE_CONNECT_EVENT, elapsed_time)
+
+ def _on_a2dp_sink_profile_connect_event_missed() -> None:
+ asserts.fail(f'Timed out after {timeout_seconds} seconds waiting for '
+ f'the specific "{ON_A2DP_SINK_PROFILE_CONNECT_EVENT}" event.')
+
+ wait_for_event(
+ callback_event_handler=setup_status_callback,
+ event_name=ON_A2DP_SINK_PROFILE_CONNECT_EVENT,
+ timeout_seconds=timeout_seconds,
+ on_received=_on_a2dp_sink_profile_connect_event_received,
+ on_waiting=_on_a2dp_sink_profile_connect_event_waiting,
+ on_missed=_on_a2dp_sink_profile_connect_event_missed)
+
+ def start_model_id_advertising(self, model_id: str, anti_spoofing_key: str) -> None:
+ """Starts model id advertising for scanning and initial pairing.
+
+ Args:
+ model_id: A 3-byte hex string for seeker side to recognize the device (ex:
+ 0x00000C).
+ anti_spoofing_key: A public key for registered headsets.
+ """
+ self._ad.log.info(
+ 'Provider simulator starts advertising as model id "%s" with anti-spoofing key "%s".',
+ model_id, anti_spoofing_key)
+ self._provider_status_callback = (
+ self._ad.fp.startModelIdAdvertising(model_id, anti_spoofing_key))
+
+ def teardown_provider_simulator(self) -> None:
+ """Tears down the Fast Pair provider simulator."""
+ self._ad.fp.teardownProviderSimulator()
+
+ @retry.retry(tries=3)
+ def get_ble_mac_address(self) -> str:
+ """Gets Bluetooth low energy mac address of the provider simulator.
+
+ The BLE mac address will be set by the AdvertisingSet.getOwnAddress()
+ callback. This is the callback flow in the custom Android build. It takes
+ a while after advertising started so we use retry here to wait it.
+
+ Returns:
+ The BLE mac address of the Fast Pair provider simulator.
+ """
+ return self._ad.fp.getBluetoothLeAddress()
+
+ def wait_for_discoverable_mode(self, timeout_seconds: int) -> None:
+ """Waits onScanModeChange event to ensure provider is discoverable.
+
+ Args:
+ timeout_seconds: The number of seconds to wait before giving up.
+ """
+
+ def _on_scan_mode_change_event_received(
+ scan_mode_change_event: SnippetEvent, elapsed_time: int) -> bool:
+ scan_mode = scan_mode_change_event.data['mode']
+ self._ad.log.info(
+ 'Provider simulator changed the scan mode to %s in %d seconds.',
+ scan_mode, elapsed_time)
+ return scan_mode == DISCOVERABLE_MODE
+
+ def _on_scan_mode_change_event_waiting(elapsed_time: int) -> None:
+ self._ad.log.info(
+ 'Still waiting "%s" event callback from provider side '
+ 'after %d seconds...', ON_SCAN_MODE_CHANGE_EVENT, elapsed_time)
+
+ def _on_scan_mode_change_event_missed() -> None:
+ asserts.fail(f'Timed out after {timeout_seconds} seconds waiting for '
+ f'the specific "{ON_SCAN_MODE_CHANGE_EVENT}" event.')
+
+ wait_for_event(
+ callback_event_handler=self._provider_status_callback,
+ event_name=ON_SCAN_MODE_CHANGE_EVENT,
+ timeout_seconds=timeout_seconds,
+ on_received=_on_scan_mode_change_event_received,
+ on_waiting=_on_scan_mode_change_event_waiting,
+ on_missed=_on_scan_mode_change_event_missed)
+
+ def wait_for_advertising_start(self, timeout_seconds: int) -> None:
+ """Waits onAdvertisingChange event to ensure provider is advertising.
+
+ Args:
+ timeout_seconds: The number of seconds to wait before giving up.
+ """
+
+ def _on_advertising_mode_change_event_received(
+ scan_mode_change_event: SnippetEvent, elapsed_time: int) -> bool:
+ advertising_mode = scan_mode_change_event.data['isAdvertising']
+ self._ad.log.info(
+ 'Provider simulator changed the advertising mode to %s in %d seconds.',
+ advertising_mode, elapsed_time)
+ return advertising_mode
+
+ def _on_advertising_mode_change_event_waiting(elapsed_time: int) -> None:
+ self._ad.log.info(
+ 'Still waiting "%s" event callback from provider side '
+ 'after %d seconds...', ON_ADVERTISING_CHANGE_EVENT, elapsed_time)
+
+ def _on_advertising_mode_change_event_missed() -> None:
+ asserts.fail(f'Timed out after {timeout_seconds} seconds waiting for '
+ f'the specific "{ON_ADVERTISING_CHANGE_EVENT}" event.')
+
+ wait_for_event(
+ callback_event_handler=self._provider_status_callback,
+ event_name=ON_ADVERTISING_CHANGE_EVENT,
+ timeout_seconds=timeout_seconds,
+ on_received=_on_advertising_mode_change_event_received,
+ on_waiting=_on_advertising_mode_change_event_waiting,
+ on_missed=_on_advertising_mode_change_event_missed)
+
+ def get_latest_received_account_key(self) -> Optional[str]:
+ """Gets the latest account key received on the provider side.
+
+ Returns:
+ The account key received at provider side.
+ """
+ return self._ad.fp.getLatestReceivedAccountKey()
diff --git a/nearby/tests/multidevices/host/test_helper/fast_pair_seeker.py b/nearby/tests/multidevices/host/test_helper/fast_pair_seeker.py
new file mode 100644
index 0000000..64fc2f2
--- /dev/null
+++ b/nearby/tests/multidevices/host/test_helper/fast_pair_seeker.py
@@ -0,0 +1,186 @@
+# 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.
+
+"""Fast Pair seeker role."""
+
+import json
+from typing import Callable, Optional
+
+from mobly import asserts
+from mobly.controllers import android_device
+from mobly.controllers.android_device_lib import snippet_event
+
+from test_helper import event_helper
+from test_helper import utils
+
+# The package name of the Nearby Mainline Fast Pair seeker Mobly snippet.
+FP_SEEKER_SNIPPETS_PACKAGE = 'android.nearby.multidevices'
+
+# Events reported from the seeker snippet.
+ON_PROVIDER_FOUND_EVENT = 'onDiscovered'
+ON_MANAGE_ACCOUNT_DEVICE_EVENT = 'onManageAccountDevice'
+
+# Abbreviations for common use type.
+AndroidDevice = android_device.AndroidDevice
+JsonObject = utils.JsonObject
+ProviderAccountKeyCallable = Callable[[], Optional[str]]
+SnippetEvent = snippet_event.SnippetEvent
+wait_for_event = event_helper.wait_callback_event
+
+
+class FastPairSeeker:
+ """A proxy for seeker snippet on the device."""
+
+ def __init__(self, ad: AndroidDevice) -> None:
+ self._ad = ad
+ self._ad.debug_tag = 'MainlineFastPairSeeker'
+ self._scan_result_callback = None
+ self._pairing_result_callback = None
+
+ def load_snippet(self) -> None:
+ """Starts the seeker snippet and connects.
+
+ Raises:
+ SnippetError: Illegal load operations are attempted.
+ """
+ self._ad.load_snippet(name='fp', package=FP_SEEKER_SNIPPETS_PACKAGE)
+
+ def start_scan(self) -> None:
+ """Starts scanning to find Fast Pair provider devices."""
+ self._scan_result_callback = self._ad.fp.startScan()
+
+ def stop_scan(self) -> None:
+ """Stops the Fast Pair seeker scanning."""
+ self._ad.fp.stopScan()
+
+ def wait_and_assert_provider_found(self, timeout_seconds: int,
+ expected_model_id: str,
+ expected_ble_mac_address: str) -> None:
+ """Waits and asserts any onDiscovered event from the seeker.
+
+ Args:
+ timeout_seconds: The number of seconds to wait before giving up.
+ expected_model_id: The expected model ID of the remote Fast Pair provider
+ device.
+ expected_ble_mac_address: The expected BLE MAC address of the remote Fast
+ Pair provider device.
+ """
+
+ def _on_provider_found_event_received(provider_found_event: SnippetEvent,
+ elapsed_time: int) -> bool:
+ nearby_device_str = provider_found_event.data['device']
+ self._ad.log.info('Seeker discovered first provider(%s) in %d seconds.',
+ nearby_device_str, elapsed_time)
+ return expected_ble_mac_address in nearby_device_str
+
+ def _on_provider_found_event_waiting(elapsed_time: int) -> None:
+ self._ad.log.info(
+ 'Still waiting "%s" event callback from seeker side '
+ 'after %d seconds...', ON_PROVIDER_FOUND_EVENT, elapsed_time)
+
+ def _on_provider_found_event_missed() -> None:
+ asserts.fail(f'Timed out after {timeout_seconds} seconds waiting for '
+ f'the specific "{ON_PROVIDER_FOUND_EVENT}" event.')
+
+ wait_for_event(
+ callback_event_handler=self._scan_result_callback,
+ event_name=ON_PROVIDER_FOUND_EVENT,
+ timeout_seconds=timeout_seconds,
+ on_received=_on_provider_found_event_received,
+ on_waiting=_on_provider_found_event_waiting,
+ on_missed=_on_provider_found_event_missed)
+
+ def put_anti_spoof_key_device_metadata(self, model_id: str, kdm_json_file_name: str) -> None:
+ """Puts a model id to FastPairAntispoofKeyDeviceMetadata pair into test data cache.
+
+ Args:
+ model_id: A string of model id to be associated with.
+ kdm_json_file_name: The FastPairAntispoofKeyDeviceMetadata JSON object.
+ """
+ self._ad.log.info('Puts FastPairAntispoofKeyDeviceMetadata into test data cache for '
+ 'model id "%s".', model_id)
+ kdm_json_object = utils.load_json_fast_pair_test_data(kdm_json_file_name)
+ self._ad.fp.putAntispoofKeyDeviceMetadata(
+ model_id,
+ utils.serialize_as_simplified_json_str(kdm_json_object))
+
+ def set_fast_pair_scan_enabled(self, enable: bool) -> None:
+ """Writes into Settings whether Fast Pair scan is enabled.
+
+ Args:
+ enable: whether the Fast Pair scan should be enabled.
+ """
+ self._ad.log.info('%s Fast Pair scan in Android settings.',
+ 'Enables' if enable else 'Disables')
+ self._ad.fp.setFastPairScanEnabled(enable)
+
+ def wait_and_assert_halfsheet_showed(self, timeout_seconds: int,
+ expected_model_id: str) -> None:
+ """Waits and asserts the onHalfSheetShowed event from the seeker.
+
+ Args:
+ timeout_seconds: The number of seconds to wait before giving up.
+ expected_model_id: A 3-byte hex string for seeker side to recognize
+ the remote provider device (ex: 0x00000c).
+ """
+ self._ad.log.info('Waits and asserts the half sheet showed for model id "%s".',
+ expected_model_id)
+ self._ad.fp.waitAndAssertHalfSheetShowed(expected_model_id, timeout_seconds)
+
+ def dismiss_halfsheet(self) -> None:
+ """Dismisses the half sheet UI if showed."""
+ self._ad.fp.dismissHalfSheet()
+
+ def start_pairing(self) -> None:
+ """Starts pairing the provider via "Connect" button on half sheet UI."""
+ self._pairing_result_callback = self._ad.fp.startPairing()
+
+ def wait_and_assert_account_device(
+ self, timeout_seconds: int,
+ get_account_key_from_provider: ProviderAccountKeyCallable) -> None:
+ """Waits and asserts the onHalfSheetShowed event from the seeker.
+
+ Args:
+ timeout_seconds: The number of seconds to wait before giving up.
+ get_account_key_from_provider: The callable to get expected account key from the provider
+ side.
+ """
+
+ def _on_manage_account_device_event_received(manage_account_device_event: SnippetEvent,
+ elapsed_time: int) -> bool:
+ account_key_json_str = manage_account_device_event.data['accountDeviceJsonString']
+ account_key_from_seeker = json.loads(account_key_json_str)['account_key']
+ account_key_from_provider = get_account_key_from_provider()
+ self._ad.log.info('Seeker add an account device with account key "%s" in %d seconds.',
+ account_key_from_seeker, elapsed_time)
+ self._ad.log.info('The latest provider side account key is "%s".',
+ account_key_from_provider)
+ return account_key_from_seeker == account_key_from_provider
+
+ def _on_manage_account_device_event_waiting(elapsed_time: int) -> None:
+ self._ad.log.info(
+ 'Still waiting "%s" event callback from seeker side '
+ 'after %d seconds...', ON_MANAGE_ACCOUNT_DEVICE_EVENT, elapsed_time)
+
+ def _on_manage_account_device_event_missed() -> None:
+ asserts.fail(f'Timed out after {timeout_seconds} seconds waiting for '
+ f'the specific "{ON_MANAGE_ACCOUNT_DEVICE_EVENT}" event.')
+
+ wait_for_event(
+ callback_event_handler=self._pairing_result_callback,
+ event_name=ON_MANAGE_ACCOUNT_DEVICE_EVENT,
+ timeout_seconds=timeout_seconds,
+ on_received=_on_manage_account_device_event_received,
+ on_waiting=_on_manage_account_device_event_waiting,
+ on_missed=_on_manage_account_device_event_missed)
diff --git a/nearby/tests/multidevices/host/test_helper/utils.py b/nearby/tests/multidevices/host/test_helper/utils.py
new file mode 100644
index 0000000..a0acb57
--- /dev/null
+++ b/nearby/tests/multidevices/host/test_helper/utils.py
@@ -0,0 +1,42 @@
+# 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.
+
+import json
+import pathlib
+import sys
+from typing import Any, Dict
+
+# Type definition
+JsonObject = Dict[str, Any]
+
+
+def load_json_fast_pair_test_data(json_file_name: str) -> JsonObject:
+ """Loads a JSON text file from test data directory into a Json object.
+
+ Args:
+ json_file_name: The name of the JSON file.
+ """
+ return json.loads(
+ pathlib.Path(sys.argv[0]).parent.joinpath(
+ 'test_data', 'fastpair', json_file_name).read_text()
+ )
+
+
+def serialize_as_simplified_json_str(json_data: JsonObject) -> str:
+ """Serializes a JSON object into a string without empty space.
+
+ Args:
+ json_data: The JSON object to be serialized.
+ """
+ return json.dumps(json_data, separators=(',', ':'))
diff --git a/nearby/tests/multidevices/host/tool/fast_pair_data_provider_shell.sh b/nearby/tests/multidevices/host/tool/fast_pair_data_provider_shell.sh
new file mode 100755
index 0000000..a74c1a9
--- /dev/null
+++ b/nearby/tests/multidevices/host/tool/fast_pair_data_provider_shell.sh
@@ -0,0 +1,107 @@
+#!/bin/bash
+
+#
+# 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.
+
+# A script to interactively manage FastPairTestDataCache of FastPairTestDataProviderService.
+#
+# FastPairTestDataProviderService (../../clients/test_service/fastpair_seeker_data_provider/) is a
+# run-Time configurable FastPairDataProviderService. It has a FastPairTestDataManager to receive
+# Intent broadcast to add/clear the FastPairTestDataCache. This cache provides the data to return to
+# the Nearby Mainline module for onXXX calls (ex: onLoadFastPairAntispoofKeyDeviceMetadata).
+#
+# To use this tool, make sure you:
+# 1. Flash the ROM your built to the device
+# 2. Build and install NearbyFastPairSeekerDataProvider to the device
+# m NearbyFastPairSeekerDataProvider
+# adb install -r -g ${ANDROID_PRODUCT_OUT}/system/app/NearbyFastPairSeekerDataProvider/NearbyFastPairSeekerDataProvider.apk
+# 3. Check FastPairService can connect to the FastPairTestDataProviderService.
+# adb logcat ServiceMonitor:* *:S
+# (ex: ServiceMonitor: [FAST_PAIR_DATA_PROVIDER] connected to {
+# android.nearby.fastpair.seeker.dataprovider/android.nearby.fastpair.seeker.dataprovider.FastPairTestDataProviderService})
+#
+# Sample Usages:
+# 1. Send FastPairAntispoofKeyDeviceMetadata for PixelBuds-A to FastPairTestDataCache
+# ./fast_pair_data_provider_shell.sh -m=718c17 -a=../test_data/fastpair/pixelbuds-a_antispoofkey_devicemeta_json.txt
+# 2. Send FastPairAccountDevicesMetadata for PixelBuds-A to FastPairTestDataCache
+# ./fast_pair_data_provider_shell.sh -d=../test_data/fastpair/pixelbuds-a_account_devicemeta_json.txt
+# 3. Send FastPairAntispoofKeyDeviceMetadata for Provider Simulator to FastPairTestDataCache
+# ./fast_pair_data_provider_shell.sh -m=00000c -a=../test_data/fastpair/simulator_antispoofkey_devicemeta_json.txt
+# 4. Send FastPairAccountDevicesMetadata for Provider Simulator to FastPairTestDataCache
+# ./fast_pair_data_provider_shell.sh -d=../test_data/fastpair/simulator_account_devicemeta_json.txt
+# 5. Clear FastPairTestDataCache
+# ./fast_pair_data_provider_shell.sh -c
+#
+# Check logcat:
+# adb logcat FastPairTestDataManager:* FastPairTestDataProviderService:* *:S
+
+for i in "$@"; do
+ case $i in
+ -a=*|--ask=*)
+ ASK_FILE="${i#*=}"
+ shift # past argument=value
+ ;;
+ -m=*|--model=*)
+ MODEL_ID="${i#*=}"
+ shift # past argument=value
+ ;;
+ -d=*|--adm=*)
+ ADM_FILE="${i#*=}"
+ shift # past argument=value
+ ;;
+ -c)
+ CLEAR="true"
+ shift # past argument
+ ;;
+ -*|--*)
+ echo "Unknown option $i"
+ exit 1
+ ;;
+ *)
+ ;;
+ esac
+done
+
+readonly ACTION_BASE="android.nearby.fastpair.seeker.action"
+readonly ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA="$ACTION_BASE.ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA"
+readonly ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA="$ACTION_BASE.ACCOUNT_KEY_DEVICE_METADATA"
+readonly ACTION_RESET_TEST_DATA_CACHE="$ACTION_BASE.RESET"
+readonly DATA_JSON_STRING_KEY="json"
+readonly DATA_MODEL_ID_STRING_KEY="modelId"
+
+if [[ -n "${ASK_FILE}" ]] && [[ -n "${MODEL_ID}" ]]; then
+ echo "Sending AntispoofKeyDeviceMetadata for model ${MODEL_ID} to the FastPairTestDataCache..."
+ ASK_JSON_TEXT=$(tr -d '\n' < "$ASK_FILE")
+ CMD="am broadcast -a $ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA "
+ CMD+="-e $DATA_MODEL_ID_STRING_KEY '$MODEL_ID' "
+ CMD+="-e $DATA_JSON_STRING_KEY '\"'$ASK_JSON_TEXT'\"'"
+ CMD="adb shell \"$CMD\""
+ echo "$CMD" && eval "$CMD"
+fi
+
+if [ -n "${ADM_FILE}" ]; then
+ echo "Sending AccountKeyDeviceMetadata to the FastPairTestDataCache..."
+ ADM_JSON_TEXT=$(tr -d '\n' < "$ADM_FILE")
+ CMD="am broadcast -a $ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA "
+ CMD+="-e $DATA_JSON_STRING_KEY '\"'$ADM_JSON_TEXT'\"'"
+ CMD="adb shell \"$CMD\""
+ echo "$CMD" && eval "$CMD"
+fi
+
+if [ -n "${CLEAR}" ]; then
+ echo "Cleaning FastPairTestDataCache..."
+ CMD="adb shell am broadcast -a $ACTION_RESET_TEST_DATA_CACHE"
+ echo "$CMD" && eval "$CMD"
+fi
diff --git a/nearby/tests/robotests/Android.bp b/nearby/tests/robotests/Android.bp
new file mode 100644
index 0000000..56c0107
--- /dev/null
+++ b/nearby/tests/robotests/Android.bp
@@ -0,0 +1,56 @@
+// 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.
+
+//############################################
+// Nearby Robolectric test target. #
+//############################################
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_robolectric_test {
+ name: "NearbyRoboTests",
+ srcs: ["src/**/*.java"],
+ instrumentation_for: "NearbyFakeTestApp",
+ java_resource_dirs: ["config"],
+
+ libs: [
+ "android-support-annotations",
+ "services.core",
+ ],
+
+ static_libs: [
+ "androidx.test.core",
+ "androidx.core_core",
+ "androidx.annotation_annotation",
+ "androidx.legacy_legacy-support-v4",
+ "androidx.recyclerview_recyclerview",
+ "androidx.preference_preference",
+ "androidx.appcompat_appcompat",
+ "androidx.lifecycle_lifecycle-runtime",
+ "androidx.mediarouter_mediarouter-nodeps",
+ "error_prone_annotations",
+ "mockito-robolectric-prebuilt",
+ "service-nearby-pre-jarjar",
+ "truth-prebuilt",
+ "robolectric_android-all-stub",
+ "Robolectric_all-target",
+ ],
+
+ test_options: {
+ // timeout in seconds.
+ timeout: 36000,
+ },
+}
diff --git a/nearby/tests/robotests/AndroidManifest.xml b/nearby/tests/robotests/AndroidManifest.xml
new file mode 100644
index 0000000..25376cf
--- /dev/null
+++ b/nearby/tests/robotests/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2018 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.android.server.nearby.common.bluetooth.fastpair.test">
+</manifest>
diff --git a/nearby/tests/robotests/config/robolectric.properties b/nearby/tests/robotests/config/robolectric.properties
new file mode 100644
index 0000000..932de7d
--- /dev/null
+++ b/nearby/tests/robotests/config/robolectric.properties
@@ -0,0 +1,16 @@
+#
+# Copyright (C) 2021 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/robotests/fake_app/Android.bp b/nearby/tests/robotests/fake_app/Android.bp
new file mode 100644
index 0000000..707b38f
--- /dev/null
+++ b/nearby/tests/robotests/fake_app/Android.bp
@@ -0,0 +1,26 @@
+// 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: "NearbyFakeTestApp",
+ srcs: ["*.java"],
+ platform_apis: true,
+ optimize: {
+ enabled: false,
+ },
+}
diff --git a/nearby/tests/robotests/fake_app/AndroidManifest.xml b/nearby/tests/robotests/fake_app/AndroidManifest.xml
new file mode 100644
index 0000000..fdb5390
--- /dev/null
+++ b/nearby/tests/robotests/fake_app/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.android.server.nearby" />
diff --git a/nearby/tests/robotests/fake_app/Empty.java b/nearby/tests/robotests/fake_app/Empty.java
new file mode 100644
index 0000000..96619d5
--- /dev/null
+++ b/nearby/tests/robotests/fake_app/Empty.java
@@ -0,0 +1,16 @@
+/*
+ * 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.
+ */
+
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Bluelet.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Bluelet.java
new file mode 100644
index 0000000..182fde7
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Bluelet.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower;
+
+import android.os.ParcelUuid;
+
+/**
+ * User interface for mocking and simulation of a Bluetooth device.
+ */
+public interface Bluelet {
+
+ /**
+ * See {@link #setCreateBondOutcome}.
+ */
+ enum CreateBondOutcome {
+ SUCCESS,
+ FAILURE,
+ TIMEOUT
+ }
+
+ /**
+ * See {@link #setIoCapabilities}. Note that Bluetooth specifies a few more choices, but this is
+ * all DeviceShadower currently supports.
+ */
+ enum IoCapabilities {
+ NO_INPUT_NO_OUTPUT,
+ DISPLAY_YES_NO,
+ KEYBOARD_ONLY
+ }
+
+ /**
+ * See {@link #setFetchUuidsTiming}.
+ */
+ enum FetchUuidsTiming {
+ BEFORE_BONDING,
+ AFTER_BONDING,
+ NEVER
+ }
+
+ /**
+ * Set the initial state of the local Bluetooth adapter at the beginning of the test.
+ * <p>This method is not associated with broadcast event and is intended to be called at the
+ * beginning of the test. Allowed states:
+ *
+ * @see android.bluetooth.BluetoothAdapter#STATE_OFF
+ * @see android.bluetooth.BluetoothAdapter#STATE_ON
+ * </p>
+ */
+ Bluelet setAdapterInitialState(int state) throws IllegalArgumentException;
+
+ /**
+ * Set the bluetooth class of the local Bluetooth device at the beginning of the test.
+ * <p>
+ *
+ * @see android.bluetooth.BluetoothClass.Device
+ * @see android.bluetooth.BluetoothClass.Service
+ */
+ Bluelet setBluetoothClass(int bluetoothClass);
+
+ /**
+ * Set the scan mode of the local Bluetooth device at the beginning of the test.
+ */
+ Bluelet setScanMode(int scanMode);
+
+ /**
+ * Set the Bluetooth profiles supported by this device (e.g. A2DP Sink).
+ */
+ Bluelet setProfileUuids(ParcelUuid... profileUuids);
+
+ /**
+ * Makes bond attempts with this device succeed or fail.
+ *
+ * @param failureReason Ignored unless outcome is {@link CreateBondOutcome#FAILURE}. This is
+ * delivered in the intent that indicates bond state has changed to BOND_NONE. Values:
+ * https://cs.corp.google.com/android/frameworks/base/core/java/android/bluetooth/BluetoothDevice.java?rcl=38d9ee4cd661c10e012f71051d23644c65607eed&l=472
+ */
+ Bluelet setCreateBondOutcome(CreateBondOutcome outcome, int failureReason);
+
+ /**
+ * Sets the IO capabilities of this device. When bonding, a device states its IO capabilities in
+ * the pairing request. The pairing variant used depends on the IO capabilities of both devices
+ * (e.g. Just Works is the only available option for a NoInputNoOutput device, while Numeric
+ * Comparison aka Passkey Confirmation is used if both devices have a display and the ability to
+ * confirm/deny).
+ *
+ * @see <a href="https://blog.bluetooth.com/bluetooth-pairing-part-4">Bluetooth blog</a>
+ */
+ Bluelet setIoCapabilities(IoCapabilities ioCapabilities);
+
+ /**
+ * Make the device refuse connections. By default, connections are accepted.
+ *
+ * @param refuse Connections are refused if True.
+ */
+ Bluelet setRefuseConnections(boolean refuse);
+
+ /**
+ * Make the device refuse GATT connections. By default. connections are accepted.
+ *
+ * @param refuse GATT connections are refused if true.
+ */
+ Bluelet setRefuseGattConnections(boolean refuse);
+
+ /**
+ * When to send the ACTION_UUID broadcast. This can be {@link FetchUuidsTiming#BEFORE_BONDING},
+ * {@link FetchUuidsTiming#AFTER_BONDING}, or {@link FetchUuidsTiming#NEVER}. The default is
+ * {@link FetchUuidsTiming#AFTER_BONDING}.
+ */
+ Bluelet setFetchUuidsTiming(FetchUuidsTiming fetchUuidsTiming);
+
+ /**
+ * Adds a bonded device to the BluetoothAdapter.
+ */
+ Bluelet addBondedDevice(String address);
+
+ /**
+ * Enables the CVE-2019-2225 represents that the pairing variant will switch from Just Works to
+ * Consent when local device's io capability is Display Yes/No and remote is NoInputNoOutput.
+ *
+ * @see <a href="https://source.android.com/security/bulletin/2019-12-01#system">the security
+ * bulletin at 2019-12-01</a>
+ */
+ Bluelet enableCVE20192225(boolean value);
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/DeviceShadowEnvironment.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/DeviceShadowEnvironment.java
new file mode 100644
index 0000000..513d649
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/DeviceShadowEnvironment.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower;
+
+import com.android.libraries.testing.deviceshadower.Enums.Distance;
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
+
+/**
+ * Environment to setup and config Bluetooth unit test.
+ */
+public class DeviceShadowEnvironment {
+
+ private static final String TAG = "DeviceShadowEnvironment";
+ private static final long RESET_TIMEOUT_MILLIS = 3000;
+
+ private static boolean sIsInitialized = false;
+
+ private DeviceShadowEnvironment() {
+ }
+
+ public static void init() {
+ sIsInitialized = true;
+ DeviceShadowEnvironmentImpl.reset();
+ }
+
+ public static void reset() {
+ sIsInitialized = false;
+
+ // Order matters because each steps check and manipulate internal objects in order.
+ // Wait Scheduler and executors complete, and shut down executors.
+ DeviceShadowEnvironmentImpl.await(RESET_TIMEOUT_MILLIS);
+
+ // Throw RuntimeException if there is any internal exceptions.
+ DeviceShadowEnvironmentImpl.checkInternalExceptions();
+
+ // Clear internal exceptions, and devicelets.
+ DeviceShadowEnvironmentImpl.reset();
+ }
+
+ public static boolean await(long timeoutMillis) {
+ return DeviceShadowEnvironmentImpl.await(timeoutMillis);
+ }
+
+ public static Devicelet addDevice(final String address) {
+ return DeviceShadowEnvironmentImpl.addDevice(address);
+ }
+
+ public static void removeDevice(String address) {
+ DeviceShadowEnvironmentImpl.removeDevice(address);
+ }
+
+ public static void setLocalDevice(final String address) {
+ DeviceShadowEnvironmentImpl.setLocalDevice(address);
+ }
+
+ public static void putNear(String address1, String address2) {
+ DeviceShadowEnvironmentImpl.setDistance(address1, address2, Distance.NEAR);
+ }
+
+ public static void setDistance(String address1, String address2, Distance distance) {
+ DeviceShadowEnvironmentImpl.setDistance(address1, address2, distance);
+ }
+
+ public static Future<Void> run(final String address, final Runnable snippet) {
+ return run(
+ address,
+ () -> {
+ snippet.run();
+ return null;
+ });
+ }
+
+ public static <T> Future<T> run(final String address, final Callable<T> snippet) {
+ return DeviceShadowEnvironmentImpl.run(address, snippet);
+ }
+
+ /* package */
+ static boolean isInitialized() {
+ return sIsInitialized;
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/DeviceShadowEnvironmentInternal.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/DeviceShadowEnvironmentInternal.java
new file mode 100644
index 0000000..a5f8e6d
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/DeviceShadowEnvironmentInternal.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower;
+
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+import com.android.libraries.testing.deviceshadower.internal.sms.SmsContentProvider;
+
+/**
+ * Internal interface for device shadower.
+ */
+public class DeviceShadowEnvironmentInternal {
+
+ /**
+ * Set an interruptible point to tested code.
+ * <p>
+ * This should only make changes when DeviceShadowEnvironment initialized, which means only in
+ * test cases.
+ */
+ public static void setInterruptibleBluetooth(int identifier) {
+ if (DeviceShadowEnvironment.isInitialized()) {
+ assert identifier > 0;
+ DeviceShadowEnvironmentImpl.setInterruptibleBluetooth(identifier);
+ }
+ }
+
+ /**
+ * Mark all bluetooth operation broken after identifier in tested code.
+ */
+ public static void interruptBluetooth(String address, int identifier) {
+ DeviceShadowEnvironmentImpl.interruptBluetooth(address, identifier);
+ }
+
+ /**
+ * Return SMS content provider to be registered by robolectric context.
+ */
+ public static Class<SmsContentProvider> getSmsContentProviderClass() {
+ return SmsContentProvider.class;
+ }
+
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Devicelet.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Devicelet.java
new file mode 100644
index 0000000..bf31ead
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Devicelet.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower;
+
+/**
+ * Devicelet is the handler to operate shadowed device objects in DeviceShadower.
+ */
+public interface Devicelet {
+
+ Bluelet bluetooth();
+
+ Nfclet nfc();
+
+ Smslet sms();
+
+ String getAddress();
+
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Enums.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Enums.java
new file mode 100644
index 0000000..9eb3514
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Enums.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower;
+
+/**
+ * Contains Enums used by DeviceShadower in interface and internally.
+ */
+public interface Enums {
+
+ /**
+ * Represents vague distance between two devicelets.
+ */
+ enum Distance {
+ NEAR,
+ MID,
+ FAR,
+ AWAY,
+ }
+
+ /**
+ * Abstract base interface for operations.
+ */
+ interface Operation {
+
+ }
+
+ /**
+ * NFC operations.
+ */
+ enum NfcOperation implements Operation {
+ GET_ADAPTER,
+ ENABLE,
+ DISABLE,
+ }
+
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Nfclet.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Nfclet.java
new file mode 100644
index 0000000..4b00f24
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Nfclet.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower;
+
+/**
+ * Interface of Nfclet
+ */
+public interface Nfclet {
+
+ Nfclet setInitialState(int state);
+
+ Nfclet setInterruptOperation(Enums.NfcOperation operation);
+
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Smslet.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Smslet.java
new file mode 100644
index 0000000..483fab6
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/Smslet.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower;
+
+import android.net.Uri;
+
+/**
+ * Interface of Smslet
+ */
+public interface Smslet {
+
+ Smslet addSms(Uri uri, String body);
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetooth.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetooth.java
new file mode 100644
index 0000000..be8390e
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetooth.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.content.AttributionSource;
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelUuid;
+
+/**
+ * Fake interface replacement for hidden IBluetooth class
+ */
+public interface IBluetooth {
+
+ // Bluetooth settings.
+ String getAddress();
+
+ String getName();
+
+ boolean setName(String name);
+
+ // Remote device properties.
+ int getRemoteClass(BluetoothDevice device);
+
+ String getRemoteName(BluetoothDevice device);
+
+ int getRemoteType(BluetoothDevice device, AttributionSource attributionSource);
+
+ ParcelUuid[] getRemoteUuids(BluetoothDevice device);
+
+ boolean fetchRemoteUuids(BluetoothDevice device);
+
+ // Bluetooth discovery.
+ int getScanMode();
+
+ boolean setScanMode(int mode, int duration);
+
+ int getDiscoverableTimeout();
+
+ boolean setDiscoverableTimeout(int timeout);
+
+ boolean startDiscovery();
+
+ boolean cancelDiscovery();
+
+ boolean isDiscovering();
+
+ // Adapter state.
+ boolean isEnabled();
+
+ int getState();
+
+ boolean enable();
+
+ boolean disable();
+
+ // Rfcomm sockets.
+ ParcelFileDescriptor connectSocket(BluetoothDevice device, int type, ParcelUuid uuid,
+ int port, int flag);
+
+ ParcelFileDescriptor createSocketChannel(int type, String serviceName, ParcelUuid uuid,
+ int port, int flag);
+
+ // BLE settings.
+ /* SINCE SDK 21 */ boolean isMultiAdvertisementSupported();
+
+ /* SINCE SDK 22 */ boolean isPeripheralModeSupported();
+
+ /* SINCE SDK 21 */ boolean isOffloadedFilteringSupported();
+
+ // Bonding (pairing).
+ int getBondState(BluetoothDevice device, AttributionSource attributionSource);
+
+ boolean createBond(BluetoothDevice device, int transport, OobData remoteP192Data,
+ OobData remoteP256Data, AttributionSource attributionSource);
+
+ boolean setPairingConfirmation(BluetoothDevice device, boolean accept,
+ AttributionSource attributionSource);
+
+ boolean setPasskey(BluetoothDevice device, int passkey);
+
+ boolean cancelBondProcess(BluetoothDevice device);
+
+ boolean removeBond(BluetoothDevice device);
+
+ BluetoothDevice[] getBondedDevices();
+
+ // Connecting to profiles.
+ int getAdapterConnectionState();
+
+ int getProfileConnectionState(int profile);
+
+ // Access permissions
+ int getPhonebookAccessPermission(BluetoothDevice device);
+
+ boolean setPhonebookAccessPermission(BluetoothDevice device, int value);
+
+ int getMessageAccessPermission(BluetoothDevice device);
+
+ boolean setMessageAccessPermission(BluetoothDevice device, int value);
+
+ int getSimAccessPermission(BluetoothDevice device);
+
+ boolean setSimAccessPermission(BluetoothDevice device, int value);
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGatt.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGatt.java
new file mode 100644
index 0000000..16e4f01
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGatt.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanSettings;
+import android.os.ParcelUuid;
+
+import java.util.List;
+
+/**
+ * Fake interface replacement for IBluetoothGatt
+ * TODO(b/200231384): include >=N interface.
+ */
+public interface IBluetoothGatt {
+
+ /* ONLY SDK 23 */
+ void startScan(int appIf, boolean isServer, ScanSettings settings,
+ List<ScanFilter> filters, List<?> scanStorages, String callPackage);
+
+ /* ONLY SDK 21 */
+ void startScan(int appIf, boolean isServer, ScanSettings settings,
+ List<ScanFilter> filters, List<?> scanStorages);
+
+ /* SINCE SDK 21 */
+ void stopScan(int appIf, boolean isServer);
+
+ /* SINCE SDK 21 */
+ void startMultiAdvertising(
+ int appIf, AdvertiseData advertiseData, AdvertiseData scanResponse,
+ AdvertiseSettings settings);
+
+ /* SINCE SDK 21 */
+ void stopMultiAdvertising(int appIf);
+
+ /* SINCE SDK 21 */
+ void registerClient(ParcelUuid appId, IBluetoothGattCallback callback);
+
+ /* SINCE SDK 21 */
+ void unregisterClient(int clientIf);
+
+ /* SINCE SDK 21 */
+ void clientConnect(int clientIf, String address, boolean isDirect, int transport);
+
+ /* SINCE SDK 21 */
+ void clientDisconnect(int clientIf, String address);
+
+ /* SINCE SDK 21 */
+ void discoverServices(int clientIf, String address);
+
+ /* SINCE SDK 21 */
+ void readCharacteristic(int clientIf, String address, int srvcType,
+ int srvcInstanceId, ParcelUuid srvcId, int charInstanceId, ParcelUuid charId,
+ int authReq);
+
+ /* SINCE SDK 21 */
+ void writeCharacteristic(int clientIf, String address, int srvcType,
+ int srvcInstanceId, ParcelUuid srvcId, int charInstanceId, ParcelUuid charId,
+ int writeType, int authReq, byte[] value);
+
+ /* SINCE SDK 21 */
+ void readDescriptor(int clientIf, String address, int srvcType,
+ int srvcInstanceId, ParcelUuid srvcId, int charInstanceId, ParcelUuid charId,
+ int descrInstanceId, ParcelUuid descrUuid, int authReq);
+
+ /* SINCE SDK 21 */
+ void writeDescriptor(int clientIf, String address, int srvcType,
+ int srvcInstanceId, ParcelUuid srvcId, int charInstanceId, ParcelUuid charId,
+ int descrInstanceId, ParcelUuid descrId, int writeType, int authReq, byte[] value);
+
+ /* SINCE SDK 21 */
+ void registerForNotification(int clientIf, String address, int srvcType,
+ int srvcInstanceId, ParcelUuid srvcId, int charInstanceId, ParcelUuid charId,
+ boolean enable);
+
+ /* SINCE SDK 21 */
+ void registerServer(ParcelUuid appId, IBluetoothGattServerCallback callback);
+
+ /* SINCE SDK 21 */
+ void unregisterServer(int serverIf);
+
+ /* SINCE SDK 21 */
+ void serverConnect(int servertIf, String address, boolean isDirect, int transport);
+
+ /* SINCE SDK 21 */
+ void serverDisconnect(int serverIf, String address);
+
+ /* SINCE SDK 21 */
+ void beginServiceDeclaration(int serverIf, int srvcType, int srvcInstanceId, int minHandles,
+ ParcelUuid srvcId, boolean advertisePreferred);
+
+ /* SINCE SDK 21 */
+ void addIncludedService(int serverIf, int srvcType, int srvcInstanceId, ParcelUuid srvcId);
+
+ /* SINCE SDK 21 */
+ void addCharacteristic(int serverIf, ParcelUuid charId, int properties, int permissions);
+
+ /* SINCE SDK 21 */
+ void addDescriptor(int serverIf, ParcelUuid descId, int permissions);
+
+ /* SINCE SDK 21 */
+ void endServiceDeclaration(int serverIf);
+
+ /* SINCE SDK 21 */
+ void removeService(int serverIf, int srvcType, int srvcInstanceId, ParcelUuid srvcId);
+
+ /* SINCE SDK 21 */
+ void clearServices(int serverIf);
+
+ /* SINCE SDK 21 */
+ void sendResponse(int serverIf, String address, int requestId,
+ int status, int offset, byte[] value);
+
+ /* SINCE SDK 21 */
+ void sendNotification(int serverIf, String address, int srvcType,
+ int srvcInstanceId, ParcelUuid srvcId, int charInstanceId, ParcelUuid charId,
+ boolean confirm, byte[] value);
+
+ /* SINCE SDK 21 */
+ void configureMTU(int clientIf, String address, int mtu);
+
+ /* SINCE SDK 21 */
+ void connectionParameterUpdate(int clientIf, String address, int connectionPriority);
+
+ void disconnectAll();
+
+ List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states);
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGattCallback.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGattCallback.java
new file mode 100644
index 0000000..b29369b
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGattCallback.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.ScanResult;
+import android.os.ParcelUuid;
+
+/**
+ * Fake interface replacement for IBluetoothGattCallback
+ * TODO(b/200231384): include >=N interface.
+ */
+public interface IBluetoothGattCallback {
+
+ /* SINCE SDK 21 */
+ void onClientRegistered(int status, int clientIf);
+
+ /* SINCE SDK 21 */
+ void onClientConnectionState(int status, int clientIf, boolean connected, String address);
+
+ /* ONLY SDK 19 */
+ void onScanResult(String address, int rssi, byte[] advData);
+
+ /* SINCE SDK 21 */
+ void onScanResult(ScanResult scanResult);
+
+ /* SINCE SDK 21 */
+ void onGetService(String address, int srvcType, int srvcInstId, ParcelUuid srvcUuid);
+
+ /* SINCE SDK 21 */
+ void onGetIncludedService(String address, int srvcType, int srvcInstId,
+ ParcelUuid srvcUuid, int inclSrvcType,
+ int inclSrvcInstId, ParcelUuid inclSrvcUuid);
+
+ /* SINCE SDK 21 */
+ void onGetCharacteristic(String address, int srvcType,
+ int srvcInstId, ParcelUuid srvcUuid,
+ int charInstId, ParcelUuid charUuid,
+ int charProps);
+
+ /* SINCE SDK 21 */
+ void onGetDescriptor(String address, int srvcType,
+ int srvcInstId, ParcelUuid srvcUuid,
+ int charInstId, ParcelUuid charUuid,
+ int descrInstId, ParcelUuid descrUuid);
+
+ /* SINCE SDK 21 */
+ void onSearchComplete(String address, int status);
+
+ /* SINCE SDK 21 */
+ void onCharacteristicRead(String address, int status, int srvcType,
+ int srvcInstId, ParcelUuid srvcUuid,
+ int charInstId, ParcelUuid charUuid,
+ byte[] value);
+
+ /* SINCE SDK 21 */
+ void onCharacteristicWrite(String address, int status, int srvcType,
+ int srvcInstId, ParcelUuid srvcUuid,
+ int charInstId, ParcelUuid charUuid);
+
+ /* SINCE SDK 21 */
+ void onExecuteWrite(String address, int status);
+
+ /* SINCE SDK 21 */
+ void onDescriptorRead(String address, int status, int srvcType,
+ int srvcInstId, ParcelUuid srvcUuid,
+ int charInstId, ParcelUuid charUuid,
+ int descrInstId, ParcelUuid descrUuid,
+ byte[] value);
+
+ /* SINCE SDK 21 */
+ void onDescriptorWrite(String address, int status, int srvcType,
+ int srvcInstId, ParcelUuid srvcUuid,
+ int charInstId, ParcelUuid charUuid,
+ int descrInstId, ParcelUuid descrUuid);
+
+ /* SINCE SDK 21 */
+ void onNotify(String address, int srvcType,
+ int srvcInstId, ParcelUuid srvcUuid,
+ int charInstId, ParcelUuid charUuid,
+ byte[] value);
+
+ /* SINCE SDK 21 */
+ void onReadRemoteRssi(String address, int rssi, int status);
+
+ /* SDK 21 */
+ void onMultiAdvertiseCallback(int status, boolean isStart,
+ AdvertiseSettings advertiseSettings);
+
+ /* SDK 21 */
+ void onConfigureMTU(String address, int mtu, int status);
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGattServerCallback.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGattServerCallback.java
new file mode 100644
index 0000000..10b91bb
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothGattServerCallback.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.os.ParcelUuid;
+
+/**
+ * Fake interface of internal IBluetoothGattServerCallback.
+ */
+public interface IBluetoothGattServerCallback {
+
+ /* SINCE SDK 21 */
+ void onServerRegistered(int status, int serverIf);
+
+ /* SINCE SDK 21 */
+ void onScanResult(String address, int rssi, byte[] advData);
+
+ /* SINCE SDK 21 */
+ void onServerConnectionState(int status, int serverIf, boolean connected, String address);
+
+ /* SINCE SDK 21 */
+ void onServiceAdded(int status, int srvcType, int srvcInstId, ParcelUuid srvcId);
+
+ /* SINCE SDK 21 */
+ void onCharacteristicReadRequest(String address, int transId, int offset, boolean isLong,
+ int srvcType, int srvcInstId, ParcelUuid srvcId, int charInstId, ParcelUuid charId);
+
+ /* SINCE SDK 21 */
+ void onDescriptorReadRequest(String address, int transId, int offset, boolean isLong,
+ int srvcType, int srvcInstId, ParcelUuid srvcId,
+ int charInstId, ParcelUuid charId, ParcelUuid descrId);
+
+ /* SINCE SDK 21 */
+ void onCharacteristicWriteRequest(String address, int transId, int offset, int length,
+ boolean isPrep, boolean needRsp, int srvcType, int srvcInstId, ParcelUuid srvcId,
+ int charInstId, ParcelUuid charId, byte[] value);
+
+ /* SINCE SDK 21 */
+ void onDescriptorWriteRequest(String address, int transId, int offset, int length,
+ boolean isPrep, boolean needRsp, int srvcType, int srvcInstId, ParcelUuid srvcId,
+ int charInstId, ParcelUuid charId, ParcelUuid descrId, byte[] value);
+
+ /* SINCE SDK 21 */
+ void onExecuteWrite(String address, int transId, boolean execWrite);
+
+ /* SINCE SDK 21 */
+ void onNotificationSent(String address, int status);
+
+ /* SINCE SDK 22 */
+ void onMtuChanged(String address, int mtu);
+
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothManager.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothManager.java
new file mode 100644
index 0000000..6bb2209
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothManager.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Intentionally in package android.bluetooth to fake existing interface in Android.
+ */
+package android.bluetooth;
+
+/**
+ * Fake interface for IBluetoothManager.
+ */
+public interface IBluetoothManager {
+
+ boolean enable();
+
+ boolean disable(boolean persist);
+
+ String getAddress();
+
+ String getName();
+
+ IBluetooth registerAdapter(IBluetoothManagerCallback callback);
+
+ IBluetoothGatt getBluetoothGatt();
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothManagerCallback.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothManagerCallback.java
new file mode 100644
index 0000000..f39b82f
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/bluetooth/IBluetoothManagerCallback.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+/**
+ * Fake interface replacement for hidden IBluetoothManagerCallback class
+ */
+public interface IBluetoothManagerCallback {
+
+}
+
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/BeamShareData.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/BeamShareData.java
new file mode 100644
index 0000000..5357a9b
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/BeamShareData.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nfc;
+
+import android.net.Uri;
+import android.os.UserHandle;
+
+/**
+ * Fake BeamShareData.
+ */
+public class BeamShareData {
+
+ public NdefMessage ndefMessage;
+ public Uri[] uris;
+ public UserHandle userHandle;
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/IAppCallback.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/IAppCallback.java
new file mode 100644
index 0000000..7b62f19
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/IAppCallback.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nfc;
+
+/**
+ * Fake interface for nfc service.
+ */
+public interface IAppCallback {
+
+ /* M */ void onNdefPushComplete(byte peerLlcpVersion);
+
+ /* M */ BeamShareData createBeamShareData(byte peerLlcpVersion);
+
+ /* L */ void onNdefPushComplete();
+
+ /* L */ BeamShareData createBeamShareData();
+
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/INfcAdapter.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/INfcAdapter.java
new file mode 100644
index 0000000..08acdbc
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/fakes/android/nfc/INfcAdapter.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nfc;
+
+/**
+ * Fake interface of INfcAdapter
+ */
+public interface INfcAdapter {
+
+ void setAppCallback(IAppCallback callback);
+
+ boolean enable();
+
+ boolean disable(boolean saveState);
+
+ int getState();
+
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BleAdvertiser.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BleAdvertiser.java
new file mode 100644
index 0000000..f3328c8
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BleAdvertiser.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.helpers.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.BluetoothLeAdvertiser;
+import android.util.Log;
+
+import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
+
+import com.google.common.base.Preconditions;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+
+/**
+ * Helper class to operate a device as BLE advertiser.
+ */
+public class BleAdvertiser {
+
+ private static final String TAG = "BleAdvertiser";
+
+ private static final int DEFAULT_MODE = AdvertiseSettings.ADVERTISE_MODE_BALANCED;
+ private static final int DEFAULT_TX_POWER_LEVEL = AdvertiseSettings.ADVERTISE_TX_POWER_HIGH;
+ private static final boolean DEFAULT_CONNECTABLE = true;
+ private static final int DEFAULT_TIMEOUT = 0;
+
+
+ /**
+ * Callback of {@link BleAdvertiser}.
+ */
+ public interface Callback {
+
+ void onStartFailure(String address, int errorCode);
+
+ void onStartSuccess(String address, AdvertiseSettings settingsInEffect);
+ }
+
+ /**
+ * Builder class of {@link BleAdvertiser}.
+ */
+ public static final class Builder {
+
+ private final String mAddress;
+ private final Callback mCallback;
+ private AdvertiseSettings mSettings = defaultSettings();
+ private AdvertiseData mData;
+ private AdvertiseData mResponse;
+
+ public Builder(String address, Callback callback) {
+ this.mAddress = Preconditions.checkNotNull(address);
+ this.mCallback = Preconditions.checkNotNull(callback);
+ }
+
+ public Builder setAdvertiseSettings(AdvertiseSettings settings) {
+ this.mSettings = settings;
+ return this;
+ }
+
+ public Builder setAdvertiseData(AdvertiseData data) {
+ this.mData = data;
+ return this;
+ }
+
+ public Builder setResponseData(AdvertiseData response) {
+ this.mResponse = response;
+ return this;
+ }
+
+ public BleAdvertiser build() {
+ return new BleAdvertiser(mAddress, mCallback, mSettings, mData, mResponse);
+ }
+ }
+
+ private static AdvertiseSettings defaultSettings() {
+ return new AdvertiseSettings.Builder()
+ .setAdvertiseMode(DEFAULT_MODE)
+ .setConnectable(DEFAULT_CONNECTABLE)
+ .setTimeout(DEFAULT_TIMEOUT)
+ .setTxPowerLevel(DEFAULT_TX_POWER_LEVEL).build();
+ }
+
+ private final String mAddress;
+ private final Callback mCallback;
+ private final AdvertiseSettings mSettings;
+ private final AdvertiseData mData;
+ private final AdvertiseData mResponse;
+ private final CountDownLatch mStartAdvertiseLatch;
+ private BluetoothLeAdvertiser mAdvertiser;
+
+ private BleAdvertiser(String address, Callback callback, AdvertiseSettings settings,
+ AdvertiseData data, AdvertiseData response) {
+ this.mAddress = address;
+ this.mCallback = callback;
+ this.mSettings = settings;
+ this.mData = data;
+ this.mResponse = response;
+ mStartAdvertiseLatch = new CountDownLatch(1);
+ DeviceShadowEnvironment.addDevice(address).bluetooth()
+ .setAdapterInitialState(BluetoothAdapter.STATE_ON);
+ }
+
+ /**
+ * Starts advertising.
+ */
+ public Future<Void> start() {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
+ mAdvertiser.startAdvertising(mSettings, mData, mResponse, mAdvertiseCallback);
+ }
+ });
+ }
+
+ /**
+ * Stops advertising.
+ */
+ public Future<Void> stop() {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ mAdvertiser.stopAdvertising(mAdvertiseCallback);
+ }
+ });
+ }
+
+ public void waitTillAdvertiseCompleted() {
+ try {
+ mStartAdvertiseLatch.await();
+ } catch (InterruptedException e) {
+ Log.w(TAG, mAddress + " fails to wait till advertise completed: ", e);
+ }
+ }
+
+ private final AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {
+ @Override
+ public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+ Log.v(TAG,
+ String.format("onStartSuccess(settingsInEffect: %s) on %s ", settingsInEffect,
+ mAddress));
+ mCallback.onStartSuccess(mAddress, settingsInEffect);
+ mStartAdvertiseLatch.countDown();
+ }
+
+ @Override
+ public void onStartFailure(int errorCode) {
+ Log.v(TAG, String.format("onStartFailure(errorCode: %d) on %s", errorCode, mAddress));
+ mCallback.onStartFailure(mAddress, errorCode);
+ mStartAdvertiseLatch.countDown();
+ }
+ };
+}
+
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BleScanner.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BleScanner.java
new file mode 100644
index 0000000..6a44c2b
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BleScanner.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.helpers.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.util.Log;
+
+import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
+
+import com.google.common.base.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Helper class to operate a device as BLE scanner.
+ */
+public class BleScanner {
+
+ private static final String TAG = "BleScanner";
+
+ private static final int DEFAULT_MODE = ScanSettings.SCAN_MODE_LOW_LATENCY;
+ private static final int DEFAULT_CALLBACK_TYPE = ScanSettings.CALLBACK_TYPE_ALL_MATCHES;
+ private static final long DEFAULT_DELAY = 0L;
+
+ /**
+ * Callback of {@link BleScanner}.
+ */
+ public interface Callback {
+
+ void onScanResult(String address, int callbackType, ScanResult result);
+
+ void onBatchScanResults(String address, List<ScanResult> results);
+
+ void onScanFailed(String address, int errorCode);
+ }
+
+ /**
+ * Builder class of {@link BleScanner}.
+ */
+ public static final class Builder {
+
+ private final String mAddress;
+ private final Callback mCallback;
+ private ScanSettings mSettings = defaultSettings();
+ private List<ScanFilter> mFilters;
+ private int mNumOfExpectedScanCallbacks = 1;
+
+ public Builder(String address, Callback callback) {
+ this.mAddress = Preconditions.checkNotNull(address);
+ this.mCallback = Preconditions.checkNotNull(callback);
+ }
+
+ public Builder setScanSettings(ScanSettings settings) {
+ this.mSettings = settings;
+ return this;
+ }
+
+ public Builder addScanFilter(ScanFilter... filterArgs) {
+ if (this.mFilters == null) {
+ this.mFilters = new ArrayList<>();
+ }
+ for (ScanFilter filter : filterArgs) {
+ this.mFilters.add(filter);
+ }
+ return this;
+ }
+
+ /**
+ * Sets number of expected scan result callback.
+ *
+ * @param num Number of expected scan result callback, default to 1.
+ */
+ public Builder setNumOfExpectedScanCallbacks(int num) {
+ mNumOfExpectedScanCallbacks = num;
+ return this;
+ }
+
+ public BleScanner build() {
+ return new BleScanner(
+ mAddress, mCallback, mSettings, mFilters, mNumOfExpectedScanCallbacks);
+ }
+ }
+
+ private static ScanSettings defaultSettings() {
+ return new ScanSettings.Builder()
+ .setScanMode(DEFAULT_MODE)
+ .setCallbackType(DEFAULT_CALLBACK_TYPE)
+ .setReportDelay(DEFAULT_DELAY).build();
+ }
+
+ private final String mAddress;
+ private final Callback mCallback;
+ private final ScanSettings mSettings;
+ private final List<ScanFilter> mFilters;
+ private final BlockingQueue<Integer> mScanResultCounts;
+ private int mNumOfExpectedScanCallbacks;
+ private int mNumOfReceivedScanCallbacks;
+ private BluetoothLeScanner mScanner;
+
+ private BleScanner(String address, Callback callback, ScanSettings settings,
+ List<ScanFilter> filters, int numOfExpectedScanResult) {
+ this.mAddress = address;
+ this.mCallback = callback;
+ this.mSettings = settings;
+ this.mFilters = filters;
+ this.mNumOfExpectedScanCallbacks = numOfExpectedScanResult;
+ this.mNumOfReceivedScanCallbacks = 0;
+ this.mScanResultCounts = new LinkedBlockingQueue<>(numOfExpectedScanResult);
+ DeviceShadowEnvironment.addDevice(address).bluetooth()
+ .setAdapterInitialState(BluetoothAdapter.STATE_ON);
+ }
+
+ public Future<Void> start() {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ mScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner();
+ mScanner.startScan(mFilters, mSettings, mScanCallback);
+ }
+ });
+ }
+
+ public void waitTillNextScanResult(long timeoutMillis) {
+ Integer result = null;
+ if (mNumOfReceivedScanCallbacks >= mNumOfExpectedScanCallbacks) {
+ return;
+ }
+ try {
+ if (timeoutMillis < 0) {
+ result = mScanResultCounts.take();
+ } else {
+ result = mScanResultCounts.poll(timeoutMillis, TimeUnit.MILLISECONDS);
+ }
+ if (result != null && result >= 0) {
+ mNumOfReceivedScanCallbacks++;
+ }
+ Log.v(TAG, "Scan results: " + result);
+ } catch (InterruptedException e) {
+ Log.w(TAG, mAddress + " fails to wait till next scan result: ", e);
+ }
+ }
+
+ public void waitTillNextScanResult() {
+ waitTillNextScanResult(-1);
+ }
+
+ public void waitTillAllScanResults() {
+ while (mNumOfReceivedScanCallbacks < mNumOfExpectedScanCallbacks) {
+ try {
+ if (mScanResultCounts.take() >= 0) {
+ mNumOfReceivedScanCallbacks++;
+ }
+ } catch (InterruptedException e) {
+ Log.w(TAG, String.format("%s fails to wait scan result", mAddress), e);
+ return;
+ }
+ }
+ }
+
+ public Future<Void> stop() {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ mScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner();
+ mScanner.stopScan(mScanCallback);
+ }
+ });
+ }
+
+ private final ScanCallback mScanCallback = new ScanCallback() {
+ @Override
+ public void onScanResult(int callbackType, ScanResult result) {
+ Log.v(TAG, String.format("onScanResult(callbackType: %d, result: %s) on %s",
+ callbackType, result, mAddress));
+ mCallback.onScanResult(mAddress, callbackType, result);
+ try {
+ mScanResultCounts.put(1);
+ } catch (InterruptedException e) {
+ // no-op.
+ }
+ }
+
+ @Override
+ public void onBatchScanResults(List<ScanResult> results) {
+ /**** Not supported yet.
+ Log.v(TAG, String.format("onBatchScanResults(results: %s) on %s",
+ Arrays.toString(results.toArray()), address));
+ callback.onBatchScanResults(address, results);
+ try {
+ scanResultCounts.put(results.size());
+ } catch (InterruptedException e) {
+ // no-op.
+ }
+ */
+ }
+
+ @Override
+ public void onScanFailed(int errorCode) {
+ /**** Not supported yet.
+ Log.v(TAG, String.format("onScanFailed(errorCode: %d) on %s", errorCode, address));
+ callback.onScanFailed(address, errorCode);
+ try {
+ scanResultCounts.put(-1);
+ } catch (InterruptedException e) {
+ // no-op.
+ }
+ */
+ }
+ };
+}
+
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothGattClient.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothGattClient.java
new file mode 100644
index 0000000..69e77af
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothGattClient.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.helpers.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.content.Context;
+import android.util.Log;
+
+import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Helper class to operate a device as gatt client.
+ */
+public class BluetoothGattClient {
+
+ private static final String TAG = "BluetoothGattClient";
+ private static final int LATCH_TIMEOUT_MILLIS = 1000;
+
+ /**
+ * Callback of BluetoothGattClient.
+ */
+ public interface Callback {
+
+ void onConnectionStateChange(String address, int status, int newState);
+
+ void onCharacteristicChanged(String address, UUID uuid, byte[] value);
+
+ void onCharacteristicRead(String address, UUID uuid, byte[] value, int status);
+
+ void onCharacteristicWrite(String address, UUID uuid, byte[] value, int status);
+
+ void onDescriptorRead(String address, UUID uuid, byte[] value, int status);
+
+ void onDescriptorWrite(String address, UUID uuid, byte[] value, int status);
+
+ void onServicesDiscovered(
+ UUID[] serviceUuid, UUID[] characteristicUuid, UUID[] descriptorUuid, int status);
+
+ void onConfigureMTU(String address, int mtu, int status);
+ }
+
+ private final String mAddress;
+ private final Callback mCallback;
+ private final Context mContext;
+ private final Map<UUID, BluetoothGattCharacteristic> mCharacteristics = new HashMap<>();
+ private final Map<UUID, BluetoothGattDescriptor> mDescriptors = new HashMap<>();
+ private BluetoothGatt mGatt;
+ private CountDownLatch mConnectionLatch;
+ private CountDownLatch mServiceDiscoverLatch;
+
+ public BluetoothGattClient(String address, Callback callback, Context context) {
+ this.mAddress = address;
+ this.mCallback = callback;
+ this.mContext = context;
+ DeviceShadowEnvironment.addDevice(address).bluetooth()
+ .setAdapterInitialState(BluetoothAdapter.STATE_ON);
+ }
+
+ public Future<Void> connect(final String remoteAddress) {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ mConnectionLatch = new CountDownLatch(1);
+ mGatt = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(remoteAddress)
+ .connectGatt(mContext, false /* auto connect */, mGattCallback);
+ try {
+ mConnectionLatch.await(LATCH_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ // no-op.
+ }
+
+ mServiceDiscoverLatch = new CountDownLatch(1);
+ mGatt.discoverServices();
+ try {
+ mServiceDiscoverLatch.await(LATCH_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ // no-op.
+ }
+ }
+ });
+ }
+
+ public Future<Void> close() {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ mGatt.disconnect();
+ mGatt.close();
+ }
+ });
+ }
+
+ public Future<Void> readCharacteristic(final UUID uuid) {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ mGatt.readCharacteristic(mCharacteristics.get(uuid));
+ }
+ });
+ }
+
+ public Future<Void> setNotification(final UUID uuid) {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ mGatt.setCharacteristicNotification(mCharacteristics.get(uuid), true);
+ }
+ });
+ }
+
+ public Future<Void> writeCharacteristic(final UUID uuid, final byte[] value) {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ BluetoothGattCharacteristic characteristic = mCharacteristics.get(uuid);
+ characteristic.setValue(value);
+ mGatt.writeCharacteristic(characteristic);
+ }
+ });
+ }
+
+ /**
+ * Reads the value of a descriptor with given UUID.
+ *
+ * <p>If different characteristics on the service have the same descriptor, use {@link
+ * BluetoothGattClient#readDescriptor(UUID, UUID)} instead.
+ */
+ public Future<Void> readDescriptor(final UUID uuid) {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ mGatt.readDescriptor(mDescriptors.get(uuid));
+ }
+ });
+ }
+
+ /**
+ * Reads the descriptor value of the specified characteristic.
+ */
+ public Future<Void> readDescriptor(final UUID descriptorUuid, final UUID characteristicUuid) {
+ return DeviceShadowEnvironment.run(
+ mAddress,
+ new Runnable() {
+ @Override
+ public void run() {
+ mGatt.readDescriptor(
+ mCharacteristics.get(characteristicUuid)
+ .getDescriptor(descriptorUuid));
+ }
+ });
+ }
+
+ /**
+ * Writes to the descriptor with given UUID.
+ *
+ * <p>If different characteristics on the service have the same descriptor, use {@link
+ * BluetoothGattClient#writeDescriptor(UUID, UUID, byte[])} instead.
+ */
+ public Future<Void> writeDescriptor(final UUID uuid, final byte[] value) {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ BluetoothGattDescriptor descriptor = mDescriptors.get(uuid);
+ descriptor.setValue(value);
+ mGatt.writeDescriptor(descriptor);
+ }
+ });
+ }
+
+ /**
+ * Writes to the descriptor of the specified characteristic.
+ */
+ public Future<Void> writeDescriptor(
+ final UUID descriptorUuid, final UUID characteristicUuid, final byte[] value) {
+ return DeviceShadowEnvironment.run(
+ mAddress,
+ new Runnable() {
+ @Override
+ public void run() {
+ BluetoothGattDescriptor descriptor =
+ mCharacteristics.get(characteristicUuid)
+ .getDescriptor(descriptorUuid);
+ descriptor.setValue(value);
+ mGatt.writeDescriptor(descriptor);
+ }
+ });
+ }
+
+ public Future<Void> requestMtu(int mtu) {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ mGatt.requestMtu(mtu);
+ }
+ });
+ }
+
+ private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
+ @Override
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+ Log.v(TAG, String.format("onConnectionStateChange(status: %s, newState: %s)",
+ status, newState));
+ if (mConnectionLatch != null) {
+ mConnectionLatch.countDown();
+ }
+ mCallback.onConnectionStateChange(gatt.getDevice().getAddress(), status, newState);
+ }
+
+ @Override
+ public void onCharacteristicChanged(
+ BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ Log.v(TAG, String.format("onCharacteristicChanged(characteristic: %s, value: %s)",
+ characteristic.getUuid(), Arrays.toString(characteristic.getValue())));
+ mCallback.onCharacteristicChanged(
+ gatt.getDevice().getAddress(), characteristic.getUuid(),
+ characteristic.getValue());
+ }
+
+ @Override
+ public void onCharacteristicRead(
+ BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ Log.v(TAG, String.format("onCharacteristicRead(descriptor: %s, status: %s)",
+ characteristic.getUuid(), status));
+ mCallback.onCharacteristicRead(
+ gatt.getDevice().getAddress(), characteristic.getUuid(),
+ characteristic.getValue(),
+ status);
+ }
+
+ @Override
+ public void onCharacteristicWrite(
+ BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ Log.v(TAG, String.format("onCharacteristicWrite(descriptor: %s, status: %s)",
+ characteristic.getUuid(), status));
+ mCallback.onCharacteristicWrite(gatt.getDevice().getAddress(),
+ characteristic.getUuid(), characteristic.getValue(), status);
+ }
+
+ @Override
+ public void onDescriptorRead(
+ BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ Log.v(TAG, String.format("onDescriptorRead(descriptor: %s, status: %s)",
+ descriptor.getUuid(), status));
+ mCallback.onDescriptorRead(
+ gatt.getDevice().getAddress(), descriptor.getUuid(), descriptor.getValue(),
+ status);
+ }
+
+ @Override
+ public void onDescriptorWrite(
+ BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+ Log.v(TAG, String.format("onDescriptorWrite(descriptor: %s, status: %s)",
+ descriptor.getUuid(), status));
+ mCallback.onDescriptorWrite(
+ gatt.getDevice().getAddress(), descriptor.getUuid(), descriptor.getValue(),
+ status);
+ }
+
+ @Override
+ public synchronized void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ Log.v(TAG, "Discovered service: " + gatt.getServices());
+ List<UUID> serviceUuid = new ArrayList<>();
+ List<UUID> characteristicUuid = new ArrayList<>();
+ List<UUID> descriptorUuid = new ArrayList<>();
+ for (BluetoothGattService service : gatt.getServices()) {
+ serviceUuid.add(service.getUuid());
+ for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
+ mCharacteristics.put(characteristic.getUuid(), characteristic);
+ characteristicUuid.add(characteristic.getUuid());
+ for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()) {
+ mDescriptors.put(descriptor.getUuid(), descriptor);
+ descriptorUuid.add(descriptor.getUuid());
+ }
+ }
+ }
+
+ Collections.sort(serviceUuid);
+ Collections.sort(characteristicUuid);
+ Collections.sort(descriptorUuid);
+
+ mCallback.onServicesDiscovered(serviceUuid.toArray(new UUID[serviceUuid.size()]),
+ characteristicUuid.toArray(new UUID[characteristicUuid.size()]),
+ descriptorUuid.toArray(new UUID[descriptorUuid.size()]),
+ status);
+ mServiceDiscoverLatch.countDown();
+ }
+
+ @Override
+ public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
+ Log.v(TAG, String.format("onMtuChanged(mtu: %s, status: %s)", mtu, status));
+ mCallback.onConfigureMTU(gatt.getDevice().getAddress(), mtu, status);
+ }
+ };
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothGattMaster.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothGattMaster.java
new file mode 100644
index 0000000..e9f364a
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothGattMaster.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.helpers.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattServer;
+import android.bluetooth.BluetoothGattServerCallback;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.util.Log;
+
+import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.Future;
+
+/**
+ * Helper class to operate a device as gatt server.
+ */
+public class BluetoothGattMaster {
+
+ private static final String TAG = "BluetoothGattMaster";
+
+ /**
+ * Callback of BluetoothGattMaster.
+ */
+ public interface Callback {
+
+ void onConnectionStateChange(String address, int status, int newState);
+
+ void onCharacteristicReadRequest(String address, UUID uuid);
+
+ void onCharacteristicWriteRequest(String address, UUID uuid, byte[] value,
+ boolean preparedWrite, boolean responseNeeded);
+
+ void onDescriptorReadRequest(String address, UUID uuid);
+
+ void onDescriptorWriteRequest(String address, UUID uuid, byte[] value,
+ boolean preparedWrite, boolean responseNeeded);
+
+ void onNotificationSent(String address, int status);
+
+ void onExecuteWrite(String address, boolean execute);
+
+ void onServiceAdded(UUID uuid, int status);
+
+ void onMtuChanged(String address, int mtu);
+ }
+
+ private final String mAddress;
+ private final Callback mCallback;
+ private final Context mContext;
+ private BluetoothGattServer mGattServer;
+ private final Map<UUID, BluetoothGattCharacteristic> mCharacteristics = new HashMap<>();
+
+ public BluetoothGattMaster(String address, Callback callback, Context context) {
+ this.mAddress = address;
+ this.mCallback = callback;
+ this.mContext = context;
+ DeviceShadowEnvironment.addDevice(address).bluetooth()
+ .setAdapterInitialState(BluetoothAdapter.STATE_ON);
+ }
+
+ public Future<Void> start(final BluetoothGattService service) {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
+ mGattServer = manager.openGattServer(mContext, mGattServerCallback);
+ mGattServer.addService(service);
+ }
+ });
+ }
+
+ public Future<Void> stop() {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ mGattServer.close();
+ }
+ });
+ }
+
+ public Future<Void> notifyCharacteristic(
+ final String remoteAddress, final UUID uuid, final byte[] value,
+ final boolean confirm) {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ BluetoothGattCharacteristic characteristic = mCharacteristics.get(uuid);
+ characteristic.setValue(value);
+ mGattServer.notifyCharacteristicChanged(
+ BluetoothAdapter.getDefaultAdapter().getRemoteDevice(remoteAddress),
+ characteristic, confirm);
+ }
+ });
+ }
+
+ private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {
+ @Override
+ public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
+ String address = device.getAddress();
+ Log.v(TAG, String.format(
+ "BluetoothGattServerManager.onConnectionStateChange on %s: status %d,"
+ + " newState %d", address, status, newState));
+ mCallback.onConnectionStateChange(address, status, newState);
+ }
+
+ @Override
+ public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
+ BluetoothGattCharacteristic characteristic) {
+ String address = device.getAddress();
+ UUID uuid = characteristic.getUuid();
+ Log.v(TAG,
+ String.format("BluetoothGattServerManager.onCharacteristicReadRequest on %s: "
+ + "characteristic %s, request %d, offset %d",
+ address, uuid, requestId, offset));
+ mCallback.onCharacteristicReadRequest(address, uuid);
+ mGattServer.sendResponse(
+ device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
+ characteristic.getValue());
+ }
+
+ @Override
+ public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId,
+ BluetoothGattCharacteristic characteristic, boolean preparedWrite,
+ boolean responseNeeded,
+ int offset, byte[] value) {
+ String address = device.getAddress();
+ UUID uuid = characteristic.getUuid();
+ Log.v(TAG,
+ String.format("BluetoothGattServerManager.onCharacteristicWriteRequest on %s: "
+ + "characteristic %s, request %d, offset %d, preparedWrite %b, "
+ + "responseNeeded %b",
+ address, uuid, requestId, offset, preparedWrite, responseNeeded));
+ mCallback.onCharacteristicWriteRequest(address, uuid, value, preparedWrite,
+ responseNeeded);
+
+ if (responseNeeded) {
+ mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
+ null);
+ }
+ }
+
+ @Override
+ public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset,
+ BluetoothGattDescriptor descriptor) {
+ String address = device.getAddress();
+ UUID uuid = descriptor.getUuid();
+ Log.v(TAG, String.format("BluetoothGattServerManager.onDescriptorReadRequest on %s: "
+ + " descriptor %s, requestId %d, offset %d",
+ address, uuid, requestId, offset));
+ mCallback.onDescriptorReadRequest(address, uuid);
+ mGattServer.sendResponse(
+ device, requestId, BluetoothGatt.GATT_SUCCESS, offset, descriptor.getValue());
+ }
+
+ @Override
+ public void onDescriptorWriteRequest(BluetoothDevice device, int requestId,
+ BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded,
+ int offset, byte[] value) {
+ String address = device.getAddress();
+ UUID uuid = descriptor.getUuid();
+ Log.v(TAG, String.format("BluetoothGattServerManager.onDescriptorWriteRequest on %s: "
+ + "descriptor %s, requestId %d, offset %d, preparedWrite %b, "
+ + "responseNeeded %b",
+ address, uuid, requestId, offset, preparedWrite, responseNeeded));
+ mCallback.onDescriptorWriteRequest(address, uuid, value, preparedWrite, responseNeeded);
+
+ if (responseNeeded) {
+ mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
+ null);
+ }
+ }
+
+ @Override
+ public void onNotificationSent(BluetoothDevice device, int status) {
+ String address = device.getAddress();
+ Log.v(TAG,
+ String.format("BluetoothGattServerManager.onNotificationSent on %s: status %d",
+ address, status));
+ mCallback.onNotificationSent(address, status);
+ }
+
+ @Override
+ public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
+ /*** Not implemented yet
+ String address = device.getAddress();
+ Log.v(TAG, String.format(
+ "BluetoothGattServerManager.onExecuteWrite on %s: requestId %d, execute %b",
+ address, requestId, execute));
+ callback.onExecuteWrite(address, execute);
+ */
+ }
+
+ @Override
+ public void onServiceAdded(int status, BluetoothGattService service) {
+ UUID uuid = service.getUuid();
+ Log.v(TAG, String.format(
+ "BluetoothGattServerManager.onServiceAdded: service %s, status %d",
+ uuid, status));
+ mCallback.onServiceAdded(uuid, status);
+
+ for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
+ mCharacteristics.put(characteristic.getUuid(), characteristic);
+ }
+ }
+
+ @Override
+ public void onMtuChanged(BluetoothDevice device, int mtu) {
+ Log.v(TAG, String.format("onMtuChanged(mtu: %s)", mtu));
+ mCallback.onMtuChanged(device.getAddress(), mtu);
+ }
+ };
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothRfcommAcceptor.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothRfcommAcceptor.java
new file mode 100644
index 0000000..5204c2a
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothRfcommAcceptor.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.helpers.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
+import android.util.Log;
+
+import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
+import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironmentInternal;
+import com.android.libraries.testing.deviceshadower.helpers.utils.IOUtils;
+
+import java.io.IOException;
+import java.util.Queue;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Helper class to operate a device with basic functionality to accept BluetoothRfcommConnection.
+ *
+ * <p>
+ * Usage: // Create a virtual device to accept incoming connection. BluetoothRfcommAcceptor acceptor
+ * = new BluetoothRfcommAcceptor(address, uuid, callback); // Start accepting incoming connection,
+ * with given uuid. acceptor.start(); // Connector needs to wait till acceptor started to make sure
+ * there is a server socket created. acceptor.waitTillServerSocketStarted();
+ *
+ * // Connector can initiate connection.
+ *
+ * // A blocking call to wait for connection. acceptor.waitTillConnected();
+ *
+ * // Acceptor sends a message acceptor.send("Hello".getBytes());
+ *
+ * // Cancel acceptor to release all blocking calls. acceptor.cancel();
+ */
+public class BluetoothRfcommAcceptor {
+
+ private static final String TAG = "BluetoothRfcommAcceptor";
+
+ /**
+ * Identifiers to control Bluetooth operation.
+ */
+ public static final int PRE_START = 4;
+ public static final int PRE_ACCEPT = 1;
+ public static final int PRE_WRITE = 3;
+ public static final int PRE_READ = 2;
+
+ private final String mAddress;
+ private final UUID mUuid;
+ private BluetoothSocket mSocket;
+ private BluetoothServerSocket mServerSocket;
+
+ private final AtomicBoolean mCancelled;
+ private final Callback mCallback;
+ private final CountDownLatch mStartLatch = new CountDownLatch(1);
+ private final CountDownLatch mConnectLatch = new CountDownLatch(1);
+ private final Queue<CountDownLatch> mReadLatches = new ConcurrentLinkedQueue<>();
+
+ /**
+ * Callback of BluetoothRfcommAcceptor.
+ */
+ public interface Callback {
+
+ void onSocketAccepted(BluetoothSocket socket);
+
+ void onDataReceived(byte[] data);
+
+ void onDataWritten(byte[] data);
+
+ void onError(Exception exception);
+ }
+
+ public BluetoothRfcommAcceptor(String address, UUID uuid, Callback callback) {
+ this.mAddress = address;
+ this.mUuid = uuid;
+ this.mCallback = callback;
+ this.mCancelled = new AtomicBoolean(false);
+ DeviceShadowEnvironment.addDevice(address).bluetooth()
+ .setAdapterInitialState(BluetoothAdapter.STATE_ON);
+ }
+
+ /**
+ * Start bluetooth server socket, accept incoming connection, and receive incoming data once
+ * connected.
+ */
+ public Future<Void> start() {
+ return DeviceShadowEnvironment.run(mAddress, mCode);
+ }
+
+ /**
+ * Blocking call to wait bluetooth server socket started.
+ */
+ public void waitTillServerSocketStarted() {
+ try {
+ mStartLatch.await();
+ } catch (InterruptedException e) {
+ Log.w(TAG, mAddress + " fail to wait till started: ", e);
+ }
+ }
+
+ public void waitTillConnected() {
+ try {
+ mConnectLatch.await();
+ } catch (InterruptedException e) {
+ Log.w(TAG, mAddress + " fail to wait till started: ", e);
+ }
+ }
+
+ public void waitTillDataReceived() {
+ try {
+ if (mReadLatches.size() > 0) {
+ mReadLatches.poll().await();
+ }
+ } catch (InterruptedException e) {
+ // no-op
+ }
+ }
+
+ /**
+ * Stop receiving data by closing socket.
+ */
+ public Future<Void> cancel() {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ mCancelled.set(true);
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, mAddress + " fail to close server socket", e);
+ }
+ }
+ });
+ }
+
+ /**
+ * Send data to connected device.
+ */
+ public Future<Void> send(final byte[] data) {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ if (mSocket != null) {
+ try {
+ DeviceShadowEnvironmentInternal.setInterruptibleBluetooth(PRE_WRITE);
+ IOUtils.write(mSocket.getOutputStream(), data);
+ Log.d(TAG, mAddress + " write: " + new String(data));
+ mCallback.onDataWritten(data);
+ } catch (IOException e) {
+ Log.w(TAG, mAddress + " fail to write: ", e);
+ mCallback.onError(new IOException("Fail to write", e));
+ }
+ }
+ }
+ });
+ }
+
+ private Runnable mCode = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ DeviceShadowEnvironmentInternal.setInterruptibleBluetooth(PRE_START);
+ mServerSocket = BluetoothAdapter.getDefaultAdapter()
+ .listenUsingInsecureRfcommWithServiceRecord("AA", mUuid);
+ } catch (IOException e) {
+ Log.w(TAG, mAddress + " fail to start server socket: ", e);
+ mCallback.onError(new IOException("Fail to start server socket", e));
+ return;
+ } finally {
+ mStartLatch.countDown();
+ }
+
+ try {
+ DeviceShadowEnvironmentInternal.setInterruptibleBluetooth(PRE_ACCEPT);
+ mSocket = mServerSocket.accept();
+ Log.d(TAG, mAddress + " accept: " + mSocket.getRemoteDevice().getAddress());
+ mCallback.onSocketAccepted(mSocket);
+ mServerSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, mAddress + " fail to connect: ", e);
+ mCallback.onError(new IOException("Fail to connect", e));
+ return;
+ } finally {
+ mConnectLatch.countDown();
+ }
+
+ do {
+ try {
+ CountDownLatch latch = new CountDownLatch(1);
+ mReadLatches.add(latch);
+ DeviceShadowEnvironmentInternal.setInterruptibleBluetooth(PRE_READ);
+ byte[] data = IOUtils.read(mSocket.getInputStream());
+ Log.d(TAG, mAddress + " read: " + new String(data));
+ mCallback.onDataReceived(data);
+ latch.countDown();
+ } catch (IOException e) {
+ Log.w(TAG, mAddress + " fail to read: ", e);
+ mCallback.onError(new IOException("Fail to read", e));
+ return;
+ }
+ } while (!mCancelled.get());
+
+ Log.d(TAG, mAddress + " stop receiving");
+ }
+ };
+
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothRfcommConnector.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothRfcommConnector.java
new file mode 100644
index 0000000..e386d59
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/bluetooth/BluetoothRfcommConnector.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.helpers.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothSocket;
+import android.util.Log;
+
+import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
+import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironmentInternal;
+import com.android.libraries.testing.deviceshadower.helpers.utils.IOUtils;
+
+import java.io.IOException;
+import java.util.Queue;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Helper class to operate a device with basic functionality to accept BluetoothRfcommConnection.
+ *
+ * <p>
+ * Usage: // Create a virtual device to initiate connection. BluetoothRfcommConnector connector =
+ * new BluetoothRfcommConnector(address, callback); // Start connection to a remote address with
+ * given uuid. connector.start(remoteAddress, remoteUuid);
+ *
+ * // A blocking call to wait for connection. connector.waitTillConnected();
+ *
+ * // Connector sends a message connector.send("Hello".getBytes());
+ *
+ * // Cancel connector to release all blocking calls. connector.cancel();
+ */
+public class BluetoothRfcommConnector {
+
+ private static final String TAG = "BluetoothRfcommConnector";
+
+ /**
+ * Identifiers to control Bluetooth operation.
+ */
+ public static final int PRE_CONNECT = 1;
+ public static final int PRE_READ = 2;
+ public static final int PRE_WRITE = 3;
+
+ private final String mAddress;
+ private String mRemoteAddress = null;
+ private final UUID mRemoteUuid;
+ private BluetoothSocket mSocket;
+
+ private final Callback mCallback;
+ private final AtomicBoolean mCancelled;
+ private final CountDownLatch mConnectLatch = new CountDownLatch(1);
+ private final Queue<CountDownLatch> mReadLatches = new ConcurrentLinkedQueue<>();
+
+ /**
+ * Callback of BluetoothRfcommConnector.
+ */
+ public interface Callback {
+
+ void onConnected(BluetoothSocket socket);
+
+ void onDataReceived(byte[] data);
+
+ void onDataWritten(byte[] data);
+
+ void onError(Exception exception);
+ }
+
+ public BluetoothRfcommConnector(String address, UUID uuid, Callback callback) {
+ this.mAddress = address;
+ this.mRemoteUuid = uuid;
+ this.mCallback = callback;
+ this.mCancelled = new AtomicBoolean(false);
+ DeviceShadowEnvironment.addDevice(address).bluetooth()
+ .setAdapterInitialState(BluetoothAdapter.STATE_ON);
+ }
+
+ /**
+ * Start connection to a remote address, and receive data once connected.
+ */
+ public Future<Void> start(String remoteAddress) {
+ this.mRemoteAddress = remoteAddress;
+ return DeviceShadowEnvironment.run(mAddress, mCode);
+ }
+
+ /**
+ * Stop receiving data.
+ */
+ public Future<Void> cancel() {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ mCancelled.set(true);
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, mAddress + " fail to close socket", e);
+ }
+ }
+ });
+ }
+
+ public void waitTillConnected() {
+ try {
+ mConnectLatch.await();
+ } catch (InterruptedException e) {
+ Log.w(TAG, mAddress + " fail to wait till started: ", e);
+ }
+ }
+
+ public void waitTillDataReceived() {
+ try {
+ if (mReadLatches.size() > 0) {
+ mReadLatches.poll().await();
+ }
+ } catch (InterruptedException e) {
+ // no-op.
+ }
+ }
+
+ /**
+ * Send data to conneceted device.
+ */
+ public Future<Void> send(final byte[] data) {
+ return DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ if (mSocket != null) {
+ try {
+ DeviceShadowEnvironmentInternal.setInterruptibleBluetooth(PRE_WRITE);
+ IOUtils.write(mSocket.getOutputStream(), data);
+ Log.d(TAG, mAddress + " write: " + new String(data));
+ mCallback.onDataWritten(data);
+ } catch (IOException e) {
+ Log.w(TAG, mAddress + " fail to write: ", e);
+ mCallback.onError(new IOException("Fail to write", e));
+ }
+ }
+ }
+ });
+ }
+
+ private Runnable mCode = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ DeviceShadowEnvironmentInternal.setInterruptibleBluetooth(PRE_CONNECT);
+ mSocket = BluetoothAdapter.getDefaultAdapter()
+ .getRemoteDevice(mRemoteAddress)
+ .createInsecureRfcommSocketToServiceRecord(mRemoteUuid);
+ mSocket.connect();
+ Log.d(TAG, mAddress + " accept: " + mSocket.getRemoteDevice().getAddress());
+ mCallback.onConnected(mSocket);
+ } catch (IOException e) {
+ Log.w(TAG, mAddress + " fail to connect: ", e);
+ mCallback.onError(new IOException("Fail to connect", e));
+ } finally {
+ mConnectLatch.countDown();
+ }
+
+ try {
+ do {
+ CountDownLatch latch = new CountDownLatch(1);
+ mReadLatches.add(latch);
+ DeviceShadowEnvironmentInternal.setInterruptibleBluetooth(PRE_READ);
+ byte[] data = IOUtils.read(mSocket.getInputStream());
+ Log.d(TAG, mAddress + " read: " + new String(data));
+ mCallback.onDataReceived(data);
+ latch.countDown();
+ } while (!mCancelled.get());
+ } catch (IOException e) {
+ Log.w(TAG, mAddress + " fail to read: ", e);
+ mCallback.onError(new IOException("Fail to read", e));
+ }
+ Log.d(TAG, mAddress + " stop receiving");
+ }
+ };
+
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcActivity.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcActivity.java
new file mode 100644
index 0000000..8ae4435
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcActivity.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.helpers.nfc;
+
+import android.app.Activity;
+
+/**
+ * Activity that triggers or receives NFC events.
+ */
+public class NfcActivity extends Activity {
+
+ private NfcReceiver.Callback mCallback;
+
+ public void setCallback(NfcReceiver.Callback callback) {
+ this.mCallback = callback;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ NfcReceiver.processIntent(mCallback, getIntent());
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcReceiver.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcReceiver.java
new file mode 100644
index 0000000..b85a124
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcReceiver.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.helpers.nfc;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.nfc.NdefMessage;
+import android.nfc.NfcAdapter;
+import android.os.Parcelable;
+import android.util.Log;
+
+import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Helper class to receive NFC events.
+ */
+public class NfcReceiver {
+
+ private static final String TAG = "NfcReceiver";
+
+ /**
+ * Callback to receive message.
+ */
+ public interface Callback {
+
+ void onReceive(String message);
+ }
+
+ private final String mAddress;
+ private final Activity mActivity;
+ private CountDownLatch mReceiveLatch;
+
+ private final BroadcastReceiver mReceiver;
+ private final IntentFilter mFilter;
+
+ public NfcReceiver(String address, Activity activity, final Callback callback) {
+ this(address, activity, new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
+ processIntent(callback, intent);
+ }
+ }
+ });
+ DeviceShadowEnvironment.addDevice(address);
+ }
+
+ public NfcReceiver(
+ final String address, Activity activity, final BroadcastReceiver clientReceiver) {
+ this.mAddress = address;
+ this.mActivity = activity;
+
+ this.mFilter = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
+ this.mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.v(TAG, "Receive broadcast on device " + address);
+ clientReceiver.onReceive(context, intent);
+ mReceiveLatch.countDown();
+ }
+ };
+ DeviceShadowEnvironment.addDevice(address);
+ }
+
+ public void startReceive() throws InterruptedException, ExecutionException {
+ mReceiveLatch = new CountDownLatch(1);
+
+ DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ mActivity.getApplication().registerReceiver(mReceiver, mFilter);
+ }
+ }).get();
+ }
+
+ public void waitUntilReceive(long timeoutMillis) throws InterruptedException {
+ mReceiveLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
+ }
+
+ public void stopReceive() throws InterruptedException, ExecutionException {
+ DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ mActivity.getApplication().unregisterReceiver(mReceiver);
+ }
+ }).get();
+ }
+
+ static void processIntent(Callback callback, Intent intent) {
+ Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
+ if (rawMsgs != null && rawMsgs.length > 0) {
+ // only one message sent during the beam
+ NdefMessage msg = (NdefMessage) rawMsgs[0];
+ if (callback != null) {
+ callback.onReceive(new String(msg.getRecords()[0].getPayload()));
+ }
+ }
+ }
+
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcSender.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcSender.java
new file mode 100644
index 0000000..dbbb5fa
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/nfc/NfcSender.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.helpers.nfc;
+
+import android.app.Activity;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcAdapter.CreateNdefMessageCallback;
+import android.nfc.NfcAdapter.OnNdefPushCompleteCallback;
+import android.nfc.NfcEvent;
+
+import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
+
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Helper class to send NFC events.
+ */
+public class NfcSender {
+
+ private static final String NFC_PACKAGE = "DS_PKG";
+ private static final String NFC_TAG = "DS_TAG";
+
+ /**
+ * Callback to update sender status.
+ */
+ public interface Callback {
+
+ void onSend(String message);
+ }
+
+ private final String mAddress;
+ private final Activity mActivity;
+ private final Callback mCallback;
+ private final SenderCallback mSenderCallback;
+ private String mSessage;
+
+ public NfcSender(String address, Activity activity, Callback callback) {
+ this.mCallback = callback;
+ this.mAddress = address;
+ this.mActivity = activity;
+ DeviceShadowEnvironment.addDevice(address);
+ this.mSenderCallback = new SenderCallback();
+ }
+
+ public void startSend(String message) throws InterruptedException, ExecutionException {
+ this.mSessage = message;
+ DeviceShadowEnvironment.run(mAddress, new Runnable() {
+ @Override
+ public void run() {
+ NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(mActivity);
+ nfcAdapter.setNdefPushMessageCallback(mSenderCallback, mActivity);
+ nfcAdapter.setOnNdefPushCompleteCallback(mSenderCallback, mActivity);
+ }
+ }).get();
+ }
+
+ class SenderCallback implements CreateNdefMessageCallback, OnNdefPushCompleteCallback {
+
+ @Override
+ public NdefMessage createNdefMessage(NfcEvent event) {
+ NdefMessage msg = new NdefMessage(new NdefRecord[]{
+ NdefRecord.createExternal(NFC_PACKAGE, NFC_TAG, mSessage.getBytes())
+ });
+ return msg;
+ }
+
+ @Override
+ public void onNdefPushComplete(NfcEvent event) {
+ mCallback.onSend(mSessage);
+ }
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/utils/IOUtils.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/utils/IOUtils.java
new file mode 100644
index 0000000..d89754b
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/helpers/utils/IOUtils.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.helpers.utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * Utils for IO methods.
+ */
+public class IOUtils {
+
+ /**
+ * Write num of bytes to be sent and payload through OutputStream.
+ */
+ public static void write(OutputStream os, byte[] data) throws IOException {
+ ByteBuffer buffer = ByteBuffer.allocate(4 + data.length).putInt(data.length).put(data);
+ os.write(buffer.array());
+ }
+
+ /**
+ * Read num of bytes to be read, and payload through InputStream.
+ *
+ * @return payload received.
+ */
+ public static byte[] read(InputStream is) throws IOException {
+ byte[] size = new byte[4];
+ is.read(size, 0, 4 /* bytes of int type */);
+
+ byte[] data = new byte[ByteBuffer.wrap(size).getInt()];
+ is.read(data);
+ return data;
+ }
+
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceShadowEnvironmentImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceShadowEnvironmentImpl.java
new file mode 100644
index 0000000..6a06ce4
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceShadowEnvironmentImpl.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal;
+
+import android.content.ContentProvider;
+import android.os.Looper;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.libraries.testing.deviceshadower.Enums.Distance;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.BlueletImpl;
+import com.android.libraries.testing.deviceshadower.internal.common.NamedRunnable;
+import com.android.libraries.testing.deviceshadower.internal.common.Scheduler;
+import com.android.libraries.testing.deviceshadower.internal.nfc.NfcletImpl;
+import com.android.libraries.testing.deviceshadower.internal.sms.SmsContentProvider;
+import com.android.libraries.testing.deviceshadower.internal.sms.SmsletImpl;
+import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
+
+import com.google.common.collect.ImmutableList;
+
+import org.robolectric.Shadows;
+import org.robolectric.shadows.ShadowLooper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Proxy to manage internal data models, and help shadows to exchange data.
+ */
+public class DeviceShadowEnvironmentImpl {
+
+ private static final Logger LOGGER = Logger.create("DeviceShadowEnvironmentImpl");
+ private static final long SCHEDULER_WAIT_TIMEOUT_MILLIS = 5000L;
+
+ // ThreadLocal to store local address for each device.
+ private static InheritableThreadLocal<DeviceletImpl> sLocalDeviceletImpl =
+ new InheritableThreadLocal<>();
+
+ // Devicelets contains all registered devicelet to simulate a device.
+ private static final Map<String, DeviceletImpl> DEVICELETS = new ConcurrentHashMap<>();
+
+ @VisibleForTesting
+ static final Map<String, ExecutorService> EXECUTORS = new ConcurrentHashMap<>();
+
+ private static final List<DeviceShadowException> INTERNAL_EXCEPTIONS =
+ Collections.synchronizedList(new ArrayList<DeviceShadowException>());
+
+ private static final ContentProvider smsContentProvider = new SmsContentProvider();
+
+ public static DeviceletImpl getDeviceletImpl(String address) {
+ return DEVICELETS.get(address);
+ }
+
+ public static void checkInternalExceptions() {
+ if (INTERNAL_EXCEPTIONS.size() > 0) {
+ for (DeviceShadowException exception : INTERNAL_EXCEPTIONS) {
+ LOGGER.e("Internal exception", exception);
+ }
+ INTERNAL_EXCEPTIONS.clear();
+ throw new RuntimeException("DeviceShadower has internal exceptions");
+ }
+ }
+
+ public static void reset() {
+ // reset local devicelet for single device testing
+ sLocalDeviceletImpl.remove();
+ DEVICELETS.clear();
+ BlueletImpl.reset();
+ INTERNAL_EXCEPTIONS.clear();
+ }
+
+ public static boolean await(long timeoutMillis) {
+ boolean schedulerDone = false;
+ try {
+ schedulerDone = Scheduler.await(timeoutMillis);
+ } catch (InterruptedException e) {
+ // no-op.
+ } finally {
+ if (!schedulerDone) {
+ catchInternalException(new DeviceShadowException("Scheduler not complete"));
+ for (DeviceletImpl devicelet : DEVICELETS.values()) {
+ LOGGER.e(
+ String.format(
+ "Device %s\n\tUI: %s\n\tService: %s",
+ devicelet.getAddress(),
+ devicelet.getUiScheduler(),
+ devicelet.getServiceScheduler()));
+ }
+ Scheduler.clear();
+ }
+ }
+ for (ExecutorService executor : EXECUTORS.values()) {
+ executor.shutdownNow();
+ }
+ boolean terminateSuccess = true;
+ for (ExecutorService executor : EXECUTORS.values()) {
+ try {
+ executor.awaitTermination(timeoutMillis, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ terminateSuccess = false;
+ }
+ if (!executor.isTerminated()) {
+ LOGGER.e("Failed to terminate executor.");
+ terminateSuccess = false;
+ }
+ }
+ EXECUTORS.clear();
+ return schedulerDone && terminateSuccess;
+ }
+
+ public static boolean hasLocalDeviceletImpl() {
+ return sLocalDeviceletImpl.get() != null;
+ }
+
+ public static DeviceletImpl getLocalDeviceletImpl() {
+ return sLocalDeviceletImpl.get();
+ }
+
+ public static List<DeviceletImpl> getDeviceletImpls() {
+ return ImmutableList.copyOf(DEVICELETS.values());
+ }
+
+ public static BlueletImpl getLocalBlueletImpl() {
+ return sLocalDeviceletImpl.get().blueletImpl();
+ }
+
+ public static BlueletImpl getBlueletImpl(String address) {
+ DeviceletImpl devicelet = getDeviceletImpl(address);
+ return devicelet == null ? null : devicelet.blueletImpl();
+ }
+
+ public static NfcletImpl getLocalNfcletImpl() {
+ return sLocalDeviceletImpl.get().nfcletImpl();
+ }
+
+ public static NfcletImpl getNfcletImpl(String address) {
+ DeviceletImpl devicelet = getDeviceletImpl(address);
+ return devicelet == null ? null : devicelet.nfcletImpl();
+ }
+
+ public static SmsletImpl getLocalSmsletImpl() {
+ return sLocalDeviceletImpl.get().smsletImpl();
+ }
+
+ public static ContentProvider getSmsContentProvider() {
+ return smsContentProvider;
+ }
+
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public static DeviceletImpl addDevice(String address) {
+ EXECUTORS.put(address, Executors.newCachedThreadPool());
+
+ // DeviceShadower keeps track of the "local" device based on the current thread. It uses an
+ // InheritableThreadLocal, so threads created by the current thread also get the same
+ // thread-local value. Add the device on its own thread, to set the thread local for that
+ // thread and its children.
+ try {
+ EXECUTORS
+ .get(address)
+ .submit(
+ () -> {
+ DeviceletImpl devicelet = new DeviceletImpl(address);
+ DEVICELETS.put(address, devicelet);
+ setLocalDevice(address);
+ // Ensure these threads are actually created, by posting one empty
+ // runnable.
+ devicelet.getServiceScheduler()
+ .post(NamedRunnable.create("Init", () -> {
+ }));
+ devicelet.getUiScheduler().post(NamedRunnable.create("Init", () -> {
+ }));
+ })
+ .get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new IllegalStateException(e);
+ }
+
+ return DEVICELETS.get(address);
+ }
+
+ public static void removeDevice(String address) {
+ DEVICELETS.remove(address);
+ EXECUTORS.remove(address);
+ }
+
+ public static void setInterruptibleBluetooth(int identifier) {
+ getLocalBlueletImpl().setInterruptible(identifier);
+ }
+
+ public static void interruptBluetooth(String address, int identifier) {
+ getBlueletImpl(address).interrupt(identifier);
+ }
+
+ public static void setDistance(String address1, String address2, final Distance distance) {
+ final DeviceletImpl device1 = getDeviceletImpl(address1);
+ final DeviceletImpl device2 = getDeviceletImpl(address2);
+
+ Future<Void> result1 = null;
+ Future<Void> result2 = null;
+ if (device1.updateDistance(address2, distance)) {
+ result1 =
+ run(
+ address1,
+ () -> {
+ device1.onDistanceChange(device2, distance);
+ return null;
+ });
+ }
+
+ if (device2.updateDistance(address1, distance)) {
+ result2 =
+ run(
+ address2,
+ () -> {
+ device2.onDistanceChange(device1, distance);
+ return null;
+ });
+ }
+
+ try {
+ if (result1 != null) {
+ result1.get();
+ }
+ if (result2 != null) {
+ result2.get();
+ }
+ } catch (InterruptedException | ExecutionException e) {
+ catchInternalException(new DeviceShadowException(e));
+ }
+ }
+
+ /**
+ * Set local Bluelet for current thread.
+ *
+ * <p>This can be used to convert current running thread to hold a bluelet object, so that unit
+ * test does not have to call BluetoothEnvironment.run() to run code.
+ */
+ @VisibleForTesting
+ public static void setLocalDevice(String address) {
+ DeviceletImpl local = DEVICELETS.get(address);
+ if (local == null) {
+ throw new RuntimeException(address + " is not initialized by BluetoothEnvironment");
+ }
+ sLocalDeviceletImpl.set(local);
+ }
+
+ public static <T> Future<T> run(final String address, final Callable<T> snippet) {
+ return EXECUTORS
+ .get(address)
+ .submit(
+ () -> {
+ DeviceShadowEnvironmentImpl.setLocalDevice(address);
+ ShadowLooper mainLooper = Shadows.shadowOf(Looper.getMainLooper());
+ try {
+ T result = snippet.call();
+
+ // Avoid idling the main looper in paused mode since doing so is
+ // only allowed from the main thread.
+ if (!mainLooper.isPaused()) {
+ // In Robolectric, runnable doesn't run when posting thread
+ // differs from looper thread, idle main looper explicitly to
+ // execute posted Runnables.
+ ShadowLooper.idleMainLooper();
+ }
+
+ // Wait all scheduled runnables complete.
+ Scheduler.await(SCHEDULER_WAIT_TIMEOUT_MILLIS);
+ return result;
+ } catch (Exception e) {
+ LOGGER.e("Fail to call code on device: " + address, e);
+ if (!mainLooper.isPaused()) {
+ // reset() is not supported in paused mode.
+ mainLooper.reset();
+ }
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ // @CanIgnoreReturnValue
+ // Return value can be ignored because {@link Scheduler} will call
+ // {@link catchInternalException} to catch exceptions, and throw when test completes.
+ public static Future<?> runOnUi(String address, NamedRunnable snippet) {
+ Scheduler scheduler = DeviceShadowEnvironmentImpl.getDeviceletImpl(address)
+ .getUiScheduler();
+ return run(scheduler, address, snippet);
+ }
+
+ // @CanIgnoreReturnValue
+ // Return value can be ignored because {@link Scheduler} will call
+ // {@link catchInternalException} to catch exceptions, and throw when test completes.
+ public static Future<?> runOnService(String address, NamedRunnable snippet) {
+ Scheduler scheduler =
+ DeviceShadowEnvironmentImpl.getDeviceletImpl(address).getServiceScheduler();
+ return run(scheduler, address, snippet);
+ }
+
+ // @CanIgnoreReturnValue
+ // Return value can be ignored because {@link Scheduler} will call
+ // {@link catchInternalException} to catch exceptions, and throw when test completes.
+ private static Future<?> run(
+ Scheduler scheduler, final String address, final NamedRunnable snippet) {
+ return scheduler.post(
+ NamedRunnable.create(
+ snippet.toString(),
+ () -> {
+ DeviceShadowEnvironmentImpl.setLocalDevice(address);
+ snippet.run();
+ }));
+ }
+
+ public static void catchInternalException(Exception exception) {
+ INTERNAL_EXCEPTIONS.add(new DeviceShadowException(exception));
+ }
+
+ // This is used to test Device Shadower internal.
+ @VisibleForTesting
+ public static void setDeviceletForTest(String address, DeviceletImpl devicelet) {
+ DEVICELETS.put(address, devicelet);
+ }
+
+ @VisibleForTesting
+ public static void setExecutorForTest(String address) {
+ setExecutorForTest(address, Executors.newCachedThreadPool());
+ }
+
+ @VisibleForTesting
+ public static void setExecutorForTest(String address, ExecutorService executor) {
+ EXECUTORS.put(address, executor);
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceShadowException.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceShadowException.java
new file mode 100644
index 0000000..77d358f
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceShadowException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal;
+
+/**
+ * Internal exception to indicate error from DeviceShadower framework.
+ */
+public class DeviceShadowException extends Exception {
+
+ public DeviceShadowException(Throwable e) {
+ super(e);
+ }
+
+ public DeviceShadowException(String msg) {
+ super(msg);
+ }
+
+ public DeviceShadowException(String msg, Throwable e) {
+ super(msg, e);
+ }
+
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceletImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceletImpl.java
new file mode 100644
index 0000000..9aea065
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/DeviceletImpl.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal;
+
+import com.android.libraries.testing.deviceshadower.Bluelet;
+import com.android.libraries.testing.deviceshadower.Devicelet;
+import com.android.libraries.testing.deviceshadower.Enums.Distance;
+import com.android.libraries.testing.deviceshadower.Nfclet;
+import com.android.libraries.testing.deviceshadower.Smslet;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.BlueletImpl;
+import com.android.libraries.testing.deviceshadower.internal.common.BroadcastManager;
+import com.android.libraries.testing.deviceshadower.internal.common.Scheduler;
+import com.android.libraries.testing.deviceshadower.internal.nfc.NfcletImpl;
+import com.android.libraries.testing.deviceshadower.internal.sms.SmsletImpl;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * DeviceletImpl is the implementation to hold different medium-let in DeviceShadowEnvironment.
+ */
+public class DeviceletImpl implements Devicelet {
+
+ private final BlueletImpl mBluelet;
+ private final NfcletImpl mNfclet;
+ private final SmsletImpl mSmslet;
+ private final BroadcastManager mBroadcastManager;
+ private final String mAddress;
+ private final Map<String, Distance> mDistanceMap = new HashMap<>();
+ private final Scheduler mServiceScheduler;
+ private final Scheduler mUiScheduler;
+
+ public DeviceletImpl(String address) {
+ this.mAddress = address;
+ this.mServiceScheduler = new Scheduler(address + "-service");
+ this.mUiScheduler = new Scheduler(address + "-main");
+ this.mBroadcastManager = new BroadcastManager(mUiScheduler);
+ this.mBluelet = new BlueletImpl(address, mBroadcastManager);
+ this.mNfclet = new NfcletImpl();
+ this.mSmslet = new SmsletImpl();
+ }
+
+ @Override
+ public Bluelet bluetooth() {
+ return mBluelet;
+ }
+
+ public BlueletImpl blueletImpl() {
+ return mBluelet;
+ }
+
+ @Override
+ public Nfclet nfc() {
+ return mNfclet;
+ }
+
+ public NfcletImpl nfcletImpl() {
+ return mNfclet;
+ }
+
+ @Override
+ public Smslet sms() {
+ return mSmslet;
+ }
+
+ public SmsletImpl smsletImpl() {
+ return mSmslet;
+ }
+
+ public BroadcastManager getBroadcastManager() {
+ return mBroadcastManager;
+ }
+
+ @Override
+ public String getAddress() {
+ return mAddress;
+ }
+
+ Scheduler getServiceScheduler() {
+ return mServiceScheduler;
+ }
+
+ Scheduler getUiScheduler() {
+ return mUiScheduler;
+ }
+
+ /**
+ * Update distance to remote device.
+ *
+ * @return true if distance updated.
+ */
+ /*package*/ boolean updateDistance(String remoteAddress, Distance distance) {
+ Distance currentDistance = mDistanceMap.get(remoteAddress);
+ if (currentDistance == null || !distance.equals(currentDistance)) {
+ mDistanceMap.put(remoteAddress, distance);
+ return true;
+ }
+ return false;
+ }
+
+ /*package*/ void onDistanceChange(DeviceletImpl remote, Distance distance) {
+ if (distance == Distance.NEAR) {
+ mNfclet.onNear(remote.mNfclet);
+ }
+ }
+
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/AdapterDelegate.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/AdapterDelegate.java
new file mode 100644
index 0000000..b5227b7
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/AdapterDelegate.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothClass.Device;
+import android.os.Build.VERSION;
+
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+import com.android.libraries.testing.deviceshadower.internal.DeviceletImpl;
+import com.android.libraries.testing.deviceshadower.internal.common.NamedRunnable;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Class handling Bluetooth Adapter State change. Currently async event processing is not supported,
+ * and there is no deferred operation when adapter is in a pending state.
+ */
+class AdapterDelegate {
+
+ /**
+ * Callback for adapter
+ */
+ public interface Callback {
+
+ void onAdapterStateChange(State prevState, State newState);
+
+ void onBleStateChange(State prevState, State newState);
+
+ void onDiscoveryStarted();
+
+ void onDiscoveryFinished();
+
+ void onDeviceFound(String address, int bluetoothClass, String name);
+ }
+
+ @GuardedBy("this")
+ private State mCurrentState;
+
+ private final String mAddress;
+ private final Callback mCallback;
+ private AtomicBoolean mIsDiscovering = new AtomicBoolean(false);
+ private final AtomicInteger mScanMode = new AtomicInteger(BluetoothAdapter.SCAN_MODE_NONE);
+ private int mBluetoothClass = Device.PHONE_SMART;
+
+ AdapterDelegate(String address, Callback callback) {
+ this.mAddress = address;
+ this.mCurrentState = State.OFF;
+ this.mCallback = callback;
+ }
+
+ synchronized void processEvent(Event event) {
+ State newState = TRANSITION[mCurrentState.ordinal()][event.ordinal()];
+ if (newState == null) {
+ return;
+ }
+ State prevState = mCurrentState;
+ mCurrentState = newState;
+ handleStateChange(prevState, newState);
+ }
+
+ private void handleStateChange(State prevState, State newState) {
+ // TODO(b/200231384): fake service bind/unbind on state change
+ if (prevState.equals(newState)) {
+ return;
+ }
+ if (VERSION.SDK_INT < 23) {
+ mCallback.onAdapterStateChange(prevState, newState);
+ } else {
+ mCallback.onBleStateChange(prevState, newState);
+ if (newState.equals(State.BLE_TURNING_ON)
+ || newState.equals(State.BLE_TURNING_OFF)
+ || newState.equals(State.OFF)
+ || (newState.equals(State.BLE_ON) && prevState.equals(State.BLE_TURNING_ON))) {
+ return;
+ }
+ if (newState.equals(State.BLE_ON)) {
+ newState = State.OFF;
+ } else if (prevState.equals(State.BLE_ON)) {
+ prevState = State.OFF;
+ }
+ mCallback.onAdapterStateChange(prevState, newState);
+ }
+ }
+
+ synchronized State getState() {
+ return mCurrentState;
+ }
+
+ synchronized void setState(State state) {
+ mCurrentState = state;
+ }
+
+ void setBluetoothClass(int bluetoothClass) {
+ this.mBluetoothClass = bluetoothClass;
+ }
+
+ int getBluetoothClass() {
+ return mBluetoothClass;
+ }
+
+ @SuppressWarnings("FutureReturnValueIgnored")
+ void startDiscovery() {
+ synchronized (this) {
+ if (mIsDiscovering.get()) {
+ return;
+ }
+ mIsDiscovering.set(true);
+ }
+
+ mCallback.onDiscoveryStarted();
+
+ NamedRunnable onDeviceFound =
+ NamedRunnable.create(
+ "BluetoothAdapter.onDeviceFound",
+ new Runnable() {
+ @Override
+ public void run() {
+ List<DeviceletImpl> devices =
+ DeviceShadowEnvironmentImpl.getDeviceletImpls();
+ for (DeviceletImpl devicelet : devices) {
+ BlueletImpl bluelet = devicelet.blueletImpl();
+ if (mAddress.equals(devicelet.getAddress())
+ || bluelet.getAdapterDelegate().mScanMode.get()
+ != BluetoothAdapter
+ .SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
+ continue;
+ }
+ mCallback.onDeviceFound(
+ bluelet.address,
+ bluelet.getAdapterDelegate().mBluetoothClass,
+ bluelet.mName);
+ }
+ finishDiscovery();
+ }
+ });
+
+ DeviceShadowEnvironmentImpl.runOnUi(mAddress, onDeviceFound);
+ }
+
+ void cancelDiscovery() {
+ finishDiscovery();
+ }
+
+ boolean isDiscovering() {
+ return mIsDiscovering.get();
+ }
+
+ void setScanMode(int scanMode) {
+ // TODO(b/200231384): broadcast scan mode change.
+ this.mScanMode.set(scanMode);
+ }
+
+ int getScanMode() {
+ return mScanMode.get();
+ }
+
+ private void finishDiscovery() {
+ synchronized (this) {
+ if (!mIsDiscovering.get()) {
+ return;
+ }
+ mIsDiscovering.set(false);
+ }
+ mCallback.onDiscoveryFinished();
+ }
+
+ enum State {
+ OFF(BluetoothAdapter.STATE_OFF),
+ TURNING_ON(BluetoothAdapter.STATE_TURNING_ON),
+ ON(BluetoothAdapter.STATE_ON),
+ TURNING_OFF(BluetoothAdapter.STATE_TURNING_OFF),
+ // States for API23+
+ BLE_TURNING_ON(BluetoothConstants.STATE_BLE_TURNING_ON),
+ BLE_ON(BluetoothConstants.STATE_BLE_ON),
+ BLE_TURNING_OFF(BluetoothConstants.STATE_BLE_TURNING_OFF);
+
+ private static final Map<Integer, State> LOOKUP = new HashMap<>();
+
+ static {
+ for (State state : State.values()) {
+ LOOKUP.put(state.getValue(), state);
+ }
+ }
+
+ static State lookup(int value) {
+ return LOOKUP.get(value);
+ }
+
+ private final int mValue;
+
+ State(int value) {
+ this.mValue = value;
+ }
+
+ int getValue() {
+ return mValue;
+ }
+ }
+
+ /*
+ * Represents Bluetooth events which can trigger adapter state change.
+ */
+ enum Event {
+ USER_TURN_ON,
+ USER_TURN_OFF,
+ BREDR_STARTED,
+ BREDR_STOPPED,
+ // Events for API23+
+ BLE_TURN_ON,
+ BLE_TURN_OFF,
+ BLE_STARTED,
+ BLE_STOPPED
+ }
+
+ private static final State[][] TRANSITION =
+ new State[State.values().length][Event.values().length];
+
+ static {
+ if (VERSION.SDK_INT < 23) {
+ // transition table before API23
+ TRANSITION[State.OFF.ordinal()][Event.USER_TURN_ON.ordinal()] = State.TURNING_ON;
+ TRANSITION[State.TURNING_ON.ordinal()][Event.BREDR_STARTED.ordinal()] = State.ON;
+ TRANSITION[State.ON.ordinal()][Event.USER_TURN_OFF.ordinal()] = State.TURNING_OFF;
+ TRANSITION[State.TURNING_OFF.ordinal()][Event.BREDR_STOPPED.ordinal()] = State.OFF;
+ } else {
+ // transition table starting from API23
+ TRANSITION[State.OFF.ordinal()][Event.BLE_TURN_ON.ordinal()] = State.BLE_TURNING_ON;
+ TRANSITION[State.BLE_TURNING_ON.ordinal()][Event.BLE_STARTED.ordinal()] = State.BLE_ON;
+ TRANSITION[State.BLE_ON.ordinal()][Event.USER_TURN_ON.ordinal()] = State.TURNING_ON;
+ TRANSITION[State.TURNING_ON.ordinal()][Event.BREDR_STARTED.ordinal()] = State.ON;
+ TRANSITION[State.ON.ordinal()][Event.BLE_TURN_OFF.ordinal()] = State.TURNING_OFF;
+ TRANSITION[State.TURNING_OFF.ordinal()][Event.BREDR_STOPPED.ordinal()] = State.BLE_ON;
+ TRANSITION[State.BLE_ON.ordinal()][Event.USER_TURN_OFF.ordinal()] =
+ State.BLE_TURNING_OFF;
+ TRANSITION[State.BLE_TURNING_OFF.ordinal()][Event.BLE_STOPPED.ordinal()] = State.OFF;
+ }
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/BlueletImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/BlueletImpl.java
new file mode 100644
index 0000000..4e534e3
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/BlueletImpl.java
@@ -0,0 +1,495 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.bluetooth;
+
+import static org.robolectric.util.ReflectionHelpers.callConstructor;
+
+import android.Manifest.permission;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.IBluetoothManager;
+import android.content.AttributionSource;
+import android.content.Intent;
+import android.os.Build.VERSION;
+import android.os.ParcelUuid;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.libraries.testing.deviceshadower.Bluelet;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.AdapterDelegate.Event;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.AdapterDelegate.State;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.RfcommDelegate;
+import com.android.libraries.testing.deviceshadower.internal.common.BroadcastManager;
+import com.android.libraries.testing.deviceshadower.internal.common.Interrupter;
+import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A container class of a real-world Bluetooth device.
+ */
+public class BlueletImpl implements Bluelet {
+
+ enum PairingConfirmation {
+ UNKNOWN,
+ CONFIRMED,
+ DENIED
+ }
+
+ /**
+ * See hidden {@link #EXTRA_REASON} and reason values in {@link BluetoothDevice}.
+ */
+ static final int REASON_SUCCESS = 0;
+ /**
+ * See hidden {@link #EXTRA_REASON} and reason values in {@link BluetoothDevice}.
+ */
+ static final int UNBOND_REASON_AUTH_FAILED = 1;
+ /**
+ * See hidden {@link #EXTRA_REASON} and reason values in {@link BluetoothDevice}.
+ */
+ static final int UNBOND_REASON_AUTH_CANCELED = 3;
+
+ /**
+ * Hidden in {@link BluetoothDevice}.
+ */
+ private static final String EXTRA_REASON = "android.bluetooth.device.extra.REASON";
+
+ private static final Logger LOGGER = Logger.create("BlueletImpl");
+
+ private static final ImmutableMap<Integer, Integer> PROFILE_STATE_TO_ADAPTER_STATE =
+ ImmutableMap.<Integer, Integer>builder()
+ .put(BluetoothProfile.STATE_CONNECTED, BluetoothAdapter.STATE_CONNECTED)
+ .put(BluetoothProfile.STATE_CONNECTING, BluetoothAdapter.STATE_CONNECTING)
+ .put(BluetoothProfile.STATE_DISCONNECTING, BluetoothAdapter.STATE_DISCONNECTING)
+ .put(BluetoothProfile.STATE_DISCONNECTED, BluetoothAdapter.STATE_DISCONNECTED)
+ .build();
+
+ public static void reset() {
+ RfcommDelegate.reset();
+ }
+
+ public final String address;
+ String mName;
+ ParcelUuid[] mProfileUuids = new ParcelUuid[0];
+ int mPhonebookAccessPermission;
+ int mMessageAccessPermission;
+ int mSimAccessPermission;
+ final BluetoothAdapter mAdapter;
+ int mPassKey;
+
+ private CreateBondOutcome mCreateBondOutcome = CreateBondOutcome.SUCCESS;
+ private int mCreateBondFailureReason;
+ private IoCapabilities mIoCapabilities = IoCapabilities.NO_INPUT_NO_OUTPUT;
+ private boolean mRefuseConnections;
+ private FetchUuidsTiming mFetchUuidsTiming = FetchUuidsTiming.AFTER_BONDING;
+ private boolean mEnableCVE20192225;
+
+ private final Interrupter mInterrupter;
+ private final AdapterDelegate mAdapterDelegate;
+ private final RfcommDelegate mRfcommDelegate;
+ private final GattDelegate mGattDelegate;
+ private final BluetoothBroadcastHandler mBluetoothBroadcastHandler;
+ private final Map<String, Integer> mRemoteAddressToBondState = new HashMap<>();
+ private final Map<String, PairingConfirmation> mRemoteAddressToPairingConfirmation =
+ new HashMap<>();
+ private final Map<Integer, Integer> mProfileTypeToConnectionState = new HashMap<>();
+ private final Set<BluetoothDevice> mBondedDevices = new HashSet<>();
+
+ public BlueletImpl(String address, BroadcastManager broadcastManager) {
+ this.address = address;
+ this.mName = address;
+ this.mAdapter = callConstructor(BluetoothAdapter.class,
+ ClassParameter.from(IBluetoothManager.class, new IBluetoothManagerImpl()),
+ ClassParameter.from(AttributionSource.class,
+ AttributionSource.myAttributionSource()));
+ mBluetoothBroadcastHandler = new BluetoothBroadcastHandler(broadcastManager);
+ mInterrupter = new Interrupter();
+ mAdapterDelegate = new AdapterDelegate(address, mBluetoothBroadcastHandler);
+ mRfcommDelegate = new RfcommDelegate(address, mBluetoothBroadcastHandler, mInterrupter);
+ mGattDelegate = new GattDelegate(address);
+ }
+
+ @Override
+ public Bluelet setAdapterInitialState(int state) throws IllegalArgumentException {
+ LOGGER.d(String.format("Address: %s, setAdapterInitialState(%d)", address, state));
+ Preconditions.checkArgument(
+ state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_ON,
+ "State must be BluetoothAdapter.STATE_ON or BluetoothAdapter.STATE_OFF.");
+ mAdapterDelegate.setState(State.lookup(state));
+ return this;
+ }
+
+ @Override
+ public Bluelet setBluetoothClass(int bluetoothClass) {
+ mAdapterDelegate.setBluetoothClass(bluetoothClass);
+ return this;
+ }
+
+ @Override
+ public Bluelet setScanMode(int scanMode) {
+ mAdapterDelegate.setScanMode(scanMode);
+ return this;
+ }
+
+ @Override
+ public Bluelet setProfileUuids(ParcelUuid... profileUuids) {
+ this.mProfileUuids = profileUuids;
+ return this;
+ }
+
+ @Override
+ public Bluelet setIoCapabilities(IoCapabilities ioCapabilities) {
+ this.mIoCapabilities = ioCapabilities;
+ return this;
+ }
+
+ @Override
+ public Bluelet setCreateBondOutcome(CreateBondOutcome outcome, int failureReason) {
+ mCreateBondOutcome = outcome;
+ mCreateBondFailureReason = failureReason;
+ return this;
+ }
+
+ @Override
+ public Bluelet setRefuseConnections(boolean refuse) {
+ mRefuseConnections = refuse;
+ return this;
+ }
+
+ @Override
+ public Bluelet setRefuseGattConnections(boolean refuse) {
+ getGattDelegate().setRefuseConnections(refuse);
+ return this;
+ }
+
+ @Override
+ public Bluelet setFetchUuidsTiming(FetchUuidsTiming fetchUuidsTiming) {
+ this.mFetchUuidsTiming = fetchUuidsTiming;
+ return this;
+ }
+
+ @Override
+ public Bluelet addBondedDevice(String address) {
+ this.mBondedDevices.add(mAdapter.getRemoteDevice(address));
+ return this;
+ }
+
+ @Override
+ public Bluelet enableCVE20192225(boolean value) {
+ this.mEnableCVE20192225 = value;
+ return this;
+ }
+
+ IoCapabilities getIoCapabilities() {
+ return mIoCapabilities;
+ }
+
+ CreateBondOutcome getCreateBondOutcome() {
+ return mCreateBondOutcome;
+ }
+
+ int getCreateBondFailureReason() {
+ return mCreateBondFailureReason;
+ }
+
+ public boolean getRefuseConnections() {
+ return mRefuseConnections;
+ }
+
+ public FetchUuidsTiming getFetchUuidsTiming() {
+ return mFetchUuidsTiming;
+ }
+
+ BluetoothDevice[] getBondedDevices() {
+ return mBondedDevices.toArray(new BluetoothDevice[0]);
+ }
+
+ public boolean getEnableCVE20192225() {
+ return mEnableCVE20192225;
+ }
+
+ public void enableAdapter() {
+ LOGGER.d(String.format("Address: %s, enableAdapter()", address));
+ // TODO(b/200231384): async enabling, configurable delay, failure path
+ if (VERSION.SDK_INT < 23) {
+ mAdapterDelegate.processEvent(Event.USER_TURN_ON);
+ mAdapterDelegate.processEvent(Event.BREDR_STARTED);
+ } else {
+ mAdapterDelegate.processEvent(Event.BLE_TURN_ON);
+ mAdapterDelegate.processEvent(Event.BLE_STARTED);
+ mAdapterDelegate.processEvent(Event.USER_TURN_ON);
+ mAdapterDelegate.processEvent(Event.BREDR_STARTED);
+ }
+ }
+
+ public void disableAdapter() {
+ LOGGER.d(String.format("Address: %s, disableAdapter()", address));
+ // TODO(b/200231384): async disabling, configurable delay, failure path
+ if (VERSION.SDK_INT < 23) {
+ mAdapterDelegate.processEvent(Event.USER_TURN_OFF);
+ mAdapterDelegate.processEvent(Event.BREDR_STOPPED);
+ } else {
+ mAdapterDelegate.processEvent(Event.BLE_TURN_OFF);
+ mAdapterDelegate.processEvent(Event.BREDR_STOPPED);
+ mAdapterDelegate.processEvent(Event.USER_TURN_OFF);
+ mAdapterDelegate.processEvent(Event.BLE_STOPPED);
+ }
+ }
+
+ public AdapterDelegate getAdapterDelegate() {
+ return mAdapterDelegate;
+ }
+
+ public RfcommDelegate getRfcommDelegate() {
+ return mRfcommDelegate;
+ }
+
+ public GattDelegate getGattDelegate() {
+ return mGattDelegate;
+ }
+
+ public BluetoothAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ public void setInterruptible(int identifier) {
+ LOGGER.d(String.format("Address: %s, setInterruptible(%d)", address, identifier));
+ mInterrupter.setInterruptible(identifier);
+ }
+
+ public void interrupt(int identifier) {
+ LOGGER.d(String.format("Address: %s, interrupt(%d)", address, identifier));
+ mInterrupter.interrupt(identifier);
+ }
+
+ @VisibleForTesting
+ public void setAdapterState(int state) throws IllegalArgumentException {
+ State s = State.lookup(state);
+ if (s == null) {
+ throw new IllegalArgumentException();
+ }
+ mAdapterDelegate.setState(s);
+ }
+
+ public int getBondState(String remoteAddress) {
+ return mRemoteAddressToBondState.containsKey(remoteAddress)
+ ? mRemoteAddressToBondState.get(remoteAddress)
+ : BluetoothDevice.BOND_NONE;
+ }
+
+ public void setBondState(String remoteAddress, int bondState, int failureReason) {
+ Intent intent =
+ newDeviceIntent(BluetoothDevice.ACTION_BOND_STATE_CHANGED, remoteAddress)
+ .putExtra(BluetoothDevice.EXTRA_BOND_STATE, bondState)
+ .putExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE,
+ getBondState(remoteAddress));
+
+ if (failureReason != REASON_SUCCESS) {
+ intent.putExtra(EXTRA_REASON, failureReason);
+ }
+
+ LOGGER.d(
+ String.format(
+ "Address: %s, Bluetooth Bond State Change Intent: remote=%s, %s -> %s "
+ + "(reason=%s)",
+ address, remoteAddress, getBondState(remoteAddress), bondState,
+ failureReason));
+ mRemoteAddressToBondState.put(remoteAddress, bondState);
+ mBluetoothBroadcastHandler.mBroadcastManager.sendBroadcast(
+ intent, android.Manifest.permission.BLUETOOTH);
+ }
+
+ public void onPairingRequest(String remoteAddress, int variant, int key) {
+ Intent intent =
+ newDeviceIntent(BluetoothDevice.ACTION_PAIRING_REQUEST, remoteAddress)
+ .putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, variant)
+ .putExtra(BluetoothDevice.EXTRA_PAIRING_KEY, key);
+
+ LOGGER.d(
+ String.format(
+ "Address: %s, Bluetooth Pairing Request Intent: remote=%s, variant=%s, "
+ + "key=%s", address, remoteAddress, variant, key));
+ mBluetoothBroadcastHandler.mBroadcastManager.sendBroadcast(intent, permission.BLUETOOTH);
+ }
+
+ public PairingConfirmation getPairingConfirmation(String remoteAddress) {
+ PairingConfirmation confirmation = mRemoteAddressToPairingConfirmation.get(remoteAddress);
+ return confirmation == null ? PairingConfirmation.UNKNOWN : confirmation;
+ }
+
+ public void setPairingConfirmation(String remoteAddress, PairingConfirmation confirmation) {
+ mRemoteAddressToPairingConfirmation.put(remoteAddress, confirmation);
+ }
+
+ public void onFetchedUuids(String remoteAddress, ParcelUuid[] profileUuids) {
+ Intent intent =
+ newDeviceIntent(BluetoothDevice.ACTION_UUID, remoteAddress)
+ .putExtra(BluetoothDevice.EXTRA_UUID, profileUuids);
+
+ LOGGER.d(
+ String.format(
+ "Address: %s, Bluetooth Found UUIDs Intent: remoteAddress=%s, uuids=%s",
+ address, remoteAddress, Arrays.toString(profileUuids)));
+ mBluetoothBroadcastHandler.mBroadcastManager.sendBroadcast(
+ intent, android.Manifest.permission.BLUETOOTH);
+ }
+
+ private static int maxProfileState(int a, int b) {
+ // Prefer connected > connecting > disconnecting > disconnected.
+ switch (a) {
+ case BluetoothProfile.STATE_CONNECTED:
+ return a;
+ case BluetoothProfile.STATE_CONNECTING:
+ return b == BluetoothProfile.STATE_CONNECTED ? b : a;
+ case BluetoothProfile.STATE_DISCONNECTING:
+ return b == BluetoothProfile.STATE_CONNECTED
+ || b == BluetoothProfile.STATE_CONNECTING
+ ? b
+ : a;
+ case BluetoothProfile.STATE_DISCONNECTED:
+ default:
+ return b;
+ }
+ }
+
+ public int getAdapterConnectionState() {
+ int maxState = BluetoothProfile.STATE_DISCONNECTED;
+ for (int state : mProfileTypeToConnectionState.values()) {
+ maxState = maxProfileState(maxState, state);
+ }
+ return PROFILE_STATE_TO_ADAPTER_STATE.get(maxState);
+ }
+
+ public int getProfileConnectionState(int profileType) {
+ return mProfileTypeToConnectionState.containsKey(profileType)
+ ? mProfileTypeToConnectionState.get(profileType)
+ : BluetoothProfile.STATE_DISCONNECTED;
+ }
+
+ public void setProfileConnectionState(int profileType, int state, String remoteAddress) {
+ int previousAdapterState = getAdapterConnectionState();
+ mProfileTypeToConnectionState.put(profileType, state);
+ int adapterState = getAdapterConnectionState();
+ if (previousAdapterState != adapterState) {
+ Intent intent =
+ newDeviceIntent(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED, remoteAddress)
+ .putExtra(BluetoothAdapter.EXTRA_PREVIOUS_CONNECTION_STATE,
+ previousAdapterState)
+ .putExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, adapterState);
+
+ LOGGER.d(
+ "Adapter Connection State Changed Intent: "
+ + previousAdapterState
+ + " -> "
+ + adapterState);
+ mBluetoothBroadcastHandler.mBroadcastManager.sendBroadcast(
+ intent, android.Manifest.permission.BLUETOOTH);
+ }
+ }
+
+ static class BluetoothBroadcastHandler implements AdapterDelegate.Callback,
+ RfcommDelegate.Callback {
+
+ private final BroadcastManager mBroadcastManager;
+
+ BluetoothBroadcastHandler(BroadcastManager broadcastManager) {
+ this.mBroadcastManager = broadcastManager;
+ }
+
+ @Override
+ public void onAdapterStateChange(State prevState, State newState) {
+ int prev = prevState.getValue();
+ int cur = newState.getValue();
+ LOGGER.d("Bluetooth State Change Intent: " + State.lookup(prev) + " -> " + State.lookup(
+ cur));
+ Intent intent = new Intent(BluetoothAdapter.ACTION_STATE_CHANGED);
+ intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, prev);
+ intent.putExtra(BluetoothAdapter.EXTRA_STATE, cur);
+ mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
+ }
+
+ @Override
+ public void onBleStateChange(State prevState, State newState) {
+ int prev = prevState.getValue();
+ int cur = newState.getValue();
+ LOGGER.d("BLE State Change Intent: " + State.lookup(prev) + " -> " + State.lookup(cur));
+ Intent intent = new Intent(BluetoothConstants.ACTION_BLE_STATE_CHANGED);
+ intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, prev);
+ intent.putExtra(BluetoothAdapter.EXTRA_STATE, cur);
+ mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
+ }
+
+ @Override
+ public void onConnectionStateChange(String remoteAddress, boolean isConnected) {
+ LOGGER.d("Bluetooth Connection State Change Intent, isConnected: " + isConnected);
+ Intent intent =
+ isConnected
+ ? newDeviceIntent(BluetoothDevice.ACTION_ACL_CONNECTED, remoteAddress)
+ : newDeviceIntent(BluetoothDevice.ACTION_ACL_DISCONNECTED,
+ remoteAddress);
+ mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
+ }
+
+ @Override
+ public void onDiscoveryStarted() {
+ LOGGER.d("Bluetooth discovery started.");
+ Intent intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
+ mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
+ }
+
+ @Override
+ public void onDiscoveryFinished() {
+ LOGGER.d("Bluetooth discovery finished.");
+ Intent intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
+ mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
+ }
+
+ @Override
+ public void onDeviceFound(String address, int bluetoothClass, String name) {
+ LOGGER.d("Bluetooth device found, address: " + address);
+ Intent intent =
+ newDeviceIntent(BluetoothDevice.ACTION_FOUND, address)
+ .putExtra(
+ BluetoothDevice.EXTRA_CLASS,
+ callConstructor(
+ BluetoothClass.class,
+ ClassParameter.from(int.class, bluetoothClass)))
+ .putExtra(BluetoothDevice.EXTRA_NAME, name);
+ // TODO(b/200231384): support rssi
+ // TODO(b/200231384): send broadcast with additional ACCESS_COARSE_LOCATION permission
+ // once broadcast permission is implemented.
+ mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
+ }
+ }
+
+ private static Intent newDeviceIntent(String action, String address) {
+ return new Intent(action)
+ .putExtra(
+ BluetoothDevice.EXTRA_DEVICE,
+ BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address));
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/BluetoothConstants.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/BluetoothConstants.java
new file mode 100644
index 0000000..fa519da
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/BluetoothConstants.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.bluetooth;
+
+/**
+ * A class to hold Bluetooth constants.
+ */
+public class BluetoothConstants {
+
+ /*** Bluetooth Adapter State ***/
+ // Must be identical to BluetoothAdapter hidden field STATE_BLE_TURNING_ON
+ public static final int STATE_BLE_TURNING_ON = 14;
+
+ // Must be identical to BluetoothAdapter hidden field STATE_BLE_ON
+ public static final int STATE_BLE_ON = 15;
+
+ // Must be identical to BluetoothAdapter hidden field STATE_BLE_TURNING_OFF
+ public static final int STATE_BLE_TURNING_OFF = 16;
+
+ // Must be identical to BluetoothAdapter hidden field ACTION_BLE_STATE_CHANGED
+ public static final String ACTION_BLE_STATE_CHANGED =
+ "android.bluetooth.adapter.action.BLE_STATE_CHANGED";
+
+ /*** Rfcomm Socket ***/
+ // Must be identical to BluetoothSocket field TYPE_RFCOMM.
+ // The field was package-private before M.
+ public static final int TYPE_RFCOMM = 1;
+
+ public static final int SOCKET_CLOSE = -10000;
+
+ // Android Bluetooth use -1 as port when creating server socket with uuid
+ public static final int SERVER_SOCKET_CHANNEL_AUTO_ASSIGN = -1;
+
+ // Android Bluetooth use -1 as port when creating socket with a uuid
+ public static final int SOCKET_CHANNEL_CONNECT_WITH_UUID = -1;
+
+ /*** BLE Advertise/Scan ***/
+ // Must be identical to AdvertiseCallback hidden field ADVERTISE_SUCCESS.
+ public static final int ADVERTISE_SUCCESS = 0;
+
+ // Must be identical to ScanRecord field DATA_TYPE_FLAGS.
+ public static final int DATA_TYPE_FLAGS = 0x01;
+
+ // Must be identical to ScanRecord field DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE.
+ public static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE = 0x07;
+
+ // Must be identical to ScanRecord field DATA_TYPE_LOCAL_NAME_COMPLETE.
+ public static final int DATA_TYPE_LOCAL_NAME_COMPLETE = 0x09;
+
+ // Must be identical to ScanRecord field DATA_TYPE_TX_POWER_LEVEL.
+ public static final int DATA_TYPE_TX_POWER_LEVEL = 0x0A;
+
+ // Must be identical to ScanRecord field DATA_TYPE_SERVICE_DATA.
+ public static final int DATA_TYPE_SERVICE_DATA = 0x16;
+
+ // Must be identical to ScanRecord field DATA_TYPE_MANUFACTURER_SPECIFIC_DATA.
+ public static final int DATA_TYPE_MANUFACTURER_SPECIFIC_DATA = 0xFF;
+
+ /**
+ * @see #DATA_TYPE_FLAGS
+ */
+ public interface Flags {
+
+ byte LE_LIMITED_DISCOVERABLE_MODE = 1;
+ byte LE_GENERAL_DISCOVERABLE_MODE = 1 << 1;
+ byte BR_EDR_NOT_SUPPORTED = 1 << 2;
+ byte SIMULTANEOUS_LE_AND_BR_EDR_CONTROLLER = 1 << 3;
+ byte SIMULTANEOUS_LE_AND_BR_EDR_HOST = 1 << 4;
+ }
+
+ /**
+ * Observed that Android sets this for {@link #DATA_TYPE_FLAGS} when a packet is connectable (on
+ * a Nexus 6P running 7.1.2).
+ */
+ public static final byte FLAGS_IN_CONNECTABLE_PACKETS =
+ Flags.BR_EDR_NOT_SUPPORTED
+ | Flags.LE_GENERAL_DISCOVERABLE_MODE
+ | Flags.SIMULTANEOUS_LE_AND_BR_EDR_CONTROLLER
+ | Flags.SIMULTANEOUS_LE_AND_BR_EDR_HOST;
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/GattDelegate.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/GattDelegate.java
new file mode 100644
index 0000000..4618561
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/GattDelegate.java
@@ -0,0 +1,609 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.bluetooth;
+
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.IBluetoothGattCallback;
+import android.bluetooth.IBluetoothGattServerCallback;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.ParcelUuid;
+import android.os.SystemClock;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+import com.android.libraries.testing.deviceshadower.internal.DeviceletImpl;
+import com.android.libraries.testing.deviceshadower.internal.utils.GattHelper;
+import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
+
+import com.google.common.base.Preconditions;
+import com.google.common.primitives.Bytes;
+
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Delegate to operate gatt operations.
+ */
+public class GattDelegate {
+
+ private static final int DEFAULT_RSSI = -50;
+ private static final Logger LOGGER = Logger.create("GattDelegate");
+
+ // chipset properties
+ // use 2 as API 21 requires multi-advertisement support to use Le Advertising.
+ private final int mMaxAdvertiseInstances = 2;
+ private final AtomicBoolean mIsOffloadedFilteringSupported = new AtomicBoolean(false);
+ private final String mAddress;
+ private final AtomicInteger mCurrentClientIf = new AtomicInteger(0);
+ private final AtomicInteger mCurrentServerIf = new AtomicInteger(0);
+ private final AtomicBoolean mCurrentConnectionState = new AtomicBoolean(false);
+ private final Map<ParcelUuid, Service> mServices = new HashMap<>();
+ private final Map<Integer, IBluetoothGattCallback> mClientCallbacks;
+ private final Map<Integer, IBluetoothGattServerCallback> mServerCallbacks;
+ private final Map<Integer, Advertiser> mAdvertisers;
+ private final Map<Integer, Scanner> mScanners;
+ @Nullable
+ private Request mLastRequest;
+ private boolean mConnectable = true;
+
+ /**
+ * The parameters of a request, e.g. readCharacteristic(). Subclass for each request.
+ *
+ * @see #getLastRequest()
+ */
+ abstract static class Request {
+
+ final int mSrvcType;
+ final int mSrvcInstId;
+ final ParcelUuid mSrvcId;
+ final int mCharInstId;
+ final ParcelUuid mCharId;
+
+ Request(int srvcType, int srvcInstId, ParcelUuid srvcId, int charInstId,
+ ParcelUuid charId) {
+ this.mSrvcType = srvcType;
+ this.mSrvcInstId = srvcInstId;
+ this.mSrvcId = srvcId;
+ this.mCharInstId = charInstId;
+ this.mCharId = charId;
+ }
+ }
+
+ /**
+ * Corresponds to {@link android.bluetooth.IBluetoothGatt#readCharacteristic}.
+ */
+ static class ReadCharacteristicRequest extends Request {
+
+ ReadCharacteristicRequest(
+ int srvcType, int srvcInstId, ParcelUuid srvcId, int charInstId,
+ ParcelUuid charId) {
+ super(srvcType, srvcInstId, srvcId, charInstId, charId);
+ }
+ }
+
+ /**
+ * Corresponds to {@link android.bluetooth.IBluetoothGatt#readDescriptor}.
+ */
+ static class ReadDescriptorRequest extends Request {
+
+ final int mDescrInstId;
+ final ParcelUuid mDescrId;
+
+ ReadDescriptorRequest(
+ int srvcType,
+ int srvcInstId,
+ ParcelUuid srvcId,
+ int charInstId,
+ ParcelUuid charId,
+ int descrInstId,
+ ParcelUuid descrId) {
+ super(srvcType, srvcInstId, srvcId, charInstId, charId);
+ this.mDescrInstId = descrInstId;
+ this.mDescrId = descrId;
+ }
+ }
+
+ GattDelegate(String address) {
+ this(
+ address,
+ new HashMap<>(),
+ new HashMap<>(),
+ new ConcurrentHashMap<>(),
+ new ConcurrentHashMap<>());
+ }
+
+ @VisibleForTesting
+ GattDelegate(
+ String address,
+ Map<Integer, IBluetoothGattCallback> clientCallbacks,
+ Map<Integer, IBluetoothGattServerCallback> serverCallbacks,
+ Map<Integer, Advertiser> advertisers,
+ Map<Integer, Scanner> scanners) {
+ this.mAddress = address;
+ this.mClientCallbacks = clientCallbacks;
+ this.mServerCallbacks = serverCallbacks;
+ this.mAdvertisers = advertisers;
+ this.mScanners = scanners;
+ }
+
+ public void setRefuseConnections(boolean refuse) {
+ this.mConnectable = !refuse;
+ }
+
+ /**
+ * Used to maintain state between the request (e.g. readCharacteristic()) and sendResponse().
+ */
+ @Nullable
+ Request getLastRequest() {
+ return mLastRequest;
+ }
+
+ /**
+ * @see #getLastRequest()
+ */
+ void setLastRequest(@Nullable Request params) {
+ mLastRequest = params;
+ }
+
+ public int getClientIf() {
+ // TODO(b/200231384): support multiple client if.
+ return mCurrentClientIf.get();
+ }
+
+ public int getServerIf() {
+ // TODO(b/200231384): support multiple server if.
+ return mCurrentServerIf.get();
+ }
+
+ public IBluetoothGattServerCallback getServerCallback(int serverIf) {
+ return mServerCallbacks.get(serverIf);
+ }
+
+ public IBluetoothGattCallback getClientCallback(int clientIf) {
+ return mClientCallbacks.get(clientIf);
+ }
+
+ public int registerServer(IBluetoothGattServerCallback callback) {
+ mServerCallbacks.put(mCurrentServerIf.incrementAndGet(), callback);
+ return getServerIf();
+ }
+
+ public int registerClient(IBluetoothGattCallback callback) {
+ mClientCallbacks.put(mCurrentClientIf.incrementAndGet(), callback);
+ LOGGER.d(String.format("Client registered on %s, clientIf: %d", mAddress, getClientIf()));
+ return getClientIf();
+ }
+
+ public void unregisterClient(int clientIf) {
+ mClientCallbacks.remove(clientIf);
+ LOGGER.d(String.format("Client unregistered on %s, clientIf: %d", mAddress, clientIf));
+ }
+
+ public void unregisterServer(int serverIf) {
+ mServerCallbacks.remove(serverIf);
+ }
+
+ public int getMaxAdvertiseInstances() {
+ return mMaxAdvertiseInstances;
+ }
+
+ public boolean isOffloadedFilteringSupported() {
+ return mIsOffloadedFilteringSupported.get();
+ }
+
+ public boolean connect(String address) {
+ return mConnectable;
+ }
+
+ public boolean disconnect(String address) {
+ return true;
+ }
+
+ public void clientConnectionStateChange(
+ int state, int clientIf, boolean connected, String address) {
+ if (connected != mCurrentConnectionState.get()) {
+ mCurrentConnectionState.set(connected);
+ IBluetoothGattCallback callback = getClientCallback(clientIf);
+ if (callback != null) {
+ callback.onClientConnectionState(state, clientIf, connected, address);
+ }
+ }
+ }
+
+ public void serverConnectionStateChange(
+ int state, int serverIf, boolean connected, String address) {
+ if (connected != mCurrentConnectionState.get()) {
+ mCurrentConnectionState.set(connected);
+ IBluetoothGattServerCallback callback = getServerCallback(serverIf);
+ if (callback != null) {
+ callback.onServerConnectionState(state, serverIf, connected, address);
+ }
+ }
+ }
+
+ public Service addService(ParcelUuid uuid) {
+ Service srvc = new Service(uuid);
+ mServices.put(uuid, srvc);
+ return srvc;
+ }
+
+ public Collection<Service> getServices() {
+ return mServices.values();
+ }
+
+ public Service getService(ParcelUuid uuid) {
+ return mServices.get(uuid);
+ }
+
+ public void clientSetMtu(int clientIf, int mtu, String serverAddress) {
+ IBluetoothGattCallback callback = getClientCallback(clientIf);
+ if (callback != null && Build.VERSION.SDK_INT >= 21) {
+ callback.onConfigureMTU(serverAddress, mtu, BluetoothGatt.GATT_SUCCESS);
+ }
+ }
+
+ public void serverSetMtu(int serverIf, int mtu, String clientAddress) {
+ IBluetoothGattServerCallback callback = getServerCallback(serverIf);
+ if (callback != null && Build.VERSION.SDK_INT >= 22) {
+ callback.onMtuChanged(clientAddress, mtu);
+ }
+ }
+
+ public void startMultiAdvertising(
+ int appIf,
+ AdvertiseData advertiseData,
+ AdvertiseData scanResponse,
+ final AdvertiseSettings settings) {
+ LOGGER.d(String.format("startMultiAdvertising(%d) on %s", appIf, mAddress));
+ final Advertiser advertiser =
+ new Advertiser(
+ appIf,
+ mAddress,
+ DeviceShadowEnvironmentImpl.getLocalBlueletImpl().mName,
+ txPowerFromFlag(settings.getTxPowerLevel()),
+ advertiseData,
+ scanResponse,
+ settings);
+ mAdvertisers.put(appIf, advertiser);
+ final IBluetoothGattCallback callback = mClientCallbacks.get(appIf);
+ @SuppressWarnings("unused") // go/futurereturn-lsc
+ Future<?> possiblyIgnoredError =
+ DeviceShadowEnvironmentImpl.run(
+ mAddress,
+ () -> {
+ callback.onMultiAdvertiseCallback(
+ BluetoothConstants.ADVERTISE_SUCCESS, true /* isStart */,
+ settings);
+ return null;
+ });
+ }
+
+ /**
+ * Returns TxPower in dBm as measured at the source.
+ *
+ * <p>Note that this will vary by device and the values are only roughly accurate. The
+ * measurements were taken with a Nexus 6. Copied from the TxEddystone-UID app:
+ * {https://github.com/google/eddystone/blob/master/eddystone-uid/tools/txeddystone-uid/TxEddystone-UID/app/src/main/java/com/google/sample/txeddystone_uid/MainActivity.java}
+ */
+ private static byte txPowerFromFlag(int txPowerFlag) {
+ switch (txPowerFlag) {
+ case AdvertiseSettings.ADVERTISE_TX_POWER_HIGH:
+ return (byte) -16;
+ case AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM:
+ return (byte) -26;
+ case AdvertiseSettings.ADVERTISE_TX_POWER_LOW:
+ return (byte) -35;
+ case AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW:
+ return (byte) -59;
+ default:
+ throw new IllegalStateException("Unknown TxPower level=" + txPowerFlag);
+ }
+ }
+
+ public void stopMultiAdvertising(int appIf) {
+ LOGGER.d(String.format("stopAdvertising(%d) on %s", appIf, mAddress));
+ Advertiser advertiser = mAdvertisers.get(appIf);
+ if (advertiser == null) {
+ LOGGER.d(String.format("Advertising already stopped on %s, clientIf: %d", mAddress,
+ appIf));
+ return;
+ }
+ mAdvertisers.remove(appIf);
+ final IBluetoothGattCallback callback = mClientCallbacks.get(appIf);
+ @SuppressWarnings("unused") // go/futurereturn-lsc
+ Future<?> possiblyIgnoredError =
+ DeviceShadowEnvironmentImpl.run(
+ mAddress,
+ () -> {
+ callback.onMultiAdvertiseCallback(
+ BluetoothConstants.ADVERTISE_SUCCESS, false /* isStart */,
+ null /* setting */);
+ return null;
+ });
+ }
+
+ public void startScan(final int appIf, ScanSettings settings, List<ScanFilter> filters) {
+ LOGGER.d(String.format("startScan(%d) on %s", appIf, mAddress));
+ if (filters == null) {
+ filters = new ArrayList<>();
+ }
+ final Scanner scanner = new Scanner(appIf, settings, filters);
+ mScanners.put(appIf, scanner);
+ @SuppressWarnings("unused") // go/futurereturn-lsc
+ Future<?> possiblyIgnoredError =
+ DeviceShadowEnvironmentImpl.run(
+ mAddress,
+ () -> {
+ try {
+ scan(scanner);
+ } catch (InterruptedException e) {
+ LOGGER.e(
+ String.format("Failed to scan on %s, clientIf: %d.",
+ mAddress, scanner.mClientIf),
+ e);
+ }
+ return null;
+ });
+ }
+
+ // TODO(b/200231384): support periodic scan with interval and scan window.
+ private void scan(Scanner scanner) throws InterruptedException {
+ // fetch existing advertisements
+ List<DeviceletImpl> devicelets = DeviceShadowEnvironmentImpl.getDeviceletImpls();
+ for (DeviceletImpl devicelet : devicelets) {
+ BlueletImpl bluelet = devicelet.blueletImpl();
+ if (bluelet.address.equals(mAddress)) {
+ continue;
+ }
+ for (Advertiser advertiser : bluelet.getGattDelegate().mAdvertisers.values()) {
+ if (VERSION.SDK_INT < 21) {
+ throw new UnsupportedOperationException(
+ String.format("API %d is not supported.", VERSION.SDK_INT));
+ }
+
+ byte[] advertiseData =
+ GattHelper.convertAdvertiseData(
+ advertiser.mAdvertiseData,
+ advertiser.mTxPowerLevel,
+ advertiser.mName,
+ advertiser.mSettings.isConnectable());
+ byte[] scanResponse =
+ GattHelper.convertAdvertiseData(
+ advertiser.mScanResponse,
+ advertiser.mTxPowerLevel,
+ advertiser.mName,
+ advertiser.mSettings.isConnectable());
+
+ ScanRecord scanRecord =
+ ReflectionHelpers.callStaticMethod(
+ ScanRecord.class,
+ "parseFromBytes",
+ ClassParameter.from(byte[].class,
+ Bytes.concat(advertiseData, scanResponse)));
+ ScanResult scanResult =
+ new ScanResult(
+ BluetoothAdapter.getDefaultAdapter()
+ .getRemoteDevice(advertiser.mAddress),
+ scanRecord,
+ DEFAULT_RSSI,
+ SystemClock.elapsedRealtimeNanos());
+
+ if (!matchFilters(scanResult, scanner.mFilters)) {
+ continue;
+ }
+
+ IBluetoothGattCallback callback = mClientCallbacks.get(scanner.mClientIf);
+ if (callback == null) {
+ LOGGER.e(
+ String.format("Callback is null on %s, clientIf: %d", mAddress,
+ scanner.mClientIf));
+ return;
+ }
+ callback.onScanResult(scanResult);
+ }
+ }
+ }
+
+ private boolean matchFilters(ScanResult scanResult, List<ScanFilter> filters) {
+ for (ScanFilter filter : filters) {
+ if (!filter.matches(scanResult)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public void stopScan(int appIf) {
+ LOGGER.d(String.format("stopScan(%d) on %s", appIf, mAddress));
+ Scanner scanner = mScanners.get(appIf);
+ if (scanner == null) {
+ LOGGER.d(
+ String.format("Scanning already stopped on %s, clientIf: %d", mAddress, appIf));
+ return;
+ }
+ mScanners.remove(appIf);
+ }
+
+ static class Service {
+
+ private Map<ParcelUuid, Characteristic> mCharacteristics = new HashMap<>();
+ private ParcelUuid mUuid;
+
+ Service(ParcelUuid uuid) {
+ this.mUuid = uuid;
+ }
+
+ Characteristic getCharacteristic(ParcelUuid uuid) {
+ return mCharacteristics.get(uuid);
+ }
+
+ Characteristic addCharacteristic(ParcelUuid uuid, int properties, int permissions) {
+ Characteristic ch = new Characteristic(uuid, properties, permissions);
+ mCharacteristics.put(uuid, ch);
+ return ch;
+ }
+
+ Collection<Characteristic> getCharacteristics() {
+ return mCharacteristics.values();
+ }
+
+ ParcelUuid getUuid() {
+ return this.mUuid;
+ }
+ }
+
+ static class Characteristic {
+
+ private int mProperties;
+ private ParcelUuid mUuid;
+ private Map<ParcelUuid, Descriptor> mDescriptors = new HashMap<>();
+ private Set<String> mNotifyClients = new HashSet<>();
+ private byte[] mValue;
+
+ Characteristic(ParcelUuid uuid, int properties, int permissions) {
+ this.mProperties = properties;
+ this.mUuid = uuid;
+ }
+
+ Descriptor getDescriptor(ParcelUuid uuid) {
+ return mDescriptors.get(uuid);
+ }
+
+ Descriptor addDescriptor(ParcelUuid uuid, int permissions) {
+ Descriptor desc = new Descriptor(uuid, permissions);
+ mDescriptors.put(uuid, desc);
+ return desc;
+ }
+
+ Collection<Descriptor> getDescriptors() {
+ return mDescriptors.values();
+ }
+
+ void setValue(byte[] value) {
+ this.mValue = value;
+ }
+
+ byte[] getValue() {
+ return mValue;
+ }
+
+ ParcelUuid getUuid() {
+ return mUuid;
+ }
+
+ int getProperties() {
+ return mProperties;
+ }
+
+ void registerNotification(String client, int clientIf) {
+ mNotifyClients.add(client);
+ }
+
+ Set<String> getNotifyClients() {
+ return mNotifyClients;
+ }
+ }
+
+ static class Descriptor {
+
+ int mPermissions;
+ ParcelUuid mUuid;
+ byte[] mValue;
+
+ Descriptor(ParcelUuid uuid, int permissions) {
+ this.mUuid = uuid;
+ this.mPermissions = permissions;
+ }
+
+ void setValue(byte[] value) {
+ this.mValue = value;
+ }
+
+ byte[] getValue() {
+ return mValue;
+ }
+
+ ParcelUuid getUuid() {
+ return mUuid;
+ }
+ }
+
+ @VisibleForTesting
+ static class Advertiser {
+
+ final int mClientIf;
+ final String mAddress;
+ final String mName;
+ final int mTxPowerLevel;
+ final AdvertiseData mAdvertiseData;
+ @Nullable
+ final AdvertiseData mScanResponse;
+ final AdvertiseSettings mSettings;
+
+ Advertiser(
+ int clientIf,
+ String address,
+ String name,
+ int txPowerLevel,
+ AdvertiseData advertiseData,
+ AdvertiseData scanResponse,
+ AdvertiseSettings settings) {
+ this.mClientIf = clientIf;
+ this.mAddress = Preconditions.checkNotNull(address);
+ this.mName = name;
+ this.mTxPowerLevel = txPowerLevel;
+ this.mAdvertiseData = Preconditions.checkNotNull(advertiseData);
+ this.mScanResponse = scanResponse;
+ this.mSettings = Preconditions.checkNotNull(settings);
+ }
+ }
+
+ @VisibleForTesting
+ static class Scanner {
+
+ final int mClientIf;
+ final ScanSettings mSettings;
+ final List<ScanFilter> mFilters;
+
+ Scanner(int clientIf, ScanSettings settings, List<ScanFilter> filters) {
+ this.mClientIf = clientIf;
+ this.mSettings = Preconditions.checkNotNull(settings);
+ this.mFilters = Preconditions.checkNotNull(filters);
+ }
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothGattImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothGattImpl.java
new file mode 100644
index 0000000..0ac287d
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothGattImpl.java
@@ -0,0 +1,707 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.bluetooth;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.IBluetoothGatt;
+import android.bluetooth.IBluetoothGattCallback;
+import android.bluetooth.IBluetoothGattServerCallback;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanSettings;
+import android.os.ParcelUuid;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.GattDelegate.ReadCharacteristicRequest;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.GattDelegate.ReadDescriptorRequest;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.GattDelegate.Request;
+import com.android.libraries.testing.deviceshadower.internal.common.NamedRunnable;
+import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implementation of IBluetoothGatt.
+ */
+public class IBluetoothGattImpl implements IBluetoothGatt {
+
+ private static final Logger LOGGER = Logger.create("IBluetoothGattImpl");
+ private GattDelegate.Service mCurrentService;
+ private GattDelegate.Characteristic mCurrentCharacteristic;
+
+ @Override
+ public void startScan(
+ int appIf,
+ boolean isServer,
+ ScanSettings settings,
+ List<ScanFilter> filters,
+ List<?> scanStorages,
+ String callingPackage) {
+ localGattDelegate().startScan(appIf, settings, filters);
+ }
+
+ @Override
+ public void startScan(
+ int appIf,
+ boolean isServer,
+ ScanSettings settings,
+ List<ScanFilter> filters,
+ List<?> scanStorages) {
+ startScan(appIf, isServer, settings, filters, scanStorages, "" /* callingPackage */);
+ }
+
+ @Override
+ public void stopScan(int appIf, boolean isServer) {
+ localGattDelegate().stopScan(appIf);
+ }
+
+ @Override
+ public void startMultiAdvertising(
+ int appIf,
+ AdvertiseData advertiseData,
+ AdvertiseData scanResponse,
+ AdvertiseSettings settings) {
+ localGattDelegate().startMultiAdvertising(appIf, advertiseData, scanResponse, settings);
+ }
+
+ @Override
+ public void stopMultiAdvertising(int appIf) {
+ localGattDelegate().stopMultiAdvertising(appIf);
+ }
+
+ @Override
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void registerClient(ParcelUuid appId, final IBluetoothGattCallback callback) {
+ final int clientIf = localGattDelegate().registerClient(callback);
+ NamedRunnable onClientRegistered =
+ NamedRunnable.create(
+ "ClientGatt.onClientRegistered=" + clientIf,
+ () -> {
+ callback.onClientRegistered(BluetoothGatt.GATT_SUCCESS, clientIf);
+ });
+
+ DeviceShadowEnvironmentImpl.runOnService(localAddress(), onClientRegistered);
+ }
+
+ @Override
+ public void unregisterClient(int clientIf) {
+ localGattDelegate().unregisterClient(clientIf);
+ }
+
+ @Override
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void clientConnect(
+ final int clientIf, final String serverAddress, boolean isDirect, int transport) {
+ // TODO(b/200231384): implement auto connect.
+ String clientAddress = localAddress();
+ int serverIf = remoteGattDelegate(serverAddress).getServerIf();
+ boolean success = remoteGattDelegate(serverAddress).connect(clientAddress);
+ if (!success) {
+ LOGGER.i(String.format("clientConnect failed: %s connect %s", serverAddress,
+ clientAddress));
+ return;
+ }
+
+ DeviceShadowEnvironmentImpl.runOnService(
+ clientAddress,
+ newClientConnectionStateChangeRunnable(clientIf, true, serverAddress));
+
+ DeviceShadowEnvironmentImpl.runOnService(
+ serverAddress,
+ newServerConnectionStateChangeRunnable(serverIf, true, clientAddress));
+ }
+
+ @Override
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void clientDisconnect(final int clientIf, final String serverAddress) {
+ final String clientAddress = localAddress();
+ remoteGattDelegate(serverAddress).disconnect(clientAddress);
+ int serverIf = remoteGattDelegate(serverAddress).getServerIf();
+
+
+ DeviceShadowEnvironmentImpl.runOnService(
+ clientAddress,
+ newClientConnectionStateChangeRunnable(clientIf, false, serverAddress));
+
+ DeviceShadowEnvironmentImpl.runOnService(
+ serverAddress,
+ newServerConnectionStateChangeRunnable(serverIf, false, clientAddress));
+ }
+
+ @Override
+ public void discoverServices(int clientIf, String serverAddress) {
+ final IBluetoothGattCallback callback = localGattDelegate().getClientCallback(clientIf);
+ if (callback == null) {
+ return;
+ }
+ for (GattDelegate.Service service : remoteGattDelegate(serverAddress).getServices()) {
+ callback.onGetService(serverAddress, 0 /*srvcType*/, 0 /*srvcInstId*/,
+ service.getUuid());
+
+ for (GattDelegate.Characteristic characteristic : service.getCharacteristics()) {
+ callback.onGetCharacteristic(
+ serverAddress,
+ 0 /*srvcType*/,
+ 0 /*srvcInstId*/,
+ service.getUuid(),
+ 0 /*charInstId*/,
+ characteristic.getUuid(),
+ characteristic.getProperties());
+ for (GattDelegate.Descriptor descriptor : characteristic.getDescriptors()) {
+ callback.onGetDescriptor(
+ serverAddress,
+ 0 /*srvcType*/,
+ 0 /*srvcInstId*/,
+ service.getUuid(),
+ 0 /*charInstId*/,
+ characteristic.getUuid(),
+ 0 /*descrInstId*/,
+ descriptor.getUuid());
+ }
+ }
+ }
+
+ callback.onSearchComplete(serverAddress, BluetoothGatt.GATT_SUCCESS);
+ }
+
+ @Override
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void readCharacteristic(
+ final int clientIf,
+ final String serverAddress,
+ final int srvcType,
+ final int srvcInstId,
+ final ParcelUuid srvcId,
+ final int charInstId,
+ final ParcelUuid charId,
+ final int authReq) {
+ // TODO(b/200231384): implement authReq.
+ final String clientAddress = localAddress();
+ localGattDelegate()
+ .setLastRequest(
+ new ReadCharacteristicRequest(srvcType, srvcInstId, srvcId, charInstId,
+ charId));
+
+ NamedRunnable serverOnCharacteristicReadRequest =
+ NamedRunnable.create(
+ "ServerGatt.onCharacteristicReadRequest",
+ () -> {
+ int serverIf = localGattDelegate().getServerIf();
+ IBluetoothGattServerCallback callback =
+ localGattDelegate().getServerCallback(serverIf);
+ if (callback != null) {
+ callback.onCharacteristicReadRequest(
+ clientAddress,
+ 0 /*transId*/,
+ 0 /*offset*/,
+ false /*isLong*/,
+ 0 /*srvcType*/,
+ srvcInstId,
+ srvcId,
+ charInstId,
+ charId);
+ }
+ });
+
+ DeviceShadowEnvironmentImpl.runOnService(serverAddress, serverOnCharacteristicReadRequest);
+ }
+
+ @Override
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void writeCharacteristic(
+ final int clientIf,
+ final String serverAddress,
+ final int srvcType,
+ final int srvcInstId,
+ final ParcelUuid srvcId,
+ final int charInstId,
+ final ParcelUuid charId,
+ final int writeType,
+ final int authReq,
+ final byte[] value) {
+ // TODO(b/200231384): implement write with response needed.
+ remoteGattDelegate(serverAddress).getService(srvcId).getCharacteristic(charId)
+ .setValue(value);
+ final String clientAddress = localAddress();
+
+ NamedRunnable clientOnCharacteristicWrite =
+ NamedRunnable.create(
+ "ClientGatt.onCharacteristicWrite",
+ () -> {
+ IBluetoothGattCallback callback = localGattDelegate().getClientCallback(
+ clientIf);
+ if (callback != null) {
+ callback.onCharacteristicWrite(
+ serverAddress,
+ BluetoothGatt.GATT_SUCCESS,
+ 0 /*srvcType*/,
+ srvcInstId,
+ srvcId,
+ charInstId,
+ charId);
+ }
+ });
+
+ NamedRunnable onCharacteristicWriteRequest =
+ NamedRunnable.create(
+ "ServerGatt.onCharacteristicWriteRequest",
+ () -> {
+ int serverIf = localGattDelegate().getServerIf();
+ IBluetoothGattServerCallback callback =
+ localGattDelegate().getServerCallback(serverIf);
+ if (callback != null) {
+ callback.onCharacteristicWriteRequest(
+ clientAddress,
+ 0 /*transId*/,
+ 0 /*offset*/,
+ value.length,
+ false /*isPrep*/,
+ false /*needRsp*/,
+ 0 /*srvcType*/,
+ srvcInstId,
+ srvcId,
+ charInstId,
+ charId,
+ value);
+ }
+ });
+
+ DeviceShadowEnvironmentImpl.runOnService(clientAddress, clientOnCharacteristicWrite);
+
+ DeviceShadowEnvironmentImpl.runOnService(serverAddress, onCharacteristicWriteRequest);
+ }
+
+ @Override
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void readDescriptor(
+ final int clientIf,
+ final String serverAddress,
+ final int srvcType,
+ final int srvcInstId,
+ final ParcelUuid srvcId,
+ final int charInstId,
+ final ParcelUuid charId,
+ final int descrInstId,
+ final ParcelUuid descrId,
+ final int authReq) {
+ final String clientAddress = localAddress();
+ localGattDelegate()
+ .setLastRequest(
+ new ReadDescriptorRequest(
+ srvcType, srvcInstId, srvcId, charInstId, charId, descrInstId,
+ descrId));
+
+ NamedRunnable serverOnDescriptorReadRequest =
+ NamedRunnable.create(
+ "ServerGatt.onDescriptorReadRequest",
+ () -> {
+ int serverIf = localGattDelegate().getServerIf();
+ IBluetoothGattServerCallback callback =
+ localGattDelegate().getServerCallback(serverIf);
+ if (callback != null) {
+ callback.onDescriptorReadRequest(
+ clientAddress,
+ 0 /*transId*/,
+ 0 /*offset*/,
+ false /*isLong*/,
+ 0 /*srvcType*/,
+ srvcInstId,
+ srvcId,
+ charInstId,
+ charId,
+ descrId);
+ }
+ });
+
+ DeviceShadowEnvironmentImpl.runOnService(serverAddress, serverOnDescriptorReadRequest);
+ }
+
+ @Override
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void writeDescriptor(
+ final int clientIf,
+ final String serverAddress,
+ final int srvcType,
+ final int srvcInstId,
+ final ParcelUuid srvcId,
+ final int charInstId,
+ final ParcelUuid charId,
+ final int descrInstId,
+ final ParcelUuid descrId,
+ final int writeType,
+ final int authReq,
+ final byte[] value) {
+ // TODO(b/200231384): implement write with response needed.
+ remoteGattDelegate(serverAddress)
+ .getService(srvcId)
+ .getCharacteristic(charId)
+ .getDescriptor(descrId)
+ .setValue(value);
+ final String clientAddress = localAddress();
+
+ NamedRunnable serverOnDescriptorWriteRequest =
+ NamedRunnable.create(
+ "ServerGatt.onDescriptorWriteRequest",
+ () -> {
+ int serverIf = localGattDelegate().getServerIf();
+ IBluetoothGattServerCallback callback =
+ localGattDelegate().getServerCallback(serverIf);
+ if (callback != null) {
+ callback.onDescriptorWriteRequest(
+ clientAddress,
+ 0 /*transId*/,
+ 0 /*offset*/,
+ value.length,
+ false /*isPrep*/,
+ false /*needRsp*/,
+ 0 /*srvcType*/,
+ srvcInstId,
+ srvcId,
+ charInstId,
+ charId,
+ descrId,
+ value);
+ }
+ });
+
+ NamedRunnable clientOnDescriptorWrite =
+ NamedRunnable.create(
+ "ClientGatt.onDescriptorWrite",
+ () -> {
+ IBluetoothGattCallback callback = localGattDelegate().getClientCallback(
+ clientIf);
+ if (callback != null) {
+ callback.onDescriptorWrite(
+ serverAddress,
+ BluetoothGatt.GATT_SUCCESS,
+ 0 /*srvcType*/,
+ srvcInstId,
+ srvcId,
+ charInstId,
+ charId,
+ descrInstId,
+ descrId);
+ }
+ });
+
+ DeviceShadowEnvironmentImpl.runOnService(serverAddress, serverOnDescriptorWriteRequest);
+
+ DeviceShadowEnvironmentImpl.runOnService(clientAddress, clientOnDescriptorWrite);
+ }
+
+ @Override
+ public void registerForNotification(
+ int clientIf,
+ String remoteAddress,
+ int srvcType,
+ int srvcInstId,
+ ParcelUuid srvcId,
+ int charInstId,
+ ParcelUuid charId,
+ boolean enable) {
+ remoteGattDelegate(remoteAddress)
+ .getService(srvcId)
+ .getCharacteristic(charId)
+ .registerNotification(localAddress(), clientIf);
+ }
+
+ @Override
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void registerServer(ParcelUuid appId, final IBluetoothGattServerCallback callback) {
+ // TODO(b/200231384): support multiple serverIf.
+ final int serverIf = localGattDelegate().registerServer(callback);
+ NamedRunnable serverOnRegistered =
+ NamedRunnable.create(
+ "ServerGatt.onServerRegistered",
+ () -> {
+ callback.onServerRegistered(BluetoothGatt.GATT_SUCCESS, serverIf);
+ });
+
+ DeviceShadowEnvironmentImpl.runOnService(localAddress(), serverOnRegistered);
+ }
+
+ @Override
+ public void unregisterServer(int serverIf) {
+ localGattDelegate().unregisterServer(serverIf);
+ }
+
+ @Override
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void serverConnect(
+ final int serverIf, final String clientAddress, boolean isDirect, int transport) {
+ // TODO(b/200231384): implement isDirect and transport.
+ boolean success = localGattDelegate().connect(clientAddress);
+ final String serverAddress = localAddress();
+ if (!success) {
+ return;
+ }
+ int clientIf = remoteGattDelegate(clientAddress).getClientIf();
+
+ DeviceShadowEnvironmentImpl.runOnService(
+ serverAddress,
+ newServerConnectionStateChangeRunnable(serverIf, true, clientAddress));
+
+ DeviceShadowEnvironmentImpl.runOnService(
+ clientAddress,
+ newClientConnectionStateChangeRunnable(clientIf, true, serverAddress));
+ }
+
+ @Override
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void serverDisconnect(final int serverIf, final String clientAddress) {
+ localGattDelegate().disconnect(clientAddress);
+ String serverAddress = localAddress();
+ int clientIf = remoteGattDelegate(clientAddress).getClientIf();
+
+ DeviceShadowEnvironmentImpl.runOnService(
+ serverAddress,
+ newServerConnectionStateChangeRunnable(serverIf, false, clientAddress));
+
+ DeviceShadowEnvironmentImpl.runOnService(
+ clientAddress,
+ newClientConnectionStateChangeRunnable(clientIf, false, serverAddress));
+ }
+
+ @Override
+ public void beginServiceDeclaration(
+ int serverIf,
+ int srvcType,
+ int srvcInstId,
+ int minHandles,
+ ParcelUuid srvcId,
+ boolean advertisePreferred) {
+ // TODO(b/200231384): support different service type, instanceId, advertisePreferred.
+ mCurrentService = localGattDelegate().addService(srvcId);
+ }
+
+ @Override
+ public void addIncludedService(int serverIf, int srvcType, int srvcInstId, ParcelUuid srvcId) {
+ // TODO(b/200231384): implement this.
+ }
+
+ @Override
+ public void addCharacteristic(int serverIf, ParcelUuid charId, int properties,
+ int permissions) {
+ mCurrentCharacteristic = mCurrentService.addCharacteristic(charId, properties, permissions);
+ }
+
+ @Override
+ public void addDescriptor(int serverIf, ParcelUuid descId, int permissions) {
+ mCurrentCharacteristic.addDescriptor(descId, permissions);
+ }
+
+ @Override
+ public void endServiceDeclaration(int serverIf) {
+ // TODO(b/200231384): choose correct srvc type and inst id.
+ IBluetoothGattServerCallback callback = localGattDelegate().getServerCallback(serverIf);
+ if (callback != null) {
+ callback.onServiceAdded(
+ BluetoothGatt.GATT_SUCCESS, 0 /*srvcType*/, 0 /*srvcInstId*/,
+ mCurrentService.getUuid());
+ }
+ mCurrentService = null;
+ }
+
+ @Override
+ public void removeService(int serverIf, int srvcType, int srvcInstId, ParcelUuid srvcId) {
+ // TODO(b/200231384): implement remove service.
+ // localGattDelegate().removeService(srvcId);
+ }
+
+ @Override
+ public void clearServices(int serverIf) {
+ // TODO(b/200231384): support multiple serverIf.
+ // localGattDelegate().clearService();
+ }
+
+ @Override
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void sendResponse(
+ int serverIf, String clientAddress, int requestId, int status, int offset,
+ byte[] value) {
+ // TODO(b/200231384): implement more operations.
+ String serverAddress = localAddress();
+
+ DeviceShadowEnvironmentImpl.runOnService(
+ clientAddress,
+ NamedRunnable.create(
+ "ClientGatt.receiveResponse",
+ () -> {
+ IBluetoothGattCallback callback =
+ localGattDelegate().getClientCallback(
+ localGattDelegate().getClientIf());
+ if (callback != null) {
+ Request request = localGattDelegate().getLastRequest();
+ localGattDelegate().setLastRequest(null);
+ if (request != null) {
+ if (request instanceof ReadCharacteristicRequest) {
+ callback.onCharacteristicRead(
+ serverAddress,
+ status,
+ request.mSrvcType,
+ request.mSrvcInstId,
+ request.mSrvcId,
+ request.mCharInstId,
+ request.mCharId,
+ value);
+ } else if (request instanceof ReadDescriptorRequest) {
+ ReadDescriptorRequest readDescriptorRequest =
+ (ReadDescriptorRequest) request;
+ callback.onDescriptorRead(
+ serverAddress,
+ status,
+ readDescriptorRequest.mSrvcType,
+ readDescriptorRequest.mSrvcInstId,
+ readDescriptorRequest.mSrvcId,
+ readDescriptorRequest.mCharInstId,
+ readDescriptorRequest.mCharId,
+ readDescriptorRequest.mDescrInstId,
+ readDescriptorRequest.mDescrId,
+ value);
+ }
+ }
+ }
+ }));
+ }
+
+ @Override
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void sendNotification(
+ final int serverIf,
+ final String address,
+ final int srvcType,
+ final int srvcInstId,
+ final ParcelUuid srvcId,
+ final int charInstId,
+ final ParcelUuid charId,
+ boolean confirm,
+ final byte[] value) {
+ GattDelegate.Characteristic characteristic =
+ localGattDelegate().getService(srvcId).getCharacteristic(charId);
+ characteristic.setValue(value);
+ final String serverAddress = localAddress();
+ for (final String clientAddress : characteristic.getNotifyClients()) {
+ NamedRunnable clientOnNotify =
+ NamedRunnable.create(
+ "ClientGatt.onNotify",
+ () -> {
+ int clientIf = localGattDelegate().getClientIf();
+ IBluetoothGattCallback callback =
+ localGattDelegate().getClientCallback(clientIf);
+ if (callback != null) {
+ callback.onNotify(
+ serverAddress, srvcType, srvcInstId, srvcId, charInstId,
+ charId, value);
+ }
+ });
+
+ DeviceShadowEnvironmentImpl.runOnService(clientAddress, clientOnNotify);
+ }
+
+ NamedRunnable serverOnNotificationSent =
+ NamedRunnable.create(
+ "ServerGatt.onNotificationSent",
+ () -> {
+ IBluetoothGattServerCallback callback =
+ localGattDelegate().getServerCallback(serverIf);
+ if (callback != null) {
+ callback.onNotificationSent(address, BluetoothGatt.GATT_SUCCESS);
+ }
+ });
+
+ DeviceShadowEnvironmentImpl.runOnService(serverAddress, serverOnNotificationSent);
+ }
+
+ @Override
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void configureMTU(int clientIf, String address, int mtu) {
+ final String clientAddress = localAddress();
+
+ NamedRunnable clientSetMtu =
+ NamedRunnable.create(
+ "ClientGatt.setMtu",
+ () -> {
+ localGattDelegate().clientSetMtu(clientIf, mtu, address);
+ });
+ NamedRunnable serverSetMtu =
+ NamedRunnable.create(
+ "ServerGatt.setMtu",
+ () -> {
+ int serverIf = localGattDelegate().getServerIf();
+ localGattDelegate().serverSetMtu(serverIf, mtu, clientAddress);
+ });
+
+ DeviceShadowEnvironmentImpl.runOnService(clientAddress, clientSetMtu);
+
+ DeviceShadowEnvironmentImpl.runOnService(address, serverSetMtu);
+ }
+
+ @Override
+ public void connectionParameterUpdate(int clientIf, String address, int connectionPriority) {
+ // TODO(b/200231384): Implement.
+ }
+
+ @Override
+ public void disconnectAll() {
+ }
+
+ @Override
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ return new ArrayList<>();
+ }
+
+ @VisibleForTesting
+ static GattDelegate remoteGattDelegate(String address) {
+ return DeviceShadowEnvironmentImpl.getBlueletImpl(address).getGattDelegate();
+ }
+
+ private static GattDelegate localGattDelegate() {
+ return DeviceShadowEnvironmentImpl.getLocalBlueletImpl().getGattDelegate();
+ }
+
+ private static String localAddress() {
+ return DeviceShadowEnvironmentImpl.getLocalBlueletImpl().address;
+ }
+
+ private static NamedRunnable newClientConnectionStateChangeRunnable(
+ final int clientIf, final boolean isConnected, final String serverAddress) {
+ return NamedRunnable.create(
+ "ClientGatt.clientConnectionStateChange",
+ () -> {
+ localGattDelegate()
+ .clientConnectionStateChange(
+ BluetoothGatt.GATT_SUCCESS, clientIf, isConnected,
+ serverAddress);
+ });
+ }
+
+ private static NamedRunnable newServerConnectionStateChangeRunnable(
+ final int serverIf, final boolean isConnected, final String clientAddress) {
+ return NamedRunnable.create(
+ "ServerGatt.serverConnectionStateChange",
+ () -> {
+ localGattDelegate()
+ .serverConnectionStateChange(
+ BluetoothGatt.GATT_SUCCESS, serverIf, isConnected,
+ clientAddress);
+ });
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothImpl.java
new file mode 100644
index 0000000..ccf0ac3
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothImpl.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.bluetooth;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.IBluetooth;
+import android.bluetooth.OobData;
+import android.content.AttributionSource;
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelUuid;
+
+import com.android.libraries.testing.deviceshadower.Bluelet.CreateBondOutcome;
+import com.android.libraries.testing.deviceshadower.Bluelet.FetchUuidsTiming;
+import com.android.libraries.testing.deviceshadower.Bluelet.IoCapabilities;
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.AdapterDelegate.State;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.BlueletImpl.PairingConfirmation;
+import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
+
+import com.google.common.base.Preconditions;
+
+import java.util.Random;
+
+/**
+ * Implementation of IBluetooth interface.
+ */
+public class IBluetoothImpl implements IBluetooth {
+
+ private static final Logger LOGGER = Logger.create("BlueletImpl");
+
+ private enum PairingVariant {
+ JUST_WORKS,
+ /**
+ * AKA Passkey Confirmation.
+ */
+ NUMERIC_COMPARISON,
+ PASSKEY_INPUT,
+ CONSENT
+ }
+
+ /**
+ * User will be prompted to accept or deny the incoming pairing request.
+ */
+ private static final int PAIRING_VARIANT_CONSENT = 3;
+
+ /**
+ * User will be prompted to enter the passkey displayed on remote device. This is used for
+ * Bluetooth 2.1 pairing.
+ */
+ private static final int PAIRING_VARIANT_DISPLAY_PASSKEY = 4;
+
+ public IBluetoothImpl() {
+ }
+
+ @Override
+ public String getAddress() {
+ return localBlueletImpl().address;
+ }
+
+ @Override
+ public String getName() {
+ return localBlueletImpl().mName;
+ }
+
+ @Override
+ public boolean setName(String name) {
+ localBlueletImpl().mName = name;
+ return true;
+ }
+
+ @Override
+ public int getRemoteClass(BluetoothDevice device) {
+ return remoteBlueletImpl(device.getAddress()).getAdapterDelegate().getBluetoothClass();
+ }
+
+ @Override
+ public String getRemoteName(BluetoothDevice device) {
+ return remoteBlueletImpl(device.getAddress()).mName;
+ }
+
+ @Override
+ public int getRemoteType(BluetoothDevice device, AttributionSource attributionSource) {
+ return BluetoothDevice.DEVICE_TYPE_LE;
+ }
+
+ @Override
+ public ParcelUuid[] getRemoteUuids(BluetoothDevice device) {
+ return remoteBlueletImpl(device.getAddress()).mProfileUuids;
+ }
+
+ @Override
+ public boolean fetchRemoteUuids(BluetoothDevice device) {
+ localBlueletImpl().onFetchedUuids(device.getAddress(), getRemoteUuids(device));
+ return true;
+ }
+
+ @Override
+ public int getBondState(BluetoothDevice device, AttributionSource attributionSource) {
+ return localBlueletImpl().getBondState(device.getAddress());
+ }
+
+ @Override
+ public boolean createBond(BluetoothDevice device, int transport, OobData remoteP192Data,
+ OobData remoteP256Data, AttributionSource attributionSource) {
+ setBondState(device.getAddress(), BluetoothDevice.BOND_BONDING, BlueletImpl.REASON_SUCCESS);
+
+ BlueletImpl remoteBluelet = remoteBlueletImpl(device.getAddress());
+ BlueletImpl localBluelet = localBlueletImpl();
+
+ // Like the real Bluetooth stack, choose a pairing variant based on IO Capabilities.
+ // https://blog.bluetooth.com/bluetooth-pairing-part-2-key-generation-methods
+ PairingVariant variant = PairingVariant.JUST_WORKS;
+ if (localBluelet.getIoCapabilities() == IoCapabilities.DISPLAY_YES_NO) {
+ if (remoteBluelet.getIoCapabilities() == IoCapabilities.DISPLAY_YES_NO) {
+ variant = PairingVariant.NUMERIC_COMPARISON;
+ } else if (remoteBluelet.getIoCapabilities() == IoCapabilities.KEYBOARD_ONLY) {
+ variant = PairingVariant.PASSKEY_INPUT;
+ } else if (remoteBluelet.getIoCapabilities() == IoCapabilities.NO_INPUT_NO_OUTPUT
+ && localBluelet.getEnableCVE20192225()) {
+ // After CVE-2019-2225, Bluetooth decides to ask consent instead of JustWorks.
+ variant = PairingVariant.CONSENT;
+ }
+ }
+
+ // Bonding doesn't complete until the passkey is confirmed on both devices. The passkey is a
+ // positive 6-digit integer, generated by the Bluetooth stack.
+ int passkey = new Random().nextInt(999999) + 1;
+ switch (variant) {
+ case NUMERIC_COMPARISON:
+ localBluelet.onPairingRequest(
+ remoteBluelet.address, BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION,
+ passkey);
+ remoteBluelet.onPairingRequest(
+ localBluelet.address, BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION,
+ passkey);
+ break;
+ case JUST_WORKS:
+ // Bonding completes immediately, with no PAIRING_REQUEST broadcast.
+ finishBonding(device);
+ break;
+ case PASSKEY_INPUT:
+ localBluelet.onPairingRequest(
+ remoteBluelet.address, PAIRING_VARIANT_DISPLAY_PASSKEY, passkey);
+ localBluelet.mPassKey = passkey;
+ remoteBluelet.onPairingRequest(
+ localBluelet.address, PAIRING_VARIANT_DISPLAY_PASSKEY, passkey);
+ break;
+ case CONSENT:
+ localBluelet.onPairingRequest(remoteBluelet.address,
+ PAIRING_VARIANT_CONSENT, /* key= */ 0);
+ if (remoteBluelet.getIoCapabilities() == IoCapabilities.NO_INPUT_NO_OUTPUT) {
+ remoteBluelet.setPairingConfirmation(localBluelet.address,
+ PairingConfirmation.CONFIRMED);
+ } else {
+ remoteBluelet.onPairingRequest(
+ localBluelet.address, PAIRING_VARIANT_CONSENT, /* key= */ 0);
+ }
+ break;
+ }
+ return true;
+ }
+
+ private void finishBonding(BluetoothDevice device) {
+ BlueletImpl remoteBluelet = remoteBlueletImpl(device.getAddress());
+ finishBonding(
+ device, remoteBluelet.getCreateBondOutcome(),
+ remoteBluelet.getCreateBondFailureReason());
+ }
+
+ private void finishBonding(BluetoothDevice device, CreateBondOutcome outcome,
+ int failureReason) {
+ switch (outcome) {
+ case SUCCESS:
+ setBondState(device.getAddress(), BluetoothDevice.BOND_BONDED,
+ BlueletImpl.REASON_SUCCESS);
+ break;
+ case FAILURE:
+ setBondState(device.getAddress(), BluetoothDevice.BOND_NONE, failureReason);
+ break;
+ case TIMEOUT:
+ // Send nothing.
+ break;
+ }
+ }
+
+ @Override
+ public boolean setPairingConfirmation(BluetoothDevice device, boolean confirmed,
+ AttributionSource attributionSource) {
+ localBlueletImpl()
+ .setPairingConfirmation(
+ device.getAddress(),
+ confirmed ? PairingConfirmation.CONFIRMED : PairingConfirmation.DENIED);
+
+ PairingConfirmation remoteConfirmation =
+ remoteBlueletImpl(device.getAddress()).getPairingConfirmation(
+ localBlueletImpl().address);
+ if (confirmed && remoteConfirmation == PairingConfirmation.CONFIRMED) {
+ LOGGER.d(String.format("CONFIRMED"));
+ finishBonding(device);
+ } else if (!confirmed || remoteConfirmation == PairingConfirmation.DENIED) {
+ LOGGER.d(String.format("NOT CONFIRMED"));
+ finishBonding(device, CreateBondOutcome.FAILURE, BlueletImpl.UNBOND_REASON_AUTH_FAILED);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean setPasskey(BluetoothDevice device, int passkey) {
+ BlueletImpl remoteBluelet = remoteBlueletImpl(device.getAddress());
+ if (passkey == remoteBluelet.mPassKey) {
+ finishBonding(device);
+ } else {
+ finishBonding(device, CreateBondOutcome.FAILURE, BlueletImpl.UNBOND_REASON_AUTH_FAILED);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean cancelBondProcess(BluetoothDevice device) {
+ finishBonding(device, CreateBondOutcome.FAILURE, BlueletImpl.UNBOND_REASON_AUTH_CANCELED);
+ return true;
+ }
+
+ @Override
+ public boolean removeBond(BluetoothDevice device) {
+ setBondState(device.getAddress(), BluetoothDevice.BOND_NONE, BlueletImpl.REASON_SUCCESS);
+ return true;
+ }
+
+ @Override
+ public BluetoothDevice[] getBondedDevices() {
+ return localBlueletImpl().getBondedDevices();
+ }
+
+ @Override
+ public int getAdapterConnectionState() {
+ return localBlueletImpl().getAdapterConnectionState();
+ }
+
+ @Override
+ public int getProfileConnectionState(int profile) {
+ return localBlueletImpl().getProfileConnectionState(profile);
+ }
+
+ @Override
+ public int getPhonebookAccessPermission(BluetoothDevice device) {
+ return remoteBlueletImpl(device.getAddress()).mPhonebookAccessPermission;
+ }
+
+ @Override
+ public boolean setPhonebookAccessPermission(BluetoothDevice device, int value) {
+ remoteBlueletImpl(device.getAddress()).mPhonebookAccessPermission = value;
+ return true;
+ }
+
+ @Override
+ public int getMessageAccessPermission(BluetoothDevice device) {
+ return remoteBlueletImpl(device.getAddress()).mMessageAccessPermission;
+ }
+
+ @Override
+ public boolean setMessageAccessPermission(BluetoothDevice device, int value) {
+ remoteBlueletImpl(device.getAddress()).mMessageAccessPermission = value;
+ return true;
+ }
+
+ @Override
+ public int getSimAccessPermission(BluetoothDevice device) {
+ return remoteBlueletImpl(device.getAddress()).mSimAccessPermission;
+ }
+
+ @Override
+ public boolean setSimAccessPermission(BluetoothDevice device, int value) {
+ remoteBlueletImpl(device.getAddress()).mSimAccessPermission = value;
+ return true;
+ }
+
+ private static void setBondState(String remoteAddress, int state, int failureReason) {
+ BlueletImpl remoteBluelet = remoteBlueletImpl(remoteAddress);
+
+ if (remoteBluelet.getFetchUuidsTiming() == FetchUuidsTiming.BEFORE_BONDING) {
+ fetchUuidsOnBondedState(remoteAddress, state);
+ }
+
+ remoteBluelet.setBondState(localBlueletImpl().address, state, failureReason);
+ localBlueletImpl().setBondState(remoteAddress, state, failureReason);
+
+ if (remoteBluelet.getFetchUuidsTiming() == FetchUuidsTiming.AFTER_BONDING) {
+ fetchUuidsOnBondedState(remoteAddress, state);
+ }
+ }
+
+ private static void fetchUuidsOnBondedState(String remoteAddress, int state) {
+ if (state == BluetoothDevice.BOND_BONDED) {
+ remoteBlueletImpl(remoteAddress)
+ .onFetchedUuids(localBlueletImpl().address, localBlueletImpl().mProfileUuids);
+ localBlueletImpl()
+ .onFetchedUuids(remoteAddress, remoteBlueletImpl(remoteAddress).mProfileUuids);
+ }
+ }
+
+ @Override
+ public int getScanMode() {
+ return localBlueletImpl().getAdapterDelegate().getScanMode();
+ }
+
+ @Override
+ public boolean setScanMode(int mode, int duration) {
+ localBlueletImpl().getAdapterDelegate().setScanMode(mode);
+ return true;
+ }
+
+ @Override
+ public int getDiscoverableTimeout() {
+ return -1;
+ }
+
+ @Override
+ public boolean setDiscoverableTimeout(int timeout) {
+ return true;
+ }
+
+ @Override
+ public boolean startDiscovery() {
+ localBlueletImpl().getAdapterDelegate().startDiscovery();
+ return true;
+ }
+
+ @Override
+ public boolean cancelDiscovery() {
+ localBlueletImpl().getAdapterDelegate().cancelDiscovery();
+ return true;
+ }
+
+ @Override
+ public boolean isDiscovering() {
+ return localBlueletImpl().getAdapterDelegate().isDiscovering();
+
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return localBlueletImpl().getAdapterDelegate().getState().equals(State.ON);
+ }
+
+ @Override
+ public int getState() {
+ return localBlueletImpl().getAdapterDelegate().getState().getValue();
+ }
+
+ @Override
+ public boolean enable() {
+ localBlueletImpl().enableAdapter();
+ return true;
+ }
+
+ @Override
+ public boolean disable() {
+ localBlueletImpl().disableAdapter();
+ return true;
+ }
+
+ @Override
+ public ParcelFileDescriptor connectSocket(BluetoothDevice device, int type, ParcelUuid uuid,
+ int port, int flag) {
+ Preconditions.checkArgument(
+ port == BluetoothConstants.SOCKET_CHANNEL_CONNECT_WITH_UUID,
+ "Connect to port is not supported.");
+ Preconditions.checkArgument(
+ type == BluetoothConstants.TYPE_RFCOMM,
+ "Only Rfcomm socket is supported.");
+ return localBlueletImpl().getRfcommDelegate()
+ .connectSocket(device.getAddress(), uuid.getUuid());
+ }
+
+ @Override
+ public ParcelFileDescriptor createSocketChannel(int type, String serviceName, ParcelUuid uuid,
+ int port, int flag) {
+ Preconditions.checkArgument(
+ port == BluetoothConstants.SERVER_SOCKET_CHANNEL_AUTO_ASSIGN,
+ "Listen on port is not supported.");
+ Preconditions.checkArgument(
+ type == BluetoothConstants.TYPE_RFCOMM,
+ "Only Rfcomm socket is supported.");
+ return localBlueletImpl().getRfcommDelegate().createSocketChannel(serviceName, uuid);
+ }
+
+ @Override
+ public boolean isMultiAdvertisementSupported() {
+ return maxAdvertiseInstances() > 1;
+ }
+
+ @Override
+ public boolean isPeripheralModeSupported() {
+ return maxAdvertiseInstances() > 0;
+ }
+
+ private int maxAdvertiseInstances() {
+ return localBlueletImpl().getGattDelegate().getMaxAdvertiseInstances();
+ }
+
+ @Override
+ public boolean isOffloadedFilteringSupported() {
+ return localBlueletImpl().getGattDelegate().isOffloadedFilteringSupported();
+ }
+
+ private static BlueletImpl localBlueletImpl() {
+ return DeviceShadowEnvironmentImpl.getLocalBlueletImpl();
+ }
+
+ private static BlueletImpl remoteBlueletImpl(String address) {
+ return DeviceShadowEnvironmentImpl.getBlueletImpl(address);
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothManagerImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothManagerImpl.java
new file mode 100644
index 0000000..cb38a41
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/IBluetoothManagerImpl.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.bluetooth;
+
+import android.bluetooth.IBluetooth;
+import android.bluetooth.IBluetoothGatt;
+import android.bluetooth.IBluetoothManager;
+import android.bluetooth.IBluetoothManagerCallback;
+
+/**
+ * Implementation of IBluetoothManager interface
+ */
+public class IBluetoothManagerImpl implements IBluetoothManager {
+
+ private final IBluetooth mFakeBluetoothService = new IBluetoothImpl();
+ private final IBluetoothGatt mFakeGattService = new IBluetoothGattImpl();
+
+ @Override
+ public String getAddress() {
+ return mFakeBluetoothService.getAddress();
+ }
+
+ @Override
+ public String getName() {
+ return mFakeBluetoothService.getName();
+ }
+
+ @Override
+ public IBluetooth registerAdapter(IBluetoothManagerCallback callback) {
+ return mFakeBluetoothService;
+ }
+
+ @Override
+ public IBluetoothGatt getBluetoothGatt() {
+ return mFakeGattService;
+ }
+
+ @Override
+ public boolean enable() {
+ mFakeBluetoothService.enable();
+ return true;
+ }
+
+ @Override
+ public boolean disable(boolean persist) {
+ mFakeBluetoothService.disable();
+ return true;
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/FileDescriptorFactory.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/FileDescriptorFactory.java
new file mode 100644
index 0000000..12fa587
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/FileDescriptorFactory.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.bluetooth.connection;
+
+import java.io.FileDescriptor;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Factory which creates {@link FileDescriptor} given an MAC address. Each MAC address can have many
+ * FileDescriptor but each FileDescriptor only maps to one MAC address.
+ */
+public class FileDescriptorFactory {
+
+ private static FileDescriptorFactory sInstance = null;
+
+ public static synchronized FileDescriptorFactory getInstance() {
+ if (sInstance == null) {
+ sInstance = new FileDescriptorFactory();
+ }
+ return sInstance;
+ }
+
+ public static synchronized void reset() {
+ sInstance = null;
+ }
+
+ private final Map<FileDescriptor, String> mAddressMap;
+
+ private FileDescriptorFactory() {
+ mAddressMap = new ConcurrentHashMap<>();
+ }
+
+ public FileDescriptor createFileDescriptor(String address) {
+ FileDescriptor fd = new FileDescriptor();
+ mAddressMap.put(fd, address);
+ return fd;
+ }
+
+ public String getAddress(FileDescriptor fd) {
+ return mAddressMap.get(fd);
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/PageScanHandler.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/PageScanHandler.java
new file mode 100644
index 0000000..82b97ff
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/PageScanHandler.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.bluetooth.connection;
+
+import android.os.Build.VERSION;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.libraries.testing.deviceshadower.internal.utils.MacAddressGenerator;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Map;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * Encapsulate page scan operations -- handle connection establishment between Bluetooth devices.
+ */
+public class PageScanHandler {
+
+ private static final ConnectionRequest REQUEST_SERVER_SOCKET_CLOSE = new ConnectionRequest();
+
+ private static PageScanHandler sInstance = null;
+
+ public static synchronized PageScanHandler getInstance() {
+ if (sInstance == null) {
+ sInstance = new PageScanHandler();
+ }
+ return sInstance;
+ }
+
+ public static synchronized void reset() {
+ sInstance = null;
+ }
+
+ // use FileDescriptor to identify incoming data before socket is connected.
+ private final Map<FileDescriptor, BlockingQueue<Integer>> mIncomingDataMap;
+ // map a server socket fd to a connection request queue
+ private final Map<FileDescriptor, BlockingQueue<ConnectionRequest>> mConnectionRequests;
+ // map a fd on client side to a fd of BluetoothSocket(not BluetoothServerSocket) on server side
+ private final Map<FileDescriptor, FileDescriptor> mClientServerFdMap;
+ // map a client fd to a connection request so the client socket can finish the pending
+ // connection
+ private final Map<FileDescriptor, ConnectionRequest> mPendingConnections;
+
+ private PageScanHandler() {
+ mIncomingDataMap = new ConcurrentHashMap<>();
+ mConnectionRequests = new ConcurrentHashMap<>();
+ mClientServerFdMap = new ConcurrentHashMap<>();
+ mPendingConnections = new ConcurrentHashMap<>();
+ }
+
+ public void postConnectionRequest(FileDescriptor serverSocketFd, ConnectionRequest request)
+ throws InterruptedException {
+ // used by the returning socket on server-side
+ FileDescriptor fd = FileDescriptorFactory.getInstance()
+ .createFileDescriptor(request.mServerAddress);
+ mClientServerFdMap.put(request.mClientFd, fd);
+ BlockingQueue<ConnectionRequest> requests = mConnectionRequests.get(serverSocketFd);
+ requests.put(request);
+ mPendingConnections.put(request.mClientFd, request);
+ }
+
+ public void addServerSocket(FileDescriptor serverSocketFd) {
+ mConnectionRequests.put(serverSocketFd, new LinkedBlockingQueue<ConnectionRequest>());
+ }
+
+ public FileDescriptor getServerFd(FileDescriptor clientFd) {
+ return mClientServerFdMap.get(clientFd);
+ }
+
+ // TODO(b/79994182): see go/objecttostring-lsc
+ @SuppressWarnings("ObjectToString")
+ public FileDescriptor processNextConnectionRequest(FileDescriptor serverSocketFd)
+ throws IOException, InterruptedException {
+ ConnectionRequest request = mConnectionRequests.get(serverSocketFd).take();
+ if (request == REQUEST_SERVER_SOCKET_CLOSE) {
+ // TODO(b/79994182): FileDescriptor does not implement toString() in serverSocketFd
+ throw new IOException("Server socket is closed. fd: " + serverSocketFd);
+ }
+ writeInitialConnectionInfo(serverSocketFd, request.mClientAddress, request.mPort);
+ return request.mClientFd;
+ }
+
+ public void waitForConnectionEstablished(FileDescriptor clientFd) throws InterruptedException {
+ ConnectionRequest request = mPendingConnections.get(clientFd);
+ if (request != null) {
+ request.mCountDownLatch.await();
+ }
+ }
+
+ public void finishPendingConnection(FileDescriptor clientFd) {
+ ConnectionRequest request = mPendingConnections.get(clientFd);
+ if (request != null) {
+ request.mCountDownLatch.countDown();
+ }
+ }
+
+ public void cancelServerSocket(FileDescriptor serverSocketFd) throws InterruptedException {
+ mConnectionRequests.get(serverSocketFd).put(REQUEST_SERVER_SOCKET_CLOSE);
+ }
+
+ public void writeInitialConnectionInfo(FileDescriptor fd, String address, int port)
+ throws InterruptedException {
+ for (byte b : initialConnectionInfo(address, port)) {
+ write(fd, Integer.valueOf(b));
+ }
+ }
+
+ public void writePort(FileDescriptor fd, int port) throws InterruptedException {
+ byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(port).array();
+ for (byte b : bytes) {
+ write(fd, Integer.valueOf(b));
+ }
+ }
+
+ public void write(FileDescriptor fd, int data) throws InterruptedException {
+ BlockingQueue<Integer> incomingData = mIncomingDataMap.get(fd);
+ if (incomingData == null) {
+ synchronized (mIncomingDataMap) {
+ incomingData = mIncomingDataMap.get(fd);
+ if (incomingData == null) {
+ incomingData = new LinkedBlockingQueue<Integer>();
+ mIncomingDataMap.put(fd, incomingData);
+ }
+ }
+ }
+ incomingData.put(data);
+ }
+
+ public int read(FileDescriptor fd) throws InterruptedException {
+ return mIncomingDataMap.get(fd).take();
+ }
+
+ /**
+ * A connection request from a {@link android.bluetooth.BluetoothSocket}.
+ */
+ @VisibleForTesting
+ public static class ConnectionRequest {
+
+ final FileDescriptor mClientFd;
+ final String mClientAddress;
+ final String mServerAddress;
+ final int mPort;
+ final CountDownLatch mCountDownLatch; // block server socket until connection established
+
+ public ConnectionRequest(FileDescriptor fd, String clientAddress, String serverAddress,
+ int port) {
+ mClientFd = fd;
+ this.mClientAddress = clientAddress;
+ this.mServerAddress = serverAddress;
+ this.mPort = port;
+ mCountDownLatch = new CountDownLatch(1);
+ }
+
+ private ConnectionRequest() {
+ mClientFd = null;
+ mClientAddress = null;
+ mServerAddress = null;
+ mPort = -1;
+ mCountDownLatch = new CountDownLatch(0);
+ }
+ }
+
+ private static byte[] initialConnectionInfo(String addr, int port) {
+ byte[] mac = MacAddressGenerator.convertStringMacAddress(addr);
+ int channel = port;
+ int status = 0;
+
+ if (VERSION.SDK_INT < 23) {
+ byte[] signal = new byte[16];
+ short signalSize = 16;
+ ByteBuffer buffer = ByteBuffer.wrap(signal);
+ buffer.order(ByteOrder.LITTLE_ENDIAN)
+ .putShort(signalSize)
+ .put(mac)
+ .putInt(channel)
+ .putInt(status);
+ return buffer.array();
+ } else {
+ byte[] signal = new byte[20];
+ short signalSize = 20;
+ short maxTxPacketSize = 10000;
+ short maxRxPacketSize = 10000;
+ ByteBuffer buffer = ByteBuffer.wrap(signal);
+ buffer.order(ByteOrder.LITTLE_ENDIAN)
+ .putShort(signalSize)
+ .put(mac)
+ .putInt(channel)
+ .putInt(status)
+ .putShort(maxTxPacketSize)
+ .putShort(maxRxPacketSize);
+ return buffer.array();
+ }
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/PhysicalLink.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/PhysicalLink.java
new file mode 100644
index 0000000..e474c69
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/PhysicalLink.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.bluetooth.connection;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
+
+import com.google.common.collect.Sets;
+
+import java.io.FileDescriptor;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A class represents a physical link for communications between two Bluetooth devices.
+ */
+public class PhysicalLink {
+
+ // Intended to use RfcommDelegate
+ private static final Logger LOGGER = Logger.create("RfcommDelegate");
+
+ private final Object mLock;
+ // Every socket has unique FileDescriptor, so use it as socket identifier during communication
+ private final Map<FileDescriptor, RfcommSocketConnection> mConnectionLookup;
+ // Map fd of a socket to the fd of the other socket it connects to
+ private final Map<FileDescriptor, FileDescriptor> mFdMap;
+ private final Set<RfcommSocketConnection> mConnections;
+ private final AtomicBoolean mIsEncrypted;
+ private final Map<String, RfcommDelegate.Callback> mCallbacks = new HashMap<>();
+
+ public PhysicalLink(String address1, String address2) {
+ this(address1,
+ DeviceShadowEnvironmentImpl.getBlueletImpl(address1).getRfcommDelegate().mCallback,
+ address2,
+ DeviceShadowEnvironmentImpl.getBlueletImpl(address2).getRfcommDelegate().mCallback,
+ new ConcurrentHashMap<FileDescriptor, RfcommSocketConnection>(),
+ new ConcurrentHashMap<FileDescriptor, FileDescriptor>(),
+ Sets.<RfcommSocketConnection>newConcurrentHashSet());
+ }
+
+ @VisibleForTesting
+ PhysicalLink(String address1, RfcommDelegate.Callback callback1,
+ String address2, RfcommDelegate.Callback callback2,
+ Map<FileDescriptor, RfcommSocketConnection> connectionLookup,
+ Map<FileDescriptor, FileDescriptor> fdMap,
+ Set<RfcommSocketConnection> connections) {
+ mLock = new Object();
+ mCallbacks.put(address1, callback1);
+ mCallbacks.put(address2, callback2);
+ this.mConnectionLookup = connectionLookup;
+ this.mFdMap = fdMap;
+ this.mConnections = connections;
+ mIsEncrypted = new AtomicBoolean(false);
+ }
+
+ public void addConnection(FileDescriptor fd1, FileDescriptor fd2) {
+ synchronized (mLock) {
+ int oldSize = mConnections.size();
+ RfcommSocketConnection connection = new RfcommSocketConnection(
+ FileDescriptorFactory.getInstance().getAddress(fd1),
+ FileDescriptorFactory.getInstance().getAddress(fd2)
+ );
+ mConnections.add(connection);
+ mConnectionLookup.put(fd1, connection);
+ mConnectionLookup.put(fd2, connection);
+ mFdMap.put(fd1, fd2);
+ mFdMap.put(fd2, fd1);
+ if (oldSize == 0) {
+ onConnectionStateChange(true);
+ }
+ }
+ }
+
+ // TODO(b/79994182): see go/objecttostring-lsc
+ @SuppressWarnings("ObjectToString")
+ public void closeConnection(FileDescriptor fd) {
+ // check for early return without locking
+ if (!mConnectionLookup.containsKey(fd)) {
+ // TODO(b/79994182): FileDescriptor does not implement toString() in fd
+ LOGGER.d("Connection doesn't exist, FileDescriptor: " + fd);
+ return;
+ }
+ synchronized (mLock) {
+ RfcommSocketConnection connection = mConnectionLookup.get(fd);
+ if (connection == null) {
+ // TODO(b/79994182): FileDescriptor does not implement toString() in fd
+ LOGGER.d("Connection doesn't exist, FileDescriptor: " + fd);
+ return;
+ }
+ int oldSize = mConnections.size();
+ FileDescriptor connectingFd = mFdMap.get(fd);
+ mConnectionLookup.remove(fd);
+ mConnectionLookup.remove(connectingFd);
+ mFdMap.remove(fd);
+ mFdMap.remove(connectingFd);
+ mConnections.remove(connection);
+ if (oldSize == 1) {
+ onConnectionStateChange(false);
+ }
+ }
+ }
+
+ public RfcommSocketConnection getConnection(FileDescriptor fd) {
+ return mConnectionLookup.get(fd);
+ }
+
+ public void encrypt() {
+ mIsEncrypted.set(true);
+ }
+
+ public boolean isEncrypted() {
+ return mIsEncrypted.get();
+ }
+
+ public boolean isConnected() {
+ return !mConnections.isEmpty();
+ }
+
+ private void onConnectionStateChange(boolean isConnected) {
+ for (Entry<String, RfcommDelegate.Callback> entry : mCallbacks.entrySet()) {
+ RfcommDelegate.Callback callback = entry.getValue();
+ String localAddress = entry.getKey();
+ callback.onConnectionStateChange(getRemoteAddress(localAddress), isConnected);
+ }
+ }
+
+ private String getRemoteAddress(String address) {
+ String remoteAddress = null;
+ for (String addr : mCallbacks.keySet()) {
+ if (!addr.equals(address)) {
+ remoteAddress = addr;
+ break;
+ }
+ }
+ return remoteAddress;
+ }
+
+ /**
+ * Represents a Rfcomm socket connection between two {@link android.bluetooth.BluetoothSocket}.
+ */
+ public static class RfcommSocketConnection {
+
+ final Map<String, BlockingQueue<Integer>> mIncomingDataMap; // address : incomingData
+
+ public RfcommSocketConnection(String address1, String address2) {
+ mIncomingDataMap = new ConcurrentHashMap<>();
+ mIncomingDataMap.put(address1, new LinkedBlockingQueue<Integer>());
+ mIncomingDataMap.put(address2, new LinkedBlockingQueue<Integer>());
+ }
+
+ public void write(String address, int b) throws InterruptedException {
+ mIncomingDataMap.get(address).put(b);
+ }
+
+ public int read(String address) throws InterruptedException {
+ return mIncomingDataMap.get(address).take();
+ }
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/RfcommDelegate.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/RfcommDelegate.java
new file mode 100644
index 0000000..3a4fdf6
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/RfcommDelegate.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.bluetooth.connection;
+
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelUuid;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.BlueletImpl;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.BluetoothConstants;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.PageScanHandler.ConnectionRequest;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.PhysicalLink.RfcommSocketConnection;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.SdpHandler.ServiceRecord;
+import com.android.libraries.testing.deviceshadower.internal.common.Interrupter;
+import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
+
+import com.google.errorprone.annotations.FormatMethod;
+
+import org.robolectric.util.ReflectionHelpers;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Delegate for Bluetooth Rfcommon operations, including creating service record, establishing
+ * connection, and data communications.
+ * <p>Socket connection with uuid is supported. Listen on port and connect to port are not
+ * supported.</p>
+ */
+public class RfcommDelegate {
+
+ private static final Logger LOGGER = Logger.create("RfcommDelegate");
+ private static final Object LOCK = new Object();
+
+ /**
+ * Callback for Rfcomm operations
+ */
+ public interface Callback {
+
+ void onConnectionStateChange(String remoteAddress, boolean isConnected);
+ }
+
+ public static void reset() {
+ PageScanHandler.reset();
+ FileDescriptorFactory.reset();
+ }
+
+ final Callback mCallback;
+ private final String mAddress;
+ private final Interrupter mInterrupter;
+ private final SdpHandler mSdpHandler;
+ private final PageScanHandler mPageScanHandler;
+ private final Map<String, PhysicalLink> mConnectionMap; // remoteAddress : physicalLink
+
+ public RfcommDelegate(String address, Callback callback, Interrupter interrupter) {
+ this.mAddress = address;
+ this.mCallback = callback;
+ this.mInterrupter = interrupter;
+ mSdpHandler = new SdpHandler(address);
+ mPageScanHandler = PageScanHandler.getInstance();
+ mConnectionMap = new ConcurrentHashMap<>();
+ }
+
+ @SuppressWarnings("ObjectToString")
+ public ParcelFileDescriptor createSocketChannel(String serviceName, ParcelUuid uuid) {
+ ServiceRecord record = mSdpHandler.createServiceRecord(uuid.getUuid(), serviceName);
+ if (record == null) {
+ LOGGER.e(
+ String.format("Address %s: failed to create socket channel, uuid: %s", mAddress,
+ uuid));
+ return null;
+ }
+ try {
+ mPageScanHandler.writePort(record.mServerSocketFd, record.mPort);
+ } catch (InterruptedException e) {
+ LOGGER.e(String.format("Address %s: failed to write port to incoming data, fd: %s",
+ mAddress,
+ record.mServerSocketFd), e);
+ return null;
+ }
+ return parcelFileDescriptor(record.mServerSocketFd);
+ }
+
+ @SuppressWarnings("ObjectToString")
+ public ParcelFileDescriptor connectSocket(String remoteAddress, UUID uuid) {
+ BlueletImpl remote = DeviceShadowEnvironmentImpl.getBlueletImpl(remoteAddress);
+ if (remote == null) {
+ LOGGER.e(String.format("Device %s is not defined.", remoteAddress));
+ return null;
+ }
+ ServiceRecord record = remote.getRfcommDelegate().mSdpHandler.lookupChannel(uuid);
+ if (record == null) {
+ LOGGER.e(String.format("Address %s: failed to connect socket, uuid: %s", mAddress,
+ uuid));
+ return null;
+ }
+ FileDescriptor fd = FileDescriptorFactory.getInstance().createFileDescriptor(mAddress);
+ try {
+ mPageScanHandler.writePort(fd, record.mPort);
+ } catch (InterruptedException e) {
+ LOGGER.e(String.format("Address %s: failed to write port to incoming data, fd: %s",
+ mAddress,
+ fd), e);
+ return null;
+ }
+
+ // establish connection
+ try {
+ initiateConnectToServer(fd, record, remoteAddress);
+ } catch (IOException e) {
+ LOGGER.e(
+ String.format("Address %s: fail to initiate connection to server, clientFd: %s",
+ mAddress, fd), e);
+ return null;
+ }
+ return parcelFileDescriptor(fd);
+ }
+
+ /**
+ * Creates connection and unblocks server socket.
+ * <p>ShadowBluetoothSocket calls the method at the end of connect().</p>
+ */
+ public void finishPendingConnection(
+ String serverAddress, FileDescriptor clientFd, boolean isEncrypted) {
+ // update states
+ PhysicalLink physicalChannel = mConnectionMap.get(serverAddress);
+ if (physicalChannel == null) {
+ // use class level lock to ensure two RfcommDelegate hold reference to the same Physical
+ // Link
+ synchronized (LOCK) {
+ physicalChannel = mConnectionMap.get(serverAddress);
+ if (physicalChannel == null) {
+ physicalChannel = new PhysicalLink(
+ serverAddress,
+ FileDescriptorFactory.getInstance().getAddress(clientFd));
+ addPhysicalChannel(serverAddress, physicalChannel);
+ BlueletImpl remote = DeviceShadowEnvironmentImpl.getBlueletImpl(serverAddress);
+ remote.getRfcommDelegate().addPhysicalChannel(mAddress, physicalChannel);
+ }
+ }
+ }
+ physicalChannel.addConnection(clientFd, mPageScanHandler.getServerFd(clientFd));
+
+ if (isEncrypted) {
+ physicalChannel.encrypt();
+ }
+ mPageScanHandler.finishPendingConnection(clientFd);
+ }
+
+ /**
+ * Process the next {@link ConnectionRequest} to {@link android.bluetooth.BluetoothServerSocket}
+ * identified by serverSocketFd. This call will block until next connection request is
+ * available.
+ */
+ @SuppressWarnings("ObjectToString")
+ public FileDescriptor processNextConnectionRequest(FileDescriptor serverSocketFd)
+ throws IOException {
+ try {
+ return mPageScanHandler.processNextConnectionRequest(serverSocketFd);
+ } catch (InterruptedException e) {
+ throw new IOException(
+ logError(e, "failed to process next connection request, serverSocketFd: %s",
+ serverSocketFd),
+ e);
+ }
+ }
+
+ /**
+ * Waits for a connection established.
+ * <p>ShadowBluetoothServerSocket calls the method at the end of accept(). Ensure that a
+ * connection is established when accept() returns.</p>
+ */
+ @SuppressWarnings("ObjectToString")
+ public void waitForConnectionEstablished(FileDescriptor clientFd) throws IOException {
+ try {
+ mPageScanHandler.waitForConnectionEstablished(clientFd);
+ } catch (InterruptedException e) {
+ throw new IOException(
+ logError(e, "failed to wait for connection established. clientFd: %s",
+ clientFd), e);
+ }
+ }
+
+ @SuppressWarnings("ObjectToString")
+ public void write(String remoteAddress, FileDescriptor localFd, int b)
+ throws IOException {
+ checkInterrupt();
+ RfcommSocketConnection connection =
+ mConnectionMap.get(remoteAddress).getConnection(localFd);
+ if (connection == null) {
+ throw new IOException("closed");
+ }
+ try {
+ connection.write(remoteAddress, b);
+ } catch (InterruptedException e) {
+ throw new IOException(
+ logError(e, "failed to write to target %s, fd: %s", remoteAddress,
+ localFd), e);
+ }
+ }
+
+ @SuppressWarnings("ObjectToString")
+ public int read(String remoteAddress, FileDescriptor localFd) throws IOException {
+ checkInterrupt();
+ // remoteAddress is null: 1. server socket, 2. client socket before connected
+ try {
+ if (remoteAddress == null) {
+ return mPageScanHandler.read(localFd);
+ }
+ } catch (InterruptedException e) {
+ throw new IOException(logError(e, "failed to read, fd: %s", localFd), e);
+ }
+
+ RfcommSocketConnection connection =
+ mConnectionMap.get(remoteAddress).getConnection(localFd);
+ if (connection == null) {
+ throw new IOException("closed");
+ }
+ try {
+ return connection.read(mAddress);
+ } catch (InterruptedException e) {
+ throw new IOException(logError(e, "failed to read, fd: %s", localFd), e);
+ }
+ }
+
+ @SuppressWarnings("ObjectToString")
+ public void shutdownInput(String remoteAddress, FileDescriptor localFd)
+ throws IOException {
+ // remoteAddress is null: 1. server socket, 2. client socket before connected
+ try {
+ if (remoteAddress == null) {
+ mPageScanHandler.write(localFd, BluetoothConstants.SOCKET_CLOSE);
+ return;
+ }
+ } catch (InterruptedException e) {
+ throw new IOException(logError(e, "failed to shutdown input. fd: %s", localFd), e);
+ }
+
+ RfcommSocketConnection connection =
+ mConnectionMap.get(remoteAddress).getConnection(localFd);
+ if (connection == null) {
+ LOGGER.d(String.format("Address %s: Connection already closed. fd: %s.", mAddress,
+ localFd));
+ return;
+ }
+ try {
+ connection.write(mAddress, BluetoothConstants.SOCKET_CLOSE);
+ } catch (InterruptedException e) {
+ throw new IOException(logError(e, "failed to shutdown input. fd: %s", localFd), e);
+ }
+ }
+
+ @SuppressWarnings("ObjectToString")
+ public void shutdownOutput(String remoteAddress, FileDescriptor localFd)
+ throws IOException {
+ RfcommSocketConnection connection =
+ mConnectionMap.get(remoteAddress).getConnection(localFd);
+ if (connection == null) {
+ LOGGER.d(String.format("Address %s: Connection already closed. fd: %s.", mAddress,
+ localFd));
+ return;
+ }
+ try {
+ connection.write(remoteAddress, BluetoothConstants.SOCKET_CLOSE);
+ } catch (InterruptedException e) {
+ throw new IOException(logError(e, "failed to shutdown output. fd: %s", localFd), e);
+ }
+ }
+
+ @SuppressWarnings("ObjectToString")
+ public void closeServerSocket(FileDescriptor serverSocketFd) throws IOException {
+ // remove service record
+ UUID uuid = mSdpHandler.getUuid(serverSocketFd);
+ mSdpHandler.removeServiceRecord(uuid);
+ // unblock accept()
+ try {
+ mPageScanHandler.cancelServerSocket(serverSocketFd);
+ } catch (InterruptedException e) {
+ throw new IOException(
+ logError(e, "failed to cancel server socket, serverSocketFd: %s",
+ serverSocketFd),
+ e);
+ }
+ }
+
+ public FileDescriptor getServerFd(FileDescriptor clientFd) {
+ return mPageScanHandler.getServerFd(clientFd);
+ }
+
+ @VisibleForTesting
+ public void addPhysicalChannel(String remoteAddress, PhysicalLink channel) {
+ mConnectionMap.put(remoteAddress, channel);
+ }
+
+ @SuppressWarnings("ObjectToString")
+ public void initiateConnectToClient(FileDescriptor clientFd, int port)
+ throws IOException {
+ checkInterrupt();
+ String clientAddress = FileDescriptorFactory.getInstance().getAddress(clientFd);
+ LOGGER.d(String.format("Address %s: init connection to %s, clientFd: %s",
+ mAddress, clientAddress, clientFd));
+ try {
+ mPageScanHandler.writeInitialConnectionInfo(clientFd, mAddress, port);
+ } catch (InterruptedException e) {
+ throw new IOException(
+ logError(e,
+ "failed to write initial connection info to %s, clientFd: %s",
+ clientAddress, clientFd),
+ e);
+ }
+ }
+
+ @SuppressWarnings("ObjectToString")
+ private void initiateConnectToServer(FileDescriptor clientFd, ServiceRecord serviceRecord,
+ String serverAddress) throws IOException {
+ checkInterrupt();
+ LOGGER.d(
+ String.format("Address %s: init connection to %s, serverSocketFd: %s, clientFd: %s",
+ mAddress, serverAddress, serviceRecord.mServerSocketFd, clientFd));
+ try {
+ ConnectionRequest request = new ConnectionRequest(clientFd, mAddress, serverAddress,
+ serviceRecord.mPort);
+ mPageScanHandler.postConnectionRequest(serviceRecord.mServerSocketFd, request);
+ } catch (InterruptedException e) {
+ throw new IOException(
+ logError(e,
+ "failed to post connection request, serverSocketFd: %s, "
+ + "clientFd: %s",
+ serviceRecord.mServerSocketFd, clientFd),
+ e);
+ }
+ }
+
+ public void checkInterrupt() throws IOException {
+ mInterrupter.checkInterrupt();
+ }
+
+ private ParcelFileDescriptor parcelFileDescriptor(FileDescriptor fd) {
+ return ReflectionHelpers.callConstructor(ParcelFileDescriptor.class,
+ ReflectionHelpers.ClassParameter.from(FileDescriptor.class, fd));
+ }
+
+ @FormatMethod
+ private String logError(Exception e, String msgTmpl, Object... args) {
+ String errMsg = String.format("Address %s: ", mAddress) + String.format(msgTmpl, args);
+ LOGGER.e(errMsg, e);
+ return errMsg;
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/SdpHandler.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/SdpHandler.java
new file mode 100644
index 0000000..dbe8651
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/bluetooth/connection/SdpHandler.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.bluetooth.connection;
+
+import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Sets;
+
+import java.io.FileDescriptor;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Encapsulates SDP operations including creating service record and allocating channel.
+ * <p>Listen on port and connect on port are not supported. </p>
+ */
+public class SdpHandler {
+
+ // intended to use "RfcommDelegate"
+ private static final Logger LOGGER = Logger.create("RfcommDelegate");
+
+ private final Object mLock;
+ private final String mAddress;
+ private final Map<UUID, ServiceRecord> mServiceRecords;
+ private final Map<FileDescriptor, UUID> mFdUuidMap;
+ private final Set<Integer> mAvailablePortPool;
+ private final Set<Integer> mInUsePortPool;
+
+ public SdpHandler(String address) {
+ mLock = new Object();
+ this.mAddress = address;
+ mServiceRecords = new ConcurrentHashMap<>();
+ mFdUuidMap = new ConcurrentHashMap<>();
+ mAvailablePortPool = Sets.newConcurrentHashSet();
+ mInUsePortPool = Sets.newConcurrentHashSet();
+ // 1 to 30 are valid RFCOMM port
+ for (int i = 1; i <= 30; i++) {
+ mAvailablePortPool.add(i);
+ }
+ }
+
+ public ServiceRecord createServiceRecord(UUID uuid, String serviceName) {
+ Preconditions.checkNotNull(uuid);
+ LOGGER.d(String.format("Address %s: createServiceRecord with uuid %s", mAddress, uuid));
+ if (isUuidRegistered(uuid) || !checkChannelAvailability()) {
+ return null;
+ }
+ synchronized (mLock) {
+ // ensure uuid is not registered and there's available channel
+ if (isUuidRegistered(uuid) || !checkChannelAvailability()) {
+ return null;
+ }
+ Iterator<Integer> available = mAvailablePortPool.iterator();
+ int port = available.next();
+ mAvailablePortPool.remove(port);
+ mInUsePortPool.add(port);
+ ServiceRecord record = new ServiceRecord(mAddress, serviceName, port);
+ mServiceRecords.put(uuid, record);
+ mFdUuidMap.put(record.mServerSocketFd, uuid);
+ PageScanHandler.getInstance().addServerSocket(record.mServerSocketFd);
+ return record;
+ }
+ }
+
+ public void removeServiceRecord(UUID uuid) {
+ Preconditions.checkNotNull(uuid);
+ LOGGER.d(String.format("Address %s: removeServiceRecord with uuid %s", mAddress, uuid));
+ if (!isUuidRegistered(uuid)) {
+ return;
+ }
+ synchronized (mLock) {
+ if (!isUuidRegistered(uuid)) {
+ return;
+ }
+ ServiceRecord record = mServiceRecords.get(uuid);
+ mServiceRecords.remove(uuid);
+ mInUsePortPool.remove(record.mPort);
+ mAvailablePortPool.add(record.mPort);
+ mFdUuidMap.remove(record.mServerSocketFd);
+ }
+ }
+
+ public ServiceRecord lookupChannel(UUID uuid) {
+ ServiceRecord record = mServiceRecords.get(uuid);
+ if (record == null) {
+ LOGGER.e(String.format("Address %s: uuid %s not registered.", mAddress, uuid));
+ }
+ return record;
+ }
+
+ public UUID getUuid(FileDescriptor serverSocketFd) {
+ return mFdUuidMap.get(serverSocketFd);
+ }
+
+ private boolean isUuidRegistered(UUID uuid) {
+ if (mServiceRecords.containsKey(uuid)) {
+ LOGGER.d(String.format("Address %s: Uuid %s in use.", mAddress, uuid));
+ return true;
+ }
+ LOGGER.d(String.format("Address %s: Uuid %s not registered.", mAddress, uuid));
+ return false;
+ }
+
+ private boolean checkChannelAvailability() {
+ if (mAvailablePortPool.isEmpty()) {
+ LOGGER.e(String.format("Address %s: No available channel.", mAddress));
+ return false;
+ }
+ return true;
+ }
+
+ static class ServiceRecord {
+
+ final FileDescriptor mServerSocketFd;
+ final String mServiceName;
+ final int mPort;
+
+ ServiceRecord(String address, String serviceName, int port) {
+ mServerSocketFd = FileDescriptorFactory.getInstance().createFileDescriptor(address);
+ this.mServiceName = serviceName;
+ this.mPort = port;
+ }
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/BroadcastManager.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/BroadcastManager.java
new file mode 100644
index 0000000..0b309ae
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/BroadcastManager.java
@@ -0,0 +1,526 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.common;
+
+import android.content.BroadcastReceiver;
+import android.content.BroadcastReceiver.PendingResult;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build.VERSION;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.AsyncFunction;
+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.MoreExecutors;
+
+import org.robolectric.Shadows;
+import org.robolectric.shadows.ShadowApplication;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Manager for broadcasting of one virtual Device Shadower device.
+ *
+ * <p>Inspired by {@link ShadowApplication} and {@link LocalBroadcastManager}.
+ * <li>Broadcast permission is not supported until manifest is supported.
+ * <li>Send Broadcast is asynchronous.
+ */
+public class BroadcastManager {
+
+ private static final Logger LOGGER = Logger.create("BroadcastManager");
+
+ private static final Comparator<ReceiverRecord> RECEIVER_RECORD_COMPARATOR =
+ new Comparator<ReceiverRecord>() {
+ @Override
+ public int compare(ReceiverRecord o1, ReceiverRecord o2) {
+ return o2.mIntentFilter.getPriority() - o1.mIntentFilter.getPriority();
+ }
+ };
+
+ private final Scheduler mScheduler;
+ private final Map<String, Intent> mStickyIntents;
+
+ @GuardedBy("mRegisteredReceivers")
+ private final Map<BroadcastReceiver, Set<String>> mRegisteredReceivers;
+
+ @GuardedBy("mRegisteredReceivers")
+ private final Map<String, List<ReceiverRecord>> mActions;
+
+ public BroadcastManager(Scheduler scheduler) {
+ this(
+ scheduler,
+ new HashMap<String, Intent>(),
+ new HashMap<BroadcastReceiver, Set<String>>(),
+ new HashMap<String, List<ReceiverRecord>>());
+ }
+
+ @VisibleForTesting
+ BroadcastManager(
+ Scheduler scheduler,
+ Map<String, Intent> stickyIntents,
+ Map<BroadcastReceiver, Set<String>> registeredReceivers,
+ Map<String, List<ReceiverRecord>> actions) {
+ this.mScheduler = scheduler;
+ this.mStickyIntents = stickyIntents;
+ this.mRegisteredReceivers = registeredReceivers;
+ this.mActions = actions;
+ }
+
+ /**
+ * Registers a {@link BroadcastReceiver} with given {@link Context}.
+ *
+ * @see Context#registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)
+ */
+ @Nullable
+ public Intent registerReceiver(
+ @Nullable BroadcastReceiver receiver,
+ IntentFilter filter,
+ @Nullable String broadcastPermission,
+ @Nullable Handler handler,
+ Context context) {
+ // Ignore broadcastPermission before fully supporting manifest
+ Preconditions.checkNotNull(filter);
+ Preconditions.checkNotNull(context);
+ if (receiver != null) {
+ synchronized (mRegisteredReceivers) {
+ ReceiverRecord receiverRecord = new ReceiverRecord(receiver, filter, context,
+ handler);
+ Set<String> actionSet = mRegisteredReceivers.get(receiver);
+ if (actionSet == null) {
+ actionSet = new HashSet<>();
+ mRegisteredReceivers.put(receiver, actionSet);
+ }
+ for (int i = 0; i < filter.countActions(); i++) {
+ String action = filter.getAction(i);
+ actionSet.add(action);
+ List<ReceiverRecord> receiverRecords = mActions.get(action);
+ if (receiverRecords == null) {
+ receiverRecords = new ArrayList<>();
+ mActions.put(action, receiverRecords);
+ }
+ receiverRecords.add(receiverRecord);
+ }
+ }
+ }
+ return processStickyIntents(receiver, filter, context);
+ }
+
+ // Broadcast all sticky intents matching the given IntentFilter.
+ @SuppressWarnings("FutureReturnValueIgnored")
+ @Nullable
+ private Intent processStickyIntents(
+ @Nullable final BroadcastReceiver receiver,
+ IntentFilter intentFilter,
+ final Context context) {
+ Intent result = null;
+ final List<Intent> matchedIntents = new ArrayList<>();
+ for (Intent intent : mStickyIntents.values()) {
+ if (match(intentFilter, intent)) {
+ if (result == null) {
+ result = intent;
+ }
+ if (receiver == null) {
+ return result;
+ }
+ matchedIntents.add(intent);
+ }
+ }
+ if (!matchedIntents.isEmpty()) {
+ mScheduler.post(
+ NamedRunnable.create(
+ "Broadcast.processStickyIntents",
+ () -> {
+ for (Intent intent : matchedIntents) {
+ receiver.onReceive(context, intent);
+ }
+ }));
+ }
+ return result;
+ }
+
+ /**
+ * Unregisters a {@link BroadcastReceiver}.
+ *
+ * @see Context#unregisterReceiver(BroadcastReceiver)
+ */
+ public void unregisterReceiver(BroadcastReceiver broadcastReceiver) {
+ synchronized (mRegisteredReceivers) {
+ if (!mRegisteredReceivers.containsKey(broadcastReceiver)) {
+ LOGGER.w("Receiver not registered: " + broadcastReceiver);
+ return;
+ }
+ Set<String> actionSet = mRegisteredReceivers.remove(broadcastReceiver);
+ for (String action : actionSet) {
+ List<ReceiverRecord> receiverRecords = mActions.get(action);
+ Iterator<ReceiverRecord> iterator = receiverRecords.iterator();
+ while (iterator.hasNext()) {
+ if (iterator.next().mBroadcastReceiver == broadcastReceiver) {
+ iterator.remove();
+ }
+ }
+ if (receiverRecords.isEmpty()) {
+ mActions.remove(action);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sends sticky broadcast with given {@link Intent}. This call is asynchronous.
+ *
+ * @see Context#sendStickyBroadcast(Intent)
+ */
+ public void sendStickyBroadcast(Intent intent) {
+ mStickyIntents.put(intent.getAction(), intent);
+ sendBroadcast(intent, null /* broadcastPermission */);
+ }
+
+ /**
+ * Sends broadcast with given {@link Intent}. Receiver permission is not supported. This call is
+ * asynchronous.
+ *
+ * @see Context#sendBroadcast(Intent, String)
+ */
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void sendBroadcast(final Intent intent, @Nullable String receiverPermission) {
+ // Ignore permission matching before fully supporting manifest
+ final List<ReceiverRecord> receivers =
+ getMatchingReceivers(intent, false /* isOrdered */);
+ if (receivers.isEmpty()) {
+ return;
+ }
+ mScheduler.post(
+ NamedRunnable.create(
+ "Broadcast.sendBroadcast",
+ () -> {
+ for (ReceiverRecord receiverRecord : receivers) {
+ // Hacky: Call the shadow method, otherwise abort() NPEs after
+ // calling onReceive().
+ // TODO(b/200231384): Sending these, via context.sendBroadcast(),
+ // won't NPE...but it may not be possible on each simulated
+ // "device"'s main thread. Check if possible.
+ BroadcastReceiver broadcastReceiver =
+ receiverRecord.mBroadcastReceiver;
+ Shadows.shadowOf(broadcastReceiver)
+ .onReceive(receiverRecord.mContext, intent, /*abort=*/
+ new AtomicBoolean(false));
+ }
+ }));
+ }
+
+ /**
+ * Sends ordered broadcast with given {@link Intent}. Receiver permission is not supported. This
+ * call is asynchronous.
+ *
+ * @see Context#sendOrderedBroadcast(Intent, String)
+ */
+ public void sendOrderedBroadcast(Intent intent, @Nullable String receiverPermission) {
+ sendOrderedBroadcast(
+ intent,
+ receiverPermission,
+ null /* resultReceiver */,
+ null /* handler */,
+ 0 /* initialCode */,
+ null /* initialData */,
+ null /* initialExtras */,
+ null /* context */);
+ }
+
+ /**
+ * Sends ordered broadcast with given {@link Intent} and result {@link BroadcastReceiver}.
+ * Receiver permission is not supported. This call is asynchronous.
+ *
+ * @see Context#sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String,
+ * Bundle)
+ */
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void sendOrderedBroadcast(
+ final Intent intent,
+ @Nullable String receiverPermission,
+ @Nullable BroadcastReceiver resultReceiver,
+ @Nullable Handler handler,
+ int initialCode,
+ @Nullable String initialData,
+ @Nullable Bundle initialExtras,
+ @Nullable Context context) {
+ // Ignore permission matching before fully supporting manifest
+ final List<ReceiverRecord> receivers =
+ getMatchingReceivers(intent, true /* isOrdered */);
+ if (receivers.isEmpty()) {
+ return;
+ }
+ if (resultReceiver != null) {
+ receivers.add(
+ new ReceiverRecord(
+ resultReceiver, null /* intentFilter */, context, handler));
+ }
+ mScheduler.post(
+ NamedRunnable.create(
+ "Broadcast.sendOrderedBroadcast",
+ () -> {
+ postOrderedIntent(
+ receivers,
+ intent,
+ 0 /* initialCode */,
+ null /* initialData */,
+ null /* initialExtras */);
+ }));
+ }
+
+ @VisibleForTesting
+ void postOrderedIntent(
+ List<ReceiverRecord> receivers,
+ final Intent intent,
+ int initialCode,
+ @Nullable String initialData,
+ @Nullable Bundle initialExtras) {
+ final AtomicBoolean abort = new AtomicBoolean(false);
+ ListenableFuture<BroadcastResult> resultFuture =
+ Futures.immediateFuture(
+ new BroadcastResult(initialCode, initialData, initialExtras));
+
+ for (ReceiverRecord receiverRecord : receivers) {
+ final BroadcastReceiver receiver = receiverRecord.mBroadcastReceiver;
+ final Context context = receiverRecord.mContext;
+ resultFuture =
+ Futures.transformAsync(
+ resultFuture,
+ new AsyncFunction<BroadcastResult, BroadcastResult>() {
+ @Override
+ public ListenableFuture<BroadcastResult> apply(
+ BroadcastResult input) {
+ PendingResult result = newPendingResult(
+ input.mCode, input.mData, input.mExtras,
+ true /* isOrdered */);
+ ReflectionHelpers.callInstanceMethod(
+ receiver, "setPendingResult",
+ ClassParameter.from(PendingResult.class, result));
+ Shadows.shadowOf(receiver).onReceive(context, intent, abort);
+ return BroadcastResult.transform(result);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+ Futures.addCallback(
+ resultFuture,
+ new FutureCallback<BroadcastResult>() {
+ @Override
+ public void onSuccess(BroadcastResult result) {
+ return;
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ throw new RuntimeException(t);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private List<ReceiverRecord> getMatchingReceivers(Intent intent, boolean isOrdered) {
+ synchronized (mRegisteredReceivers) {
+ List<ReceiverRecord> result = new ArrayList<>();
+ if (!mActions.containsKey(intent.getAction())) {
+ return result;
+ }
+ Iterator<ReceiverRecord> iterator = mActions.get(intent.getAction()).iterator();
+ while (iterator.hasNext()) {
+ ReceiverRecord next = iterator.next();
+ if (match(next.mIntentFilter, intent)) {
+ result.add(next);
+ }
+ }
+ if (isOrdered) {
+ Collections.sort(result, RECEIVER_RECORD_COMPARATOR);
+ }
+ return result;
+ }
+ }
+
+ private boolean match(IntentFilter intentFilter, Intent intent) {
+ // Action test
+ if (!intentFilter.matchAction(intent.getAction())) {
+ return false;
+ }
+ // Category test
+ if (intentFilter.matchCategories(intent.getCategories()) != null) {
+ return false;
+ }
+ // Data test
+ int matchResult =
+ intentFilter.matchData(intent.getType(), intent.getScheme(), intent.getData());
+ return matchResult != IntentFilter.NO_MATCH_TYPE
+ && matchResult != IntentFilter.NO_MATCH_DATA;
+ }
+
+ private static PendingResult newPendingResult(
+ int resultCode, String resultData, Bundle resultExtras, boolean isOrdered) {
+ ClassParameter<?>[] parameters;
+ // PendingResult constructor takes different parameters in different SDK levels.
+ if (VERSION.SDK_INT < 17) {
+ parameters =
+ ClassParameter.fromComponentLists(
+ new Class<?>[]{
+ int.class,
+ String.class,
+ Bundle.class,
+ int.class,
+ boolean.class,
+ boolean.class,
+ IBinder.class
+ },
+ new Object[]{
+ resultCode,
+ resultData,
+ resultExtras,
+ 0 /* type */,
+ isOrdered,
+ false /* sticky */,
+ null /* IBinder */
+ });
+ } else if (VERSION.SDK_INT < 23) {
+ parameters =
+ ClassParameter.fromComponentLists(
+ new Class<?>[]{
+ int.class,
+ String.class,
+ Bundle.class,
+ int.class,
+ boolean.class,
+ boolean.class,
+ IBinder.class,
+ int.class
+ },
+ new Object[]{
+ resultCode,
+ resultData,
+ resultExtras,
+ 0 /* type */,
+ isOrdered,
+ false /* sticky */,
+ null /* IBinder */,
+ 0 /* userId */
+ });
+ } else {
+ parameters =
+ ClassParameter.fromComponentLists(
+ new Class<?>[]{
+ int.class,
+ String.class,
+ Bundle.class,
+ int.class,
+ boolean.class,
+ boolean.class,
+ IBinder.class,
+ int.class,
+ int.class
+ },
+ new Object[]{
+ resultCode,
+ resultData,
+ resultExtras,
+ 0 /* type */,
+ isOrdered,
+ false /* sticky */,
+ null /* IBinder */,
+ 0 /* userId */,
+ 0 /* flags */
+ });
+ }
+ return ReflectionHelpers.callConstructor(PendingResult.class, parameters);
+ }
+
+ /**
+ * Holder of broadcast result from previous receiver.
+ */
+ private static final class BroadcastResult {
+
+ private final int mCode;
+ private final String mData;
+ private final Bundle mExtras;
+
+ BroadcastResult(int code, String data, Bundle extras) {
+ this.mCode = code;
+ this.mData = data;
+ this.mExtras = extras;
+ }
+
+ private static ListenableFuture<BroadcastResult> transform(PendingResult result) {
+ return Futures.transform(
+ Shadows.shadowOf(result).getFuture(),
+ new Function<PendingResult, BroadcastResult>() {
+ @Override
+ public BroadcastResult apply(PendingResult input) {
+ return new BroadcastResult(
+ input.getResultCode(), input.getResultData(),
+ input.getResultExtras(false));
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+ }
+
+ /**
+ * Information of a registered BroadcastReceiver.
+ */
+ @VisibleForTesting
+ static final class ReceiverRecord {
+
+ final BroadcastReceiver mBroadcastReceiver;
+ final IntentFilter mIntentFilter;
+ final Context mContext;
+ final Handler mHandler;
+
+ @VisibleForTesting
+ ReceiverRecord(
+ BroadcastReceiver broadcastReceiver,
+ IntentFilter intentFilter,
+ Context context,
+ Handler handler) {
+ this.mBroadcastReceiver = broadcastReceiver;
+ this.mIntentFilter = intentFilter;
+ this.mContext = context;
+ this.mHandler = handler;
+ }
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/ContentDatabase.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/ContentDatabase.java
new file mode 100644
index 0000000..1f4d778
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/ContentDatabase.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.common;
+
+import android.database.Cursor;
+
+import org.robolectric.fakes.RoboCursor;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Simulate Sqlite database for Android content provider.
+ */
+public class ContentDatabase {
+
+ private final List<String> mColumnNames;
+ private final List<List<Object>> mData;
+
+ public ContentDatabase(String... names) {
+ mColumnNames = Arrays.asList(names);
+ mData = new ArrayList<>();
+ }
+
+ public void addData(Object... items) {
+ mData.add(Arrays.asList(items));
+ }
+
+ public Cursor getCursor() {
+ RoboCursor cursor = new RoboCursor();
+ cursor.setColumnNames(mColumnNames);
+ Object[][] dataArr = new Object[mData.size()][mColumnNames.size()];
+ for (int i = 0; i < mData.size(); i++) {
+ dataArr[i] = new Object[mColumnNames.size()];
+ mData.get(i).toArray(dataArr[i]);
+ }
+ cursor.setResults(dataArr);
+ return cursor;
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/Interrupter.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/Interrupter.java
new file mode 100644
index 0000000..66e9cb0
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/Interrupter.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.common;
+
+import com.android.libraries.testing.deviceshadower.Enums;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Interrupter sets and checks interruptible point, and interrupt operation by throwing
+ * IOException.
+ */
+public class Interrupter {
+
+ private final InheritableThreadLocal<Integer> mCurrentIdentifier;
+ private int mInterruptIdentifier;
+
+ private final Set<Enums.Operation> mInterruptOperations = new HashSet<>();
+
+ public Interrupter() {
+ mCurrentIdentifier = new InheritableThreadLocal<Integer>() {
+ @Override
+ protected Integer initialValue() {
+ return -1;
+ }
+ };
+ }
+
+ public void checkInterrupt() throws IOException {
+ if (mCurrentIdentifier.get() == mInterruptIdentifier) {
+ throw new IOException(
+ "Bluetooth interrupted at identifier: " + mCurrentIdentifier.get());
+ }
+ }
+
+ public void setInterruptible(int identifier) {
+ mCurrentIdentifier.set(identifier);
+ }
+
+ public void interrupt(int identifier) {
+ mInterruptIdentifier = identifier;
+ }
+
+ public void addInterruptOperation(Enums.Operation operation) {
+ mInterruptOperations.add(operation);
+ }
+
+ public boolean shouldInterrupt(Enums.Operation operation) {
+ return mInterruptOperations.contains(operation);
+ }
+
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/NamedRunnable.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/NamedRunnable.java
new file mode 100644
index 0000000..4e84d71
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/NamedRunnable.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.common;
+
+/**
+ * Runnable with a name defined.
+ */
+public abstract class NamedRunnable implements Runnable {
+
+ private final String mName;
+
+ private NamedRunnable(String name) {
+ this.mName = name;
+ }
+
+ public static NamedRunnable create(String name, Runnable runnable) {
+ return new NamedRunnable(name) {
+ @Override
+ public void run() {
+ runnable.run();
+ }
+ };
+ }
+
+ @Override
+ public String toString() {
+ return mName;
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/Scheduler.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/Scheduler.java
new file mode 100644
index 0000000..96e9b15
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/common/Scheduler.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.common;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Scheduler to post runnables to a single thread.
+ */
+public class Scheduler {
+
+ private static final Logger LOGGER = Logger.create("Scheduler");
+
+ @GuardedBy("Scheduler.class")
+ private static int sTotalRunnables = 0;
+
+ private static CountDownLatch sCompleteLatch;
+
+ public Scheduler() {
+ this(null);
+ }
+
+ public Scheduler(String name) {
+ mExecutor =
+ Executors.newSingleThreadExecutor(
+ r -> {
+ Thread thread = Executors.defaultThreadFactory().newThread(r);
+ if (name != null) {
+ thread.setName(name);
+ }
+ return thread;
+ });
+ }
+
+ public static boolean await(long timeoutMillis) throws InterruptedException {
+
+ synchronized (Scheduler.class) {
+ if (isComplete()) {
+ return true;
+ }
+ if (sCompleteLatch == null) {
+ sCompleteLatch = new CountDownLatch(1);
+ }
+ }
+
+ // TODO(b/200231384): solve potential NPE caused by race condition.
+ boolean result = sCompleteLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
+ synchronized (Scheduler.class) {
+ sCompleteLatch = null;
+ }
+ return result;
+ }
+
+ private final ExecutorService mExecutor;
+
+ @GuardedBy("this")
+ private final List<ScheduledRunnable> mRunnables = new ArrayList<>();
+
+ @GuardedBy("this")
+ private long mCurrentTimeMillis = 0;
+
+ @GuardedBy("this")
+ private List<ScheduledRunnable> mRunningRunnables = new ArrayList<>();
+
+ /**
+ * Post a {@link NamedRunnable} to scheduler.
+ *
+ * <p>Return value can be ignored because exception will be handled by {@link
+ * DeviceShadowEnvironmentImpl#catchInternalException}.
+ */
+ // @CanIgnoreReturnValue
+ public synchronized Future<?> post(NamedRunnable r) {
+ synchronized (Scheduler.class) {
+ sTotalRunnables++;
+ }
+ advance(0);
+ return mExecutor.submit(new ScheduledRunnable(r, mCurrentTimeMillis).mRunnable);
+ }
+
+ public synchronized void post(NamedRunnable r, long delayMillis) {
+ synchronized (Scheduler.class) {
+ sTotalRunnables++;
+ }
+ addRunnables(new ScheduledRunnable(r, mCurrentTimeMillis + delayMillis));
+ advance(0);
+ }
+
+ public synchronized void shutdown() {
+ mExecutor.shutdown();
+ }
+
+ @VisibleForTesting
+ synchronized void advance(long durationMillis) {
+ mCurrentTimeMillis += durationMillis;
+ while (mRunnables.size() > 0) {
+ ScheduledRunnable r = mRunnables.get(0);
+ if (r.mTimeMillis <= mCurrentTimeMillis) {
+ mRunnables.remove(0);
+ mExecutor.execute(r.mRunnable);
+ } else {
+ break;
+ }
+ }
+ }
+
+ private synchronized void addRunnables(ScheduledRunnable r) {
+ int index = 0;
+ while (index < mRunnables.size() && mRunnables.get(index).mTimeMillis <= r.mTimeMillis) {
+ index++;
+ }
+ mRunnables.add(index, r);
+ }
+
+ @VisibleForTesting
+ static synchronized boolean isComplete() {
+ return sTotalRunnables == 0;
+ }
+
+ // Can only be called by DeviceShadowEnvironmentImpl when reset.
+ public static synchronized void clear() {
+ sTotalRunnables = 0;
+ }
+
+ class ScheduledRunnable {
+
+ final NamedRunnable mRunnable;
+ final long mTimeMillis;
+
+ ScheduledRunnable(final NamedRunnable r, long timeMillis) {
+ this.mTimeMillis = timeMillis;
+ this.mRunnable =
+ NamedRunnable.create(
+ r.toString(),
+ () -> {
+ synchronized (Scheduler.this) {
+ Scheduler.this.mRunningRunnables.add(ScheduledRunnable.this);
+ }
+
+ try {
+ r.run();
+ } catch (Exception e) {
+ LOGGER.e("Error in scheduler runnable " + r, e);
+ DeviceShadowEnvironmentImpl.catchInternalException(e);
+ }
+
+ synchronized (Scheduler.this) {
+ // Remove the last one.
+ Scheduler.this.mRunningRunnables.remove(
+ Scheduler.this.mRunningRunnables.size() - 1);
+ }
+
+ // If this is last runnable,
+ // When this section runs before await:
+ // totalRunnable will be 0, await will return directly.
+ // When this section runs after await:
+ // latch will not be null, count down will terminate await.
+
+ // TODO(b/200231384): when there are two threads running at same
+ // time, there will be a case when totalRunnable is 0, but another
+ // thread pending to acquire Scheduler.class lock to post a
+ // runnable. Hence, await here might not be correct in this case.
+ synchronized (Scheduler.class) {
+ sTotalRunnables--;
+ if (isComplete()) {
+ if (sCompleteLatch != null) {
+ sCompleteLatch.countDown();
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public String toString() {
+ return mRunnable.toString();
+ }
+ }
+
+ @Override
+ public synchronized String toString() {
+ return String.format(
+ "\t%d scheduled runnables %s\n\t%d still running or aborted %s",
+ mRunnables.size(), mRunnables, mRunningRunnables.size(), mRunningRunnables);
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/nfc/INfcAdapterImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/nfc/INfcAdapterImpl.java
new file mode 100644
index 0000000..01dcac2
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/nfc/INfcAdapterImpl.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.nfc;
+
+import android.nfc.IAppCallback;
+import android.nfc.INfcAdapter;
+
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+
+/**
+ * Implementation of INfcAdapter
+ */
+public class INfcAdapterImpl implements INfcAdapter {
+
+ public INfcAdapterImpl() {
+ }
+
+ @Override
+ public void setAppCallback(IAppCallback callback) {
+ DeviceShadowEnvironmentImpl.getLocalNfcletImpl().mAppCallback = callback;
+ }
+
+ @Override
+ public boolean enable() {
+ return DeviceShadowEnvironmentImpl.getLocalNfcletImpl().enable();
+ }
+
+ @Override
+ public boolean disable(boolean saveState) {
+ // We do not need to save state because test only run once.
+ return DeviceShadowEnvironmentImpl.getLocalNfcletImpl().disable();
+ }
+
+ @Override
+ public int getState() {
+ return DeviceShadowEnvironmentImpl.getLocalNfcletImpl().getState();
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/nfc/NfcletImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/nfc/NfcletImpl.java
new file mode 100644
index 0000000..137f6b8
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/nfc/NfcletImpl.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.nfc;
+
+import android.content.Intent;
+import android.nfc.BeamShareData;
+import android.nfc.IAppCallback;
+import android.nfc.NdefMessage;
+import android.nfc.NfcAdapter;
+
+import com.android.libraries.testing.deviceshadower.Enums.NfcOperation;
+import com.android.libraries.testing.deviceshadower.Nfclet;
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+import com.android.libraries.testing.deviceshadower.internal.common.Interrupter;
+import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Implementation of Nfclet.
+ */
+public class NfcletImpl implements Nfclet {
+
+ private static final Logger LOGGER = Logger.create("NfcletImpl");
+
+ IAppCallback mAppCallback;
+ private final Interrupter mInterrupter;
+
+ @GuardedBy("this")
+ private int mCurrentState;
+
+ public NfcletImpl() {
+ mInterrupter = new Interrupter();
+ mCurrentState = NfcAdapter.STATE_OFF;
+ }
+
+ public void onNear(NfcletImpl remote) {
+ if (remote.mAppCallback != null) {
+ LOGGER.v("NFC receiver get beam share data from remote");
+ BeamShareData data = remote.mAppCallback.createBeamShareData();
+ DeviceShadowEnvironmentImpl.getLocalDeviceletImpl().getBroadcastManager()
+ .sendBroadcast(createNdefDiscoveredIntent(data), null);
+ }
+ if (mAppCallback != null) {
+ LOGGER.v("NFC sender onNdefPushComplete");
+ mAppCallback.onNdefPushComplete();
+ }
+ }
+
+ public synchronized int getState() {
+ return mCurrentState;
+ }
+
+ public boolean enable() {
+ if (shouldInterrupt(NfcOperation.ENABLE)) {
+ return false;
+ }
+ LOGGER.v("Enable NFC Adapter");
+ updateState(NfcAdapter.STATE_TURNING_ON);
+ updateState(NfcAdapter.STATE_ON);
+ return true;
+ }
+
+ public boolean disable() {
+ if (shouldInterrupt(NfcOperation.DISABLE)) {
+ return false;
+ }
+ LOGGER.v("Disable NFC Adapter");
+ updateState(NfcAdapter.STATE_TURNING_OFF);
+ updateState(NfcAdapter.STATE_OFF);
+ return true;
+ }
+
+ @Override
+ public synchronized Nfclet setInitialState(int state) {
+ mCurrentState = state;
+ return this;
+ }
+
+ @Override
+ public Nfclet setInterruptOperation(NfcOperation operation) {
+ mInterrupter.addInterruptOperation(operation);
+ return this;
+ }
+
+ public boolean shouldInterrupt(NfcOperation operation) {
+ return mInterrupter.shouldInterrupt(operation);
+ }
+
+ private synchronized void updateState(int state) {
+ if (mCurrentState != state) {
+ mCurrentState = state;
+ DeviceShadowEnvironmentImpl.getLocalDeviceletImpl().getBroadcastManager()
+ .sendBroadcast(createAdapterStateChangedIntent(state), null);
+ }
+ }
+
+ private Intent createAdapterStateChangedIntent(int state) {
+ Intent intent = new Intent(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED);
+ intent.putExtra(NfcAdapter.EXTRA_ADAPTER_STATE, state);
+ return intent;
+ }
+
+ private Intent createNdefDiscoveredIntent(BeamShareData data) {
+ Intent intent = new Intent();
+ intent.setAction(NfcAdapter.ACTION_NDEF_DISCOVERED);
+ intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, new NdefMessage[]{data.ndefMessage});
+ // TODO(b/200231384): uncomment when uri and mime type implemented.
+ // ndefUri = message.getRecords()[0].toUri();
+ // ndefMimeType = message.getRecords()[0].toMimeType();
+ return intent;
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/sms/SmsContentProvider.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/sms/SmsContentProvider.java
new file mode 100644
index 0000000..6bc535b
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/sms/SmsContentProvider.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.sms;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+
+/**
+ * Content provider for SMS query.
+ */
+public class SmsContentProvider extends ContentProvider {
+
+ public SmsContentProvider() {
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(
+ Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ return DeviceShadowEnvironmentImpl.getLocalSmsletImpl().getCursor(uri);
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/sms/SmsletImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/sms/SmsletImpl.java
new file mode 100644
index 0000000..00a581e
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/sms/SmsletImpl.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.sms;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Telephony;
+
+import com.android.libraries.testing.deviceshadower.Smslet;
+import com.android.libraries.testing.deviceshadower.internal.common.ContentDatabase;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Implementation of SMS functionality.
+ */
+public class SmsletImpl implements Smslet {
+
+ private final Map<Uri, ContentDatabase> mUriToDataMap;
+
+ public SmsletImpl() {
+ mUriToDataMap = new HashMap<>();
+ mUriToDataMap.put(
+ Telephony.Sms.Inbox.CONTENT_URI, new ContentDatabase(Telephony.Sms.Inbox.BODY));
+ mUriToDataMap.put(Telephony.Sms.Sent.CONTENT_URI,
+ new ContentDatabase(Telephony.Sms.Inbox.BODY));
+ // TODO(b/200231384): implement Outbox, Intents, Conversations.
+ }
+
+ @Override
+ public Smslet addSms(Uri contentUri, String body) {
+ mUriToDataMap.get(contentUri).addData(body);
+ return this;
+ }
+
+ public Cursor getCursor(Uri uri) {
+ return mUriToDataMap.get(uri).getCursor();
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/GattHelper.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/GattHelper.java
new file mode 100644
index 0000000..f45b125
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/GattHelper.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.utils;
+
+import android.bluetooth.le.AdvertiseData;
+import android.os.ParcelUuid;
+import android.util.SparseArray;
+
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.BluetoothConstants;
+
+import com.google.common.io.ByteArrayDataOutput;
+import com.google.common.io.ByteStreams;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.UUID;
+
+/**
+ * Helper class for Gatt functionality.
+ */
+public class GattHelper {
+
+ public static byte[] convertAdvertiseData(
+ AdvertiseData data, int txPowerLevel, String localName, boolean isConnectable) {
+ if (data == null) {
+ return new byte[0];
+ }
+ ByteArrayDataOutput result = ByteStreams.newDataOutput();
+ if (isConnectable) {
+ writeDataUnit(
+ result,
+ BluetoothConstants.DATA_TYPE_FLAGS,
+ new byte[]{BluetoothConstants.FLAGS_IN_CONNECTABLE_PACKETS});
+ }
+ // tx power level is signed 8-bit int, range -100 to 20.
+ if (data.getIncludeTxPowerLevel()) {
+ writeDataUnit(
+ result,
+ BluetoothConstants.DATA_TYPE_TX_POWER_LEVEL,
+ new byte[]{(byte) txPowerLevel});
+ }
+ // Local name
+ if (data.getIncludeDeviceName()) {
+ writeDataUnit(
+ result,
+ BluetoothConstants.DATA_TYPE_LOCAL_NAME_COMPLETE,
+ localName.getBytes(Charset.defaultCharset()));
+ }
+ // Manufacturer data
+ SparseArray<byte[]> manufacturerData = data.getManufacturerSpecificData();
+ for (int i = 0; i < manufacturerData.size(); i++) {
+ int manufacturerId = manufacturerData.keyAt(i);
+ writeDataUnit(
+ result,
+ BluetoothConstants.DATA_TYPE_MANUFACTURER_SPECIFIC_DATA,
+ parseManufacturerData(manufacturerId, manufacturerData.get(manufacturerId))
+ );
+ }
+ // Service data
+ Map<ParcelUuid, byte[]> serviceData = data.getServiceData();
+ for (Entry<ParcelUuid, byte[]> entry : serviceData.entrySet()) {
+ writeDataUnit(
+ result,
+ BluetoothConstants.DATA_TYPE_SERVICE_DATA,
+ parseServiceData(entry.getKey().getUuid(), entry.getValue())
+ );
+ }
+ // Service UUID, 128-bit UUID in little endian
+ if (data.getServiceUuids() != null && !data.getServiceUuids().isEmpty()) {
+ ByteBuffer uuidBytes =
+ ByteBuffer.allocate(data.getServiceUuids().size() * 16)
+ .order(ByteOrder.LITTLE_ENDIAN);
+ for (ParcelUuid parcelUuid : data.getServiceUuids()) {
+ UUID uuid = parcelUuid.getUuid();
+ uuidBytes.putLong(uuid.getLeastSignificantBits())
+ .putLong(uuid.getMostSignificantBits());
+ }
+ writeDataUnit(
+ result,
+ BluetoothConstants.DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE,
+ uuidBytes.array()
+ );
+ }
+ return result.toByteArray();
+ }
+
+ private static byte[] parseServiceData(UUID uuid, byte[] serviceData) {
+ // First two bytes of the data are data UUID in little endian
+ int length = 2 + serviceData.length;
+ byte[] result = new byte[length];
+ // extract 16-bit UUID value
+ int uuidValue = (int) ((uuid.getMostSignificantBits() & 0x0000FFFF00000000L) >>> 32);
+ result[0] = (byte) (uuidValue & 0xFF);
+ result[1] = (byte) ((uuidValue >> 8) & 0xFF);
+ System.arraycopy(serviceData, 0, result, 2, serviceData.length);
+ return result;
+
+ }
+
+ private static byte[] parseManufacturerData(int manufacturerId, byte[] manufacturerData) {
+ // First two bytes are manufacturer id in little endian.
+ int length = 2 + manufacturerData.length;
+ byte[] result = new byte[length];
+ result[0] = (byte) (manufacturerId & 0xFF);
+ result[1] = (byte) ((manufacturerId >> 8) & 0xFF);
+ System.arraycopy(manufacturerData, 0, result, 2, manufacturerData.length);
+ return result;
+ }
+
+ private static void writeDataUnit(ByteArrayDataOutput output, int type, byte[] data) {
+ // Length includes the length of the field type, which is 1 byte.
+ int length = 1 + data.length;
+ // Length and type are unsigned 8-bit int. Assume the values are valid.
+ output.write(length);
+ output.write(type);
+ output.write(data);
+ }
+
+ private GattHelper() {
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/Logger.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/Logger.java
new file mode 100644
index 0000000..31f7202
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/Logger.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.utils;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * Logger class to provide formatted log for Device Shadower.
+ *
+ * <p>Log is formatted as "[TAG] [Keyword1, Keyword2 ...] Log Message Body".</p>
+ */
+public class Logger {
+
+ private static final String TAG = "DeviceShadower";
+
+ private final String mTag;
+ private final String mPrefix;
+
+ public Logger(String tag, String... keywords) {
+ mTag = tag;
+ mPrefix = buildPrefix(keywords);
+ }
+
+ public static Logger create(String... keywords) {
+ return new Logger(TAG, keywords);
+ }
+
+ private static String buildPrefix(String... keywords) {
+ if (keywords.length == 0) {
+ return "";
+ }
+ return String.format(" [%s] ", TextUtils.join(", ", keywords));
+ }
+
+ /**
+ * @see Log#e(String, String)
+ */
+ public void e(String msg) {
+ Log.e(mTag, format(msg));
+ }
+
+ /**
+ * @see Log#e(String, String, Throwable)
+ */
+ public void e(String msg, Throwable throwable) {
+ Log.e(mTag, format(msg), throwable);
+ }
+
+ /**
+ * @see Log#d(String, String)
+ */
+ public void d(String msg) {
+ Log.d(mTag, format(msg));
+ }
+
+ /**
+ * @see Log#d(String, String, Throwable)
+ */
+ public void d(String msg, Throwable throwable) {
+ Log.d(mTag, format(msg), throwable);
+ }
+
+ /**
+ * @see Log#i(String, String)
+ */
+ public void i(String msg) {
+ Log.i(mTag, format(msg));
+ }
+
+ /**
+ * @see Log#i(String, String, Throwable)
+ */
+ public void i(String msg, Throwable throwable) {
+ Log.i(mTag, format(msg), throwable);
+ }
+
+ /**
+ * @see Log#v(String, String)
+ */
+ public void v(String msg) {
+ Log.v(mTag, format(msg));
+ }
+
+ /**
+ * @see Log#v(String, String, Throwable)
+ */
+ public void v(String msg, Throwable throwable) {
+ Log.v(mTag, format(msg), throwable);
+ }
+
+ /**
+ * @see Log#w(String, String)
+ */
+ public void w(String msg) {
+ Log.w(mTag, format(msg));
+ }
+
+ /**
+ * @see Log#w(String, Throwable)
+ */
+ public void w(Throwable throwable) {
+ Log.w(mTag, null, throwable);
+ }
+
+ /**
+ * @see Log#w(String, String, Throwable)
+ */
+ public void w(String msg, Throwable throwable) {
+ Log.w(mTag, format(msg), throwable);
+ }
+
+ /**
+ * @see Log#wtf(String, String)
+ */
+ public void wtf(String msg) {
+ Log.wtf(mTag, format(msg));
+ }
+
+ /**
+ * @see Log#wtf(String, String, Throwable)
+ */
+ public void wtf(String msg, Throwable throwable) {
+ Log.wtf(mTag, format(msg), throwable);
+ }
+
+ /**
+ * @see Log#isLoggable(String, int)
+ */
+ public boolean isLoggable(int level) {
+ return Log.isLoggable(mTag, level);
+ }
+
+ /**
+ * @see Log#println(int, String, String)
+ */
+ public int println(int priority, String msg) {
+ return Log.println(priority, mTag, format(msg));
+ }
+
+ private String format(String msg) {
+ return mPrefix + msg;
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/MacAddressGenerator.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/MacAddressGenerator.java
new file mode 100644
index 0000000..f8d3193
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/internal/utils/MacAddressGenerator.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.internal.utils;
+
+import android.bluetooth.BluetoothAdapter;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Locale;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * A class which generates and converts valid Bluetooth MAC addresses.
+ */
+public class MacAddressGenerator {
+
+ @GuardedBy("MacAddressGenerator.class")
+ private static MacAddressGenerator sInstance = new MacAddressGenerator();
+
+ @VisibleForTesting
+ public static synchronized void setInstanceForTest(MacAddressGenerator generator) {
+ sInstance = generator;
+ }
+
+ public static synchronized MacAddressGenerator get() {
+ return sInstance;
+ }
+
+ private long mLastAddress = 0x0L;
+
+ private MacAddressGenerator() {
+ }
+
+ public String generateMacAddress() {
+ byte[] bytes = generateMacAddressBytes();
+ return convertByteMacAddress(bytes);
+ }
+
+ public byte[] generateMacAddressBytes() {
+ long addr = mLastAddress++;
+ byte[] bytes = new byte[6];
+ for (int i = 5; i >= 0; i--) {
+ bytes[i] = (byte) (addr & 0xFF);
+ addr = addr >> 8;
+ }
+ return bytes;
+ }
+
+ public static byte[] convertStringMacAddress(String address) {
+ if (!BluetoothAdapter.checkBluetoothAddress(address)) {
+ throw new IllegalArgumentException("Not a valid bluetooth mac hex string: " + address);
+ }
+ byte[] bytes = new byte[6];
+ String[] macValues = address.split(":");
+ for (int i = 0; i < bytes.length; i++) {
+ bytes[i] = Integer.decode("0x" + macValues[i]).byteValue();
+ }
+ return bytes;
+ }
+
+ public static String convertByteMacAddress(byte[] address) {
+ if (address == null || address.length != 6) {
+ throw new IllegalArgumentException("Bluetooth address must have 6 bytes");
+ }
+ return String.format(Locale.US, "%02X:%02X:%02X:%02X:%02X:%02X",
+ address[0], address[1], address[2], address[3], address[4], address[5]);
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothA2dp.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothA2dp.java
new file mode 100644
index 0000000..344103b
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothA2dp.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.shadows.bluetooth;
+
+import static com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl.getBlueletImpl;
+import static com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl.getLocalBlueletImpl;
+
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProfile.ServiceListener;
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.BlueletImpl;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Shadow of the Bluetooth A2DP service.
+ */
+@Implements(BluetoothA2dp.class)
+public class ShadowBluetoothA2dp {
+
+ /**
+ * Hidden in {@link BluetoothProfile}.
+ */
+ public static final int A2DP_SINK = 11;
+
+ private final Map<BluetoothDevice, Integer> mDeviceToConnectionState = new HashMap<>();
+ private Context mContext;
+ @RealObject
+ private BluetoothA2dp mRealObject;
+
+ public void __constructor__(Context context, ServiceListener l) {
+ this.mContext = context;
+ l.onServiceConnected(BluetoothProfile.A2DP, mRealObject);
+ }
+
+ @Implementation
+ public List<BluetoothDevice> getConnectedDevices() {
+ List<BluetoothDevice> result = new ArrayList<>();
+ for (BluetoothDevice device : mDeviceToConnectionState.keySet()) {
+ if (getConnectionState(device) == BluetoothProfile.STATE_CONNECTED) {
+ result.add(device);
+ }
+ }
+ return result;
+ }
+
+ @Implementation
+ public int getConnectionState(BluetoothDevice device) {
+ return mDeviceToConnectionState.containsKey(device)
+ ? mDeviceToConnectionState.get(device)
+ : BluetoothProfile.STATE_DISCONNECTED;
+ }
+
+ @Implementation
+ public boolean connect(BluetoothDevice device) {
+ setConnectionState(BluetoothProfile.STATE_CONNECTING, device);
+ // Only successfully connect if the device is in the environment (i.e. nearby) and accepts
+ // connections.
+ BlueletImpl blueLet = getBlueletImpl(device.getAddress());
+ if (blueLet != null && !blueLet.getRefuseConnections()) {
+ setConnectionState(BluetoothProfile.STATE_CONNECTED, device);
+ } else {
+ // If the device isn't in the environment, still return true (no immediate failure, i.e.
+ // we're trying to connect) but send CONNECTING -> DISCONNECTED (like the OS does).
+ setConnectionState(BluetoothProfile.STATE_DISCONNECTED, device);
+ }
+ return true;
+ }
+
+ @Implementation
+ public void close() {
+ }
+
+ private void setConnectionState(int state, BluetoothDevice device) {
+ int previousState = getConnectionState(device);
+ mDeviceToConnectionState.put(device, state);
+
+ getLocalBlueletImpl()
+ .setProfileConnectionState(BluetoothProfile.A2DP, state, device.getAddress());
+ BlueletImpl remoteDevice = getBlueletImpl(device.getAddress());
+ if (remoteDevice != null) {
+ remoteDevice.setProfileConnectionState(A2DP_SINK, state, getLocalBlueletImpl().address);
+ }
+
+ mContext.sendBroadcast(
+ new Intent(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)
+ .putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, previousState)
+ .putExtra(BluetoothProfile.EXTRA_STATE, state)
+ .putExtra(BluetoothDevice.EXTRA_DEVICE, device));
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothAdapter.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothAdapter.java
new file mode 100644
index 0000000..394afbc
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothAdapter.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.shadows.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.content.AttributionSource;
+
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.BlueletImpl;
+import com.android.libraries.testing.deviceshadower.internal.utils.MacAddressGenerator;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+
+/**
+ * Shadow of {@link BluetoothAdapter} to be used with Device Shadower in Robolectric test.
+ */
+@Implements(BluetoothAdapter.class)
+public class ShadowBluetoothAdapter {
+
+ @RealObject
+ BluetoothAdapter mRealAdapter;
+
+ public ShadowBluetoothAdapter() {
+ }
+
+ @Implementation
+ public static synchronized BluetoothAdapter getDefaultAdapter() {
+ // Add a device and set local devicelet in case no local bluelet set
+ if (!DeviceShadowEnvironmentImpl.hasLocalDeviceletImpl()) {
+ String address = MacAddressGenerator.get().generateMacAddress();
+ DeviceShadowEnvironmentImpl.addDevice(address);
+ DeviceShadowEnvironmentImpl.setLocalDevice(address);
+ }
+ BlueletImpl localBluelet = DeviceShadowEnvironmentImpl.getLocalBlueletImpl();
+ return localBluelet.getAdapter();
+ }
+
+ @Implementation
+ public static BluetoothAdapter createAdapter(AttributionSource attributionSource) {
+ // Add a device and set local devicelet in case no local bluelet set
+ if (!DeviceShadowEnvironmentImpl.hasLocalDeviceletImpl()) {
+ String address = MacAddressGenerator.get().generateMacAddress();
+ DeviceShadowEnvironmentImpl.addDevice(address);
+ DeviceShadowEnvironmentImpl.setLocalDevice(address);
+ }
+ BlueletImpl localBluelet = DeviceShadowEnvironmentImpl.getLocalBlueletImpl();
+ return localBluelet.getAdapter();
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothDevice.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothDevice.java
new file mode 100644
index 0000000..247f46e
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothDevice.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.shadows.bluetooth;
+
+import android.bluetooth.BluetoothDevice;
+
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.IBluetoothImpl;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Placeholder for BluetoothDevice improvements
+ */
+@Implements(BluetoothDevice.class)
+public class ShadowBluetoothDevice {
+
+ @RealObject
+ private BluetoothDevice mBluetoothDevice;
+ private static final Map<String, Integer> sBondTransport = new HashMap<>();
+ private static Map<String, Boolean> sPairingConfirmation = new HashMap<>();
+
+ public ShadowBluetoothDevice() {
+ }
+
+ @Implementation
+ public boolean setPasskey(int passkey) {
+ return new IBluetoothImpl().setPasskey(mBluetoothDevice, passkey);
+ }
+
+ @Implementation
+ public boolean createBond(int transport) {
+ sBondTransport.put(mBluetoothDevice.getAddress(), transport);
+ return Shadow.directlyOn(
+ mBluetoothDevice,
+ BluetoothDevice.class,
+ "createBond",
+ ClassParameter.from(int.class, transport));
+ }
+
+ public static int getBondTransport(String address) {
+ return sBondTransport.containsKey(address)
+ ? sBondTransport.get(address)
+ : BluetoothDevice.TRANSPORT_AUTO;
+ }
+
+ @Implementation
+ public boolean setPairingConfirmation(boolean confirm) {
+ sPairingConfirmation.put(mBluetoothDevice.getAddress(), confirm);
+ return Shadow.directlyOn(
+ mBluetoothDevice,
+ BluetoothDevice.class,
+ "setPairingConfirmation",
+ ClassParameter.from(boolean.class, confirm));
+ }
+
+ /**
+ * Gets the confirmation value previously set with a call to {@link
+ * BluetoothDevice#setPairingConfirmation(boolean)}. Default is false.
+ */
+ public static boolean getPairingConfirmation(String address) {
+ return sPairingConfirmation.containsKey(address) && sPairingConfirmation.get(address);
+ }
+
+ /**
+ * Resets the confirmation values.
+ */
+ public static void resetPairingConfirmation() {
+ sPairingConfirmation = new HashMap<>();
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothLeScanner.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothLeScanner.java
new file mode 100644
index 0000000..1f7da14
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothLeScanner.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.shadows.bluetooth;
+
+import android.bluetooth.le.BluetoothLeScanner;
+
+import org.robolectric.annotation.Implements;
+
+/**
+ * Shadow of {@link BluetoothLeScanner} to be used with Device Shadower in Robolectric test.
+ */
+@Implements(BluetoothLeScanner.class)
+public class ShadowBluetoothLeScanner {
+
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothServerSocket.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothServerSocket.java
new file mode 100644
index 0000000..bffcf32
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothServerSocket.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.shadows.bluetooth;
+
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
+import android.net.LocalSocket;
+import android.os.ParcelFileDescriptor;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.RfcommDelegate;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * Placeholder for BluetoothServerSocket updates
+ */
+@Implements(BluetoothServerSocket.class)
+public class ShadowBluetoothServerSocket {
+
+ @RealObject
+ BluetoothServerSocket mRealServerSocket;
+
+ public ShadowBluetoothServerSocket() {
+ }
+
+ @Implementation
+ public BluetoothSocket accept(int timeout) throws IOException {
+ FileDescriptor serverSocketFd = getServerSocketFileDescriptor();
+ if (serverSocketFd == null) {
+ throw new IOException("socket is closed.");
+ }
+ RfcommDelegate local = getLocalRfcommDelegate();
+ local.checkInterrupt();
+ FileDescriptor clientFd = local.processNextConnectionRequest(serverSocketFd);
+ // configure the LocalSocket of the BluetoothServerSocket
+ BluetoothSocket internalSocket = ReflectionHelpers.getField(mRealServerSocket, "mSocket");
+ ShadowLocalSocket internalLocalSocket = getLocalSocketShadow(internalSocket);
+ internalLocalSocket.setAncillaryFd(local.getServerFd(clientFd));
+
+ // call original method
+ BluetoothSocket socket = Shadow.directlyOn(mRealServerSocket, BluetoothServerSocket.class,
+ "accept", ClassParameter.from(int.class, timeout));
+
+ // setup local socket of the returned BluetoothSocket
+ String remoteAddress = socket.getRemoteDevice().getAddress();
+ ShadowLocalSocket shadowLocalSocket = getLocalSocketShadow(socket);
+ shadowLocalSocket.setRemoteAddress(remoteAddress);
+ // init connection to client
+ local.initiateConnectToClient(clientFd, getPort());
+ local.waitForConnectionEstablished(clientFd);
+ return socket;
+ }
+
+ @Implementation
+ public void close() throws IOException {
+ getLocalRfcommDelegate().closeServerSocket(getServerSocketFileDescriptor());
+ Shadow.directlyOn(mRealServerSocket, BluetoothServerSocket.class, "close");
+ }
+
+ @VisibleForTesting
+ FileDescriptor getServerSocketFileDescriptor() {
+ BluetoothSocket socket = ReflectionHelpers.getField(mRealServerSocket, "mSocket");
+ ParcelFileDescriptor pfd = ReflectionHelpers.getField(socket, "mPfd");
+ if (pfd == null) {
+ return null;
+ }
+ return pfd.getFileDescriptor();
+ }
+
+ @VisibleForTesting
+ int getPort() {
+ BluetoothSocket socket = ReflectionHelpers.getField(mRealServerSocket, "mSocket");
+ return ReflectionHelpers.getField(socket, "mPort");
+ }
+
+ private ShadowLocalSocket getLocalSocketShadow(BluetoothSocket socket) {
+ LocalSocket localSocket = ReflectionHelpers.getField(socket, "mSocket");
+ return (ShadowLocalSocket) Shadow.extract(localSocket);
+ }
+
+ private RfcommDelegate getLocalRfcommDelegate() {
+ return DeviceShadowEnvironmentImpl.getLocalBlueletImpl().getRfcommDelegate();
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothSocket.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothSocket.java
new file mode 100644
index 0000000..5d417cf
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowBluetoothSocket.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.shadows.bluetooth;
+
+import android.bluetooth.BluetoothSocket;
+import android.os.ParcelFileDescriptor;
+
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.RfcommDelegate;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Shadow implementation of a Bluetooth Socket
+ */
+@Implements(BluetoothSocket.class)
+public class ShadowBluetoothSocket {
+
+ @RealObject
+ BluetoothSocket mRealSocket;
+
+ public ShadowBluetoothSocket() {
+ }
+
+ @Implementation
+ public void connect() throws IOException {
+ Shadow.directlyOn(mRealSocket, BluetoothSocket.class, "connect");
+
+ boolean isEncrypted = ReflectionHelpers.getField(mRealSocket, "mEncrypt");
+ FileDescriptor localFd =
+ ((ParcelFileDescriptor) ReflectionHelpers.getField(mRealSocket,
+ "mPfd")).getFileDescriptor();
+ RfcommDelegate local = DeviceShadowEnvironmentImpl.getLocalBlueletImpl()
+ .getRfcommDelegate();
+ String remoteAddress = mRealSocket.getRemoteDevice().getAddress();
+ local.finishPendingConnection(remoteAddress, localFd, isEncrypted);
+
+ ShadowLocalSocket shadowLocalSocket = getLocalSocketShadow();
+ shadowLocalSocket.setRemoteAddress(remoteAddress);
+ }
+
+ @Implementation
+ public InputStream getInputStream() throws IOException {
+ ShadowLocalSocket socket = getLocalSocketShadow();
+ return socket.getInputStream();
+ }
+
+ @Implementation
+ public OutputStream getOutputStream() throws IOException {
+ ShadowLocalSocket socket = getLocalSocketShadow();
+ return socket.getOutputStream();
+ }
+
+ private ShadowLocalSocket getLocalSocketShadow() throws IOException {
+ try {
+ return (ShadowLocalSocket) Shadow.extract(
+ ReflectionHelpers.getField(mRealSocket, "mSocket"));
+ } catch (NullPointerException e) {
+ throw new IOException(e);
+ }
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowLocalSocket.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowLocalSocket.java
new file mode 100644
index 0000000..5189330
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowLocalSocket.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.shadows.bluetooth;
+
+import android.net.LocalSocket;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.BluetoothConstants;
+import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.RfcommDelegate;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Shadow implementation of a LocalSocket to make bluetooth connections function.
+ */
+@Implements(LocalSocket.class)
+public class ShadowLocalSocket {
+
+ private String mRemoteAddress;
+ private FileDescriptor mFd;
+ private FileDescriptor mAncillaryFd;
+
+ public ShadowLocalSocket() {
+ }
+
+ public void __constructor__(FileDescriptor fd) {
+ this.mFd = fd;
+ }
+
+ @Implementation
+ public FileDescriptor[] getAncillaryFileDescriptors() throws IOException {
+ return new FileDescriptor[]{mAncillaryFd};
+ }
+
+ @Implementation
+ @SuppressWarnings("InputStreamSlowMultibyteRead")
+ public InputStream getInputStream() throws IOException {
+ final RfcommDelegate local = getLocalRfcommDelegate();
+ return new InputStream() {
+ @Override
+ public int read() throws IOException {
+ int res = local.read(mRemoteAddress, mFd);
+ if (res == BluetoothConstants.SOCKET_CLOSE) {
+ throw new IOException("closed");
+ }
+ return res & 0xFF;
+ }
+ };
+ }
+
+ @Implementation
+ public OutputStream getOutputStream() throws IOException {
+ final RfcommDelegate local = getLocalRfcommDelegate();
+ return new OutputStream() {
+ @Override
+ public void write(int b) throws IOException {
+ local.write(mRemoteAddress, mFd, b);
+ }
+ };
+ }
+
+ @Implementation
+ public void setSoTimeout(int n) throws IOException {
+ // Nothing
+ }
+
+ @Implementation
+ public void shutdownInput() throws IOException {
+ getLocalRfcommDelegate().shutdownInput(mRemoteAddress, mFd);
+ }
+
+ @Implementation
+ public void shutdownOutput() throws IOException {
+ if (mRemoteAddress == null) {
+ return;
+ }
+ getLocalRfcommDelegate().shutdownOutput(mRemoteAddress, mFd);
+ }
+
+ void setAncillaryFd(FileDescriptor fd) {
+ mAncillaryFd = fd;
+ }
+
+ void setRemoteAddress(String address) {
+ mRemoteAddress = address;
+ }
+
+ @VisibleForTesting
+ void setFileDescriptorForTest(FileDescriptor fd) {
+ this.mFd = fd;
+ }
+
+ private RfcommDelegate getLocalRfcommDelegate() {
+ return DeviceShadowEnvironmentImpl.getLocalBlueletImpl().getRfcommDelegate();
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowParcelFileDescriptor.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowParcelFileDescriptor.java
new file mode 100644
index 0000000..585939b
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/bluetooth/ShadowParcelFileDescriptor.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.shadows.bluetooth;
+
+import android.os.ParcelFileDescriptor;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * Inert implementation of a ParcelFileDescriptor to make bluetooth connections function.
+ */
+@Implements(ParcelFileDescriptor.class)
+public class ShadowParcelFileDescriptor {
+
+ private FileDescriptor mFd;
+
+ public ShadowParcelFileDescriptor() {
+ }
+
+ public void __constructor__(FileDescriptor fd) {
+ this.mFd = fd;
+ }
+
+ @Implementation
+ public FileDescriptor getFileDescriptor() {
+ return mFd;
+ }
+
+ @Implementation
+ public void close() throws IOException {
+ // Nothing
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/common/DeviceShadowContextImpl.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/common/DeviceShadowContextImpl.java
new file mode 100644
index 0000000..9bbcee7
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/common/DeviceShadowContextImpl.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.shadows.common;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+import com.android.libraries.testing.deviceshadower.internal.DeviceletImpl;
+import com.android.libraries.testing.deviceshadower.internal.common.BroadcastManager;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.shadows.ShadowContextImpl;
+
+import javax.annotation.Nullable;
+
+/**
+ * Extends {@link ShadowContextImpl} to achieve automatic method redirection to correct virtual
+ * device.
+ *
+ * <p>Supports:
+ * <li>Broadcasting</li>
+ * Includes send regular, regular sticky, ordered broadcast, and register/unregister receiver.
+ * </p>
+ */
+@Implements(className = "android.app.ContextImpl")
+public class DeviceShadowContextImpl extends ShadowContextImpl {
+
+ private static final String TAG = "DeviceShadowContextImpl";
+
+ @RealObject
+ private Context mContextImpl;
+
+ @Override
+ @Implementation
+ @Nullable
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+ if (receiver == null) {
+ return null;
+ }
+ BroadcastManager manager = getLocalBroadcastManager();
+ if (manager == null) {
+ Log.w(TAG, "Receiver registered before any devices added: " + receiver);
+ return null;
+ }
+ return manager.registerReceiver(
+ receiver, filter, null /* permission */, null /* handler */, mContextImpl);
+ }
+
+ @Override
+ @Implementation
+ @Nullable
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
+ @Nullable String broadcastPermission, @Nullable Handler scheduler) {
+ return getLocalBroadcastManager().registerReceiver(
+ receiver, filter, broadcastPermission, scheduler, mContextImpl);
+ }
+
+ @Override
+ @Implementation
+ public void unregisterReceiver(BroadcastReceiver broadcastReceiver) {
+ getLocalBroadcastManager().unregisterReceiver(broadcastReceiver);
+ }
+
+ @Override
+ @Implementation
+ public void sendBroadcast(Intent intent) {
+ getLocalBroadcastManager().sendBroadcast(intent, null /* permission */);
+ }
+
+ @Override
+ @Implementation
+ public void sendBroadcast(Intent intent, @Nullable String receiverPermission) {
+ getLocalBroadcastManager().sendBroadcast(intent, receiverPermission);
+ }
+
+ @Override
+ @Implementation
+ public void sendOrderedBroadcast(Intent intent, @Nullable String receiverPermission) {
+ getLocalBroadcastManager().sendOrderedBroadcast(intent, receiverPermission);
+ }
+
+ @Override
+ @Implementation
+ public void sendOrderedBroadcast(Intent intent, @Nullable String receiverPermission,
+ @Nullable BroadcastReceiver resultReceiver, @Nullable Handler scheduler,
+ int initialCode, @Nullable String initialData, @Nullable Bundle initialExtras) {
+ getLocalBroadcastManager().sendOrderedBroadcast(intent, receiverPermission, resultReceiver,
+ scheduler, initialCode, initialData, initialExtras, mContextImpl);
+ }
+
+ @Override
+ @Implementation
+ public void sendStickyBroadcast(Intent intent) {
+ getLocalBroadcastManager().sendStickyBroadcast(intent);
+ }
+
+ private BroadcastManager getLocalBroadcastManager() {
+ DeviceletImpl devicelet = DeviceShadowEnvironmentImpl.getLocalDeviceletImpl();
+ if (devicelet == null) {
+ return null;
+ }
+ return devicelet.getBroadcastManager();
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/nfc/ShadowNfcAdapter.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/nfc/ShadowNfcAdapter.java
new file mode 100644
index 0000000..e7112fb
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/shadows/nfc/ShadowNfcAdapter.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.shadows.nfc;
+
+import static org.robolectric.util.ReflectionHelpers.callConstructor;
+
+import android.content.Context;
+import android.nfc.NfcAdapter;
+
+import com.android.libraries.testing.deviceshadower.Enums.NfcOperation;
+import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
+import com.android.libraries.testing.deviceshadower.internal.nfc.INfcAdapterImpl;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+/**
+ * Shadow implementation of Nfc Adapter.
+ */
+@Implements(NfcAdapter.class)
+public class ShadowNfcAdapter {
+
+ @Implementation
+ public static NfcAdapter getDefaultAdapter(Context context) {
+ if (DeviceShadowEnvironmentImpl.getLocalNfcletImpl()
+ .shouldInterrupt(NfcOperation.GET_ADAPTER)) {
+ return null;
+ }
+ ReflectionHelpers.setStaticField(NfcAdapter.class, "sService", new INfcAdapterImpl());
+ return callConstructor(NfcAdapter.class, ClassParameter.from(Context.class, context));
+ }
+
+ // TODO(b/200231384): support state change.
+ public ShadowNfcAdapter() {
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/BaseTestCase.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/BaseTestCase.java
new file mode 100644
index 0000000..8a3c0e7
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/BaseTestCase.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.testcases;
+
+import android.app.Application;
+
+import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
+import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowLocalSocket;
+import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowParcelFileDescriptor;
+import com.android.libraries.testing.deviceshadower.shadows.common.DeviceShadowContextImpl;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.internal.AssumptionViolatedException;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/**
+ * Base class for all DeviceShadower client.
+ */
+@Config(
+ // sdk = 21,
+ shadows = {
+ DeviceShadowContextImpl.class,
+ ShadowParcelFileDescriptor.class,
+ ShadowLocalSocket.class
+ })
+public class BaseTestCase {
+
+ protected Application mContext = RuntimeEnvironment.application;
+
+ /**
+ * Test Watcher which logs test starting and finishing so log messages are easier to read.
+ */
+ @Rule
+ public TestWatcher watcher = new TestWatcher() {
+ @Override
+ protected void succeeded(Description description) {
+ super.succeeded(description);
+ logMessage(
+ String.format("Test %s finished successfully.", description.getDisplayName()));
+ }
+
+ @Override
+ protected void failed(Throwable e, Description description) {
+ super.failed(e, description);
+ logMessage(String.format("Test %s failed.", description.getDisplayName()));
+ }
+
+ @Override
+ protected void skipped(AssumptionViolatedException e, Description description) {
+ super.skipped(e, description);
+ logMessage(String.format("Test %s is skipped.", description.getDisplayName()));
+ }
+
+ @Override
+ protected void starting(Description description) {
+ super.starting(description);
+ logMessage(String.format("Test %s started.", description.getDisplayName()));
+ }
+
+ @Override
+ protected void finished(Description description) {
+ super.finished(description);
+ }
+
+ private void logMessage(String message) {
+ System.out.println("\n*** " + message);
+ }
+ };
+
+ @Before
+ public void setUp() throws Exception {
+ DeviceShadowEnvironment.init();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ DeviceShadowEnvironment.reset();
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/BluetoothTestCase.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/BluetoothTestCase.java
new file mode 100644
index 0000000..cddc6fe
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/BluetoothTestCase.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.testcases;
+
+import static org.robolectric.Shadows.shadowOf;
+import static org.robolectric.util.ReflectionHelpers.callConstructor;
+
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+
+import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowBluetoothA2dp;
+import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowBluetoothAdapter;
+import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowBluetoothDevice;
+import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowBluetoothLeScanner;
+import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowBluetoothServerSocket;
+import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowBluetoothSocket;
+
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+/**
+ * Base class for Bluetooth Test
+ */
+@Config(
+ shadows = {
+ ShadowBluetoothAdapter.class,
+ ShadowBluetoothDevice.class,
+ ShadowBluetoothLeScanner.class,
+ ShadowBluetoothSocket.class,
+ ShadowBluetoothServerSocket.class,
+ ShadowBluetoothA2dp.class
+ })
+public class BluetoothTestCase extends BaseTestCase {
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ // TODO(b/28087747): Get bluetooth Manager from robolectric framework.
+ shadowOf(RuntimeEnvironment.application)
+ .setSystemService(
+ Context.BLUETOOTH_SERVICE,
+ callConstructor(BluetoothManager.class,
+ ClassParameter.from(Context.class, mContext)));
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/Matchers.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/Matchers.java
new file mode 100644
index 0000000..3bfe43b
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/Matchers.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.testcases;
+
+import static org.mockito.ArgumentMatchers.argThat;
+
+import android.bluetooth.BluetoothSocket;
+
+import org.mockito.ArgumentMatcher;
+
+/**
+ * Convenient methods to create mockito matchers.
+ */
+public class Matchers {
+
+ private Matchers() {
+ }
+
+ public static <T extends Exception> T exception(final Class<T> clazz, final String... msgs) {
+ return argThat(
+ new ArgumentMatcher<T>() {
+ @Override
+ public boolean matches(T obj) {
+ if (!clazz.isInstance(obj)) {
+ return false;
+ }
+ Throwable exception = clazz.cast(obj);
+ for (String msg : msgs) {
+ if (exception == null || !exception.getMessage().contains(msg)) {
+ return false;
+ }
+ exception = exception.getCause();
+ }
+ return true;
+ }
+ });
+ }
+
+ public static BluetoothSocket socket(final String addr) {
+ return argThat(
+ new ArgumentMatcher<BluetoothSocket>() {
+ @Override
+ public boolean matches(BluetoothSocket obj) {
+ return ((BluetoothSocket) obj)
+ .getRemoteDevice()
+ .getAddress()
+ .toUpperCase()
+ .equals(addr.toUpperCase());
+ }
+ });
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/NfcTestCase.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/NfcTestCase.java
new file mode 100644
index 0000000..a80164b
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/NfcTestCase.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.testcases;
+
+import com.android.libraries.testing.deviceshadower.shadows.nfc.ShadowNfcAdapter;
+
+import org.robolectric.annotation.Config;
+
+/**
+ * Base class for NFC Test
+ */
+@Config(shadows = {ShadowNfcAdapter.class})
+public class NfcTestCase extends BaseTestCase {
+
+}
+
diff --git a/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/SmsTestCase.java b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/SmsTestCase.java
new file mode 100644
index 0000000..edfcc6d
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/libraries/testing/deviceshadower/testcases/SmsTestCase.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libraries.testing.deviceshadower.testcases;
+
+import android.content.pm.ProviderInfo;
+import android.provider.Telephony;
+
+import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironmentInternal;
+
+import org.robolectric.Robolectric;
+
+/**
+ * Base class for SMS Test
+ */
+public class SmsTestCase extends BaseTestCase {
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ ProviderInfo info = new ProviderInfo();
+ info.authority = Telephony.Sms.CONTENT_URI.getAuthority();
+ Robolectric.buildContentProvider(
+ DeviceShadowEnvironmentInternal.getSmsContentProviderClass())
+ .create(info);
+ }
+}
diff --git a/nearby/tests/robotests/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairerTest.java b/nearby/tests/robotests/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairerTest.java
new file mode 100644
index 0000000..1ac2aaf
--- /dev/null
+++ b/nearby/tests/robotests/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairerTest.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static org.robolectric.Shadows.shadowOf;
+
+import android.Manifest.permission;
+import android.bluetooth.BluetoothAdapter;
+
+import com.android.libraries.testing.deviceshadower.Bluelet.IoCapabilities;
+import com.android.libraries.testing.deviceshadower.DeviceShadowEnvironment;
+import com.android.libraries.testing.deviceshadower.shadows.bluetooth.ShadowBluetoothDevice;
+import com.android.libraries.testing.deviceshadower.testcases.BluetoothTestCase;
+
+import com.google.common.base.VerifyException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Tests for {@link BluetoothClassicPairer}.
+ */
+@RunWith(RobolectricTestRunner.class)
+public class BluetoothClassicPairerTest extends BluetoothTestCase {
+
+ private static final String LOCAL_DEVICE_ADDRESS = "AA:AA:AA:AA:AA:01";
+
+ /**
+ * The remote device's Bluetooth Classic address.
+ */
+ private static final String REMOTE_DEVICE_PUBLIC_ADDRESS = "BB:BB:BB:BB:BB:0C";
+
+ private Preferences.Builder mPrefsBuilder;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mPrefsBuilder = Preferences.builder().setCreateBondTimeoutSeconds(10);
+
+ ShadowBluetoothDevice.resetPairingConfirmation();
+ shadowOf(mContext)
+ .grantPermissions(
+ permission.BLUETOOTH, permission.BLUETOOTH_ADMIN,
+ permission.BLUETOOTH_PRIVILEGED);
+
+ DeviceShadowEnvironment.addDevice(LOCAL_DEVICE_ADDRESS)
+ .bluetooth()
+ .setAdapterInitialState(BluetoothAdapter.STATE_ON)
+ .setIoCapabilities(IoCapabilities.DISPLAY_YES_NO);
+ DeviceShadowEnvironment.addDevice(REMOTE_DEVICE_PUBLIC_ADDRESS)
+ .bluetooth()
+ .setAdapterInitialState(BluetoothAdapter.STATE_ON)
+ .setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE)
+ .setIoCapabilities(IoCapabilities.DISPLAY_YES_NO);
+
+ // By default, code runs as if it's on this virtual "device".
+ DeviceShadowEnvironment.setLocalDevice(LOCAL_DEVICE_ADDRESS);
+ }
+
+ @Test
+ public void pair_setPairingConfirmationTrue_deviceBonded() throws Exception {
+ // TODO(b/217195327): replace deviceshadower with injector.
+ /*
+ AtomicReference<BluetoothDevice> targetRemoteDevice = new AtomicReference<>();
+ BluetoothClassicPairer bluetoothClassicPairer =
+ new BluetoothClassicPairer(
+ mContext,
+ BluetoothAdapter.getDefaultAdapter()
+ .getRemoteDevice(REMOTE_DEVICE_PUBLIC_ADDRESS),
+ mPrefsBuilder.build(),
+ (BluetoothDevice remoteDevice, int key) -> {
+ targetRemoteDevice.set(remoteDevice);
+ // Confirms at remote device to pair with local one.
+ setPairingConfirmationAtRemoteDevice(true);
+
+ // Confirms to pair with remote device.
+ remoteDevice.setPairingConfirmation(true);
+ });
+
+ bluetoothClassicPairer.pair();
+
+ assertThat(targetRemoteDevice.get()).isNotNull();
+ assertThat(targetRemoteDevice.get().getAddress()).isEqualTo(REMOTE_DEVICE_PUBLIC_ADDRESS);
+ assertThat(targetRemoteDevice.get().getBondState()).isEqualTo(BluetoothDevice.BOND_BONDED);
+ assertThat(bluetoothClassicPairer.isPaired()).isTrue();
+ */
+ }
+
+ @Test
+ public void pair_setPairingConfirmationFalse_throwsExceptionDeviceNotBonded() throws Exception {
+ // TODO(b/217195327): replace deviceshadower with injector.
+ /*
+ AtomicReference<BluetoothDevice> targetRemoteDevice = new AtomicReference<>();
+ BluetoothClassicPairer bluetoothClassicPairer =
+ new BluetoothClassicPairer(
+ mContext,
+ BluetoothAdapter.getDefaultAdapter()
+ .getRemoteDevice(REMOTE_DEVICE_PUBLIC_ADDRESS),
+ mPrefsBuilder.build(),
+ (BluetoothDevice remoteDevice, int key) -> {
+ targetRemoteDevice.set(remoteDevice);
+ // Confirms at remote device to pair with local one.
+ setPairingConfirmationAtRemoteDevice(true);
+
+ // Confirms NOT to pair with remote device.
+ remoteDevice.setPairingConfirmation(false);
+ });
+
+ assertThrows(PairingException.class, bluetoothClassicPairer::pair);
+
+ assertThat(targetRemoteDevice.get()).isNotNull();
+ assertThat(targetRemoteDevice.get().getAddress()).isEqualTo(REMOTE_DEVICE_PUBLIC_ADDRESS);
+ assertThat(targetRemoteDevice.get().getBondState()).isNotEqualTo(
+ BluetoothDevice.BOND_BONDED);
+ assertThat(bluetoothClassicPairer.isPaired()).isFalse();
+ */
+ }
+
+ @Test
+ public void pair_setPairingConfirmationIgnored_throwsExceptionDeviceNotBonded()
+ throws Exception {
+ // TODO(b/217195327): replace deviceshadower with injector.
+ /*
+ AtomicReference<BluetoothDevice> targetRemoteDevice = new AtomicReference<>();
+ BluetoothClassicPairer bluetoothClassicPairer =
+ new BluetoothClassicPairer(
+ mContext,
+ BluetoothAdapter.getDefaultAdapter()
+ .getRemoteDevice(REMOTE_DEVICE_PUBLIC_ADDRESS),
+ mPrefsBuilder.build(),
+ (BluetoothDevice remoteDevice, int key) -> {
+ targetRemoteDevice.set(remoteDevice);
+ // Confirms at remote device to pair with local one.
+ setPairingConfirmationAtRemoteDevice(true);
+
+ // Ignores the setPairingConfirmation.
+ });
+
+ assertThrows(PairingException.class, bluetoothClassicPairer::pair);
+ assertThat(targetRemoteDevice.get()).isNotNull();
+ assertThat(targetRemoteDevice.get().getAddress()).isEqualTo(REMOTE_DEVICE_PUBLIC_ADDRESS);
+ assertThat(targetRemoteDevice.get().getBondState()).isNotEqualTo(
+ BluetoothDevice.BOND_BONDED);
+ assertThat(bluetoothClassicPairer.isPaired()).isFalse();
+ */
+ }
+
+ private static void setPairingConfirmationAtRemoteDevice(boolean confirm) {
+ try {
+ DeviceShadowEnvironment.run(REMOTE_DEVICE_PUBLIC_ADDRESS,
+ () -> BluetoothAdapter.getDefaultAdapter()
+ .getRemoteDevice(LOCAL_DEVICE_ADDRESS)
+ .setPairingConfirmation(confirm)).get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new VerifyException("failed to set pairing confirmation at remote device", e);
+ }
+ }
+}
diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp
new file mode 100644
index 0000000..9b35452
--- /dev/null
+++ b/nearby/tests/unit/Android.bp
@@ -0,0 +1,57 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "NearbyUnitTests",
+ defaults: ["mts-target-sdk-version-current"],
+ sdk_version: "test_current",
+ min_sdk_version: "31",
+
+ // Include all test java files.
+ srcs: ["src/**/*.java"],
+
+ libs: [
+ "android.test.base",
+ "android.test.mock",
+ "android.test.runner",
+ ],
+ compile_multilib: "both",
+
+ static_libs: [
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "framework-nearby-static",
+ "guava",
+ "junit",
+ "libprotobuf-java-lite",
+ "mockito-target-extended-minus-junit4",
+ "platform-test-annotations",
+ "service-nearby-pre-jarjar",
+ "truth-prebuilt",
+ // "Robolectric_all-target",
+ ],
+ // these are needed for Extended Mockito
+ jni_libs: [
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
+}
diff --git a/nearby/tests/unit/AndroidManifest.xml b/nearby/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..88c0f5f
--- /dev/null
+++ b/nearby/tests/unit/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.nearby.test">
+
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.BLUETOOTH" />
+ <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.nearby.test"
+ android:label="Nearby Mainline Module Tests" />
+</manifest>
diff --git a/nearby/tests/unit/AndroidTest.xml b/nearby/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..fdf665d
--- /dev/null
+++ b/nearby/tests/unit/AndroidTest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<configuration description="Runs Nearby Mainline API Tests.">
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="NearbyUnitTests.apk" />
+ </target_preparer>
+
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-tag" value="NearbyUnitTests" />
+ <option name="config-descriptor:metadata" key="mainline-param"
+ value="com.google.android.tethering.next.apex" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.nearby.test" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+
+ <!-- Only run NearbyUnitTests in MTS if the Nearby Mainline module is installed. -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+</configuration>
diff --git a/nearby/tests/unit/src/android/nearby/ScanRequestTest.java b/nearby/tests/unit/src/android/nearby/ScanRequestTest.java
new file mode 100644
index 0000000..a45d8bb
--- /dev/null
+++ b/nearby/tests/unit/src/android/nearby/ScanRequestTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import static android.nearby.ScanRequest.SCAN_MODE_BALANCED;
+import static android.nearby.ScanRequest.SCAN_MODE_LOW_POWER;
+import static android.nearby.ScanRequest.SCAN_TYPE_FAST_PAIR;
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+import android.os.WorkSource;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Units tests for {@link ScanRequest}. */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ScanRequestTest {
+
+ private static WorkSource getWorkSource() {
+ final int uid = 1001;
+ final String appName = "android.nearby.tests";
+ return new WorkSource(uid, appName);
+ }
+
+ /** Test creating a scan request. */
+ @Test
+ public void testScanRequestBuilder() {
+ final int scanType = SCAN_TYPE_FAST_PAIR;
+ ScanRequest request = new ScanRequest.Builder().setScanType(scanType).build();
+
+ assertThat(request.getScanType()).isEqualTo(scanType);
+ assertThat(request.getScanMode()).isEqualTo(SCAN_MODE_LOW_POWER);
+ // Work source is null if not set.
+ assertThat(request.getWorkSource().isEmpty()).isTrue();
+ }
+
+ /** Verify RuntimeException is thrown when creating scan request with invalid scan type. */
+ @Test(expected = RuntimeException.class)
+ public void testScanRequestBuilder_invalidScanType() {
+ final int invalidScanType = -1;
+ ScanRequest.Builder builder = new ScanRequest.Builder().setScanType(invalidScanType);
+
+ builder.build();
+ }
+
+ /** Verify RuntimeException is thrown when creating scan mode with invalid scan mode. */
+ @Test(expected = RuntimeException.class)
+ public void testScanModeBuilder_invalidScanType() {
+ final int invalidScanMode = -5;
+ ScanRequest.Builder builder = new ScanRequest.Builder().setScanType(
+ SCAN_TYPE_FAST_PAIR).setScanMode(invalidScanMode);
+ builder.build();
+ }
+
+ /** Verify setting work source in the scan request. */
+ @Test
+ public void testSetWorkSource() {
+ WorkSource workSource = getWorkSource();
+ ScanRequest request = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_FAST_PAIR)
+ .setWorkSource(workSource)
+ .build();
+
+ assertThat(request.getWorkSource()).isEqualTo(workSource);
+ }
+
+ /** Verify setting work source with null value in the scan request. */
+ @Test
+ public void testSetWorkSource_nullValue() {
+ ScanRequest request = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_FAST_PAIR)
+ .setWorkSource(null)
+ .build();
+
+ // Null work source is allowed.
+ assertThat(request.getWorkSource().isEmpty()).isTrue();
+ }
+
+ /** Verify toString returns expected string. */
+ @Test
+ public void testToString() {
+ WorkSource workSource = getWorkSource();
+ ScanRequest request = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_FAST_PAIR)
+ .setScanMode(SCAN_MODE_BALANCED)
+ .setBleEnabled(true)
+ .setWorkSource(workSource)
+ .build();
+
+ assertThat(request.toString()).isEqualTo(
+ "Request[scanType=1, scanMode=SCAN_MODE_BALANCED, "
+ + "enableBle=true, workSource=WorkSource{1001 android.nearby.tests}, "
+ + "scanFilters=[]]");
+ }
+
+ /** Verify toString works correctly with null WorkSource. */
+ @Test
+ public void testToString_nullWorkSource() {
+ ScanRequest request = new ScanRequest.Builder().setScanType(
+ SCAN_TYPE_FAST_PAIR).setWorkSource(null).build();
+
+ assertThat(request.toString()).isEqualTo("Request[scanType=1, "
+ + "scanMode=SCAN_MODE_LOW_POWER, enableBle=true, workSource=WorkSource{}, "
+ + "scanFilters=[]]");
+ }
+
+ /** Verify writing and reading from parcel for scan request. */
+ @Test
+ public void testParceling() {
+ final int scanType = SCAN_TYPE_NEARBY_PRESENCE;
+ WorkSource workSource = getWorkSource();
+ ScanRequest originalRequest = new ScanRequest.Builder()
+ .setScanType(scanType)
+ .setScanMode(SCAN_MODE_BALANCED)
+ .setBleEnabled(true)
+ .setWorkSource(workSource)
+ .build();
+
+ // Write the scan request to parcel, then read from it.
+ ScanRequest request = writeReadFromParcel(originalRequest);
+
+ // Verify the request read from parcel equals to the original request.
+ assertThat(request).isEqualTo(originalRequest);
+ }
+
+ /** Verify parceling with null WorkSource. */
+ @Test
+ public void testParceling_nullWorkSource() {
+ final int scanType = SCAN_TYPE_NEARBY_PRESENCE;
+ ScanRequest originalRequest = new ScanRequest.Builder()
+ .setScanType(scanType).build();
+
+ ScanRequest request = writeReadFromParcel(originalRequest);
+
+ assertThat(request).isEqualTo(originalRequest);
+ }
+
+ private ScanRequest writeReadFromParcel(ScanRequest originalRequest) {
+ Parcel parcel = Parcel.obtain();
+ originalRequest.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ return ScanRequest.CREATOR.createFromParcel(parcel);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
new file mode 100644
index 0000000..31965a4
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby;
+
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.nearby.IScanListener;
+import android.nearby.ScanRequest;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+public final class NearbyServiceTest {
+
+ private Context mContext;
+ private NearbyService mService;
+ private ScanRequest mScanRequest;
+ private UiAutomation mUiAutomation =
+ InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+ @Mock
+ private IScanListener mScanListener;
+
+ @Before
+ public void setup() {
+ initMocks(this);
+ mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG);
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mService = new NearbyService(mContext);
+ mScanRequest = createScanRequest();
+ }
+
+ @Test
+ public void test_register() {
+ mService.registerScanListener(mScanRequest, mScanListener);
+ }
+
+ @Test
+ public void test_unregister() {
+ mService.unregisterScanListener(mScanListener);
+ }
+
+ private ScanRequest createScanRequest() {
+ return new ScanRequest.Builder()
+ .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR)
+ .setBleEnabled(true)
+ .build();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java
new file mode 100644
index 0000000..1d3653b
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java
@@ -0,0 +1,476 @@
+/*
+ * 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.ble;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.bluetooth.BluetoothDevice;
+import android.os.ParcelUuid;
+import android.util.SparseArray;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.nearby.common.ble.testing.FastPairTestData;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+@RunWith(AndroidJUnit4.class)
+public class BleFilterTest {
+
+
+ public static final ParcelUuid EDDYSTONE_SERVICE_DATA_PARCELUUID =
+ ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB");
+
+ private ParcelUuid mServiceDataUuid;
+ private BleSighting mBleSighting;
+ private BleFilter.Builder mFilterBuilder;
+
+ @Before
+ public void setUp() throws Exception {
+ // This is the service data UUID in TestData.sd1.
+ // Can't be static because of Robolectric.
+ mServiceDataUuid = ParcelUuid.fromString("000000E0-0000-1000-8000-00805F9B34FB");
+
+ byte[] bleRecordBytes =
+ new byte[]{
+ 0x02,
+ 0x01,
+ 0x1a, // advertising flags
+ 0x05,
+ 0x02,
+ 0x0b,
+ 0x11,
+ 0x0a,
+ 0x11, // 16 bit service uuids
+ 0x04,
+ 0x09,
+ 0x50,
+ 0x65,
+ 0x64, // setName
+ 0x02,
+ 0x0A,
+ (byte) 0xec, // tx power level
+ 0x05,
+ 0x16,
+ 0x0b,
+ 0x11,
+ 0x50,
+ 0x64, // service data
+ 0x05,
+ (byte) 0xff,
+ (byte) 0xe0,
+ 0x00,
+ 0x02,
+ 0x15, // manufacturer specific data
+ 0x03,
+ 0x50,
+ 0x01,
+ 0x02, // an unknown data type won't cause trouble
+ };
+
+ mBleSighting = new BleSighting(null /* device */, bleRecordBytes,
+ -10, 1397545200000000L);
+ mFilterBuilder = new BleFilter.Builder();
+ }
+
+ @Test
+ public void setNameFilter() {
+ BleFilter filter = mFilterBuilder.setDeviceName("Ped").build();
+ assertThat(filter.matches(mBleSighting)).isTrue();
+
+ filter = mFilterBuilder.setDeviceName("Pem").build();
+ assertThat(filter.matches(mBleSighting)).isFalse();
+ }
+
+ @Test
+ public void setServiceUuidFilter() {
+ BleFilter filter =
+ mFilterBuilder.setServiceUuid(
+ ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB"))
+ .build();
+ assertThat(filter.matches(mBleSighting)).isTrue();
+
+ filter =
+ mFilterBuilder.setServiceUuid(
+ ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB"))
+ .build();
+ assertThat(filter.matches(mBleSighting)).isFalse();
+
+ filter =
+ mFilterBuilder
+ .setServiceUuid(
+ ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB"),
+ ParcelUuid.fromString("FFFFFFF0-FFFF-FFFF-FFFF-FFFFFFFFFFFF"))
+ .build();
+ assertThat(filter.matches(mBleSighting)).isTrue();
+ }
+
+ @Test
+ public void setServiceDataFilter() {
+ byte[] setServiceData = new byte[]{0x50, 0x64};
+ ParcelUuid serviceDataUuid = ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB");
+ BleFilter filter = mFilterBuilder.setServiceData(serviceDataUuid, setServiceData).build();
+ assertThat(filter.matches(mBleSighting)).isTrue();
+
+ byte[] emptyData = new byte[0];
+ filter = mFilterBuilder.setServiceData(serviceDataUuid, emptyData).build();
+ assertThat(filter.matches(mBleSighting)).isTrue();
+
+ byte[] prefixData = new byte[]{0x50};
+ filter = mFilterBuilder.setServiceData(serviceDataUuid, prefixData).build();
+ assertThat(filter.matches(mBleSighting)).isTrue();
+
+ byte[] nonMatchData = new byte[]{0x51, 0x64};
+ byte[] mask = new byte[]{(byte) 0x00, (byte) 0xFF};
+ filter = mFilterBuilder.setServiceData(serviceDataUuid, nonMatchData, mask).build();
+ assertThat(filter.matches(mBleSighting)).isTrue();
+
+ filter = mFilterBuilder.setServiceData(serviceDataUuid, nonMatchData).build();
+ assertThat(filter.matches(mBleSighting)).isFalse();
+ }
+
+ @Test
+ public void manufacturerSpecificData() {
+ byte[] setManufacturerData = new byte[]{0x02, 0x15};
+ int manufacturerId = 0xE0;
+ BleFilter filter =
+ mFilterBuilder.setManufacturerData(manufacturerId, setManufacturerData).build();
+ assertThat(filter.matches(mBleSighting)).isTrue();
+
+ byte[] emptyData = new byte[0];
+ filter = mFilterBuilder.setManufacturerData(manufacturerId, emptyData).build();
+ assertThat(filter.matches(mBleSighting)).isTrue();
+
+ byte[] prefixData = new byte[]{0x02};
+ filter = mFilterBuilder.setManufacturerData(manufacturerId, prefixData).build();
+ assertThat(filter.matches(mBleSighting)).isTrue();
+
+ // Data and mask are nullable. Check that we still match when they're null.
+ filter = mFilterBuilder.setManufacturerData(manufacturerId,
+ null /* data */).build();
+ assertThat(filter.matches(mBleSighting)).isTrue();
+ filter = mFilterBuilder.setManufacturerData(manufacturerId,
+ null /* data */, null /* mask */).build();
+ assertThat(filter.matches(mBleSighting)).isTrue();
+
+ // Test data mask
+ byte[] nonMatchData = new byte[]{0x02, 0x14};
+ filter = mFilterBuilder.setManufacturerData(manufacturerId, nonMatchData).build();
+ assertThat(filter.matches(mBleSighting)).isFalse();
+ byte[] mask = new byte[]{(byte) 0xFF, (byte) 0x00};
+ filter = mFilterBuilder.setManufacturerData(manufacturerId, nonMatchData, mask).build();
+ assertThat(filter.matches(mBleSighting)).isTrue();
+ }
+
+ @Test
+ public void manufacturerDataNotInBleRecord() {
+ byte[] bleRecord = FastPairTestData.adv_2;
+ // Verify manufacturer with no data
+ byte[] data = {(byte) 0xe0, (byte) 0x00};
+ BleFilter filter = mFilterBuilder.setManufacturerData(0x00e0, data).build();
+ assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+ }
+
+ @Test
+ public void manufacturerDataMaskNotInBleRecord() {
+ byte[] bleRecord = FastPairTestData.adv_2;
+
+ // Verify matching partial manufacturer with data and mask
+ byte[] data = {(byte) 0x15};
+ byte[] mask = {(byte) 0xff};
+
+ BleFilter filter = mFilterBuilder
+ .setManufacturerData(0x00e0, data, mask).build();
+ assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+ }
+
+
+ @Test
+ public void serviceData() throws Exception {
+ byte[] bleRecord = FastPairTestData.sd1;
+ byte[] serviceData = {(byte) 0x15};
+
+ // Verify manufacturer 2-byte UUID with no data
+ BleFilter filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData).build();
+ assertMatches(filter, null, 0, bleRecord);
+ }
+
+ @Test
+ public void serviceDataNoMatch() {
+ byte[] bleRecord = FastPairTestData.sd1;
+ byte[] serviceData = {(byte) 0xe1, (byte) 0x00};
+
+ // Verify manufacturer 2-byte UUID with no data
+ BleFilter filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData).build();
+ assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+ }
+
+ @Test
+ public void serviceDataMask() {
+ byte[] bleRecord = FastPairTestData.sd1;
+ BleFilter filter;
+
+ // Verify matching partial manufacturer with data and mask
+ byte[] serviceData1 = {(byte) 0x15};
+ byte[] mask1 = {(byte) 0xff};
+ filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData1, mask1).build();
+ assertMatches(filter, null, 0, bleRecord);
+ }
+
+ @Test
+ public void serviceDataMaskNoMatch() {
+ byte[] bleRecord = FastPairTestData.sd1;
+ BleFilter filter;
+
+ // Verify non-matching partial manufacturer with data and mask
+ byte[] serviceData2 = {(byte) 0xe0, (byte) 0x00, (byte) 0x10};
+ byte[] mask2 = {(byte) 0xff, (byte) 0xff, (byte) 0xff};
+ filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData2, mask2).build();
+ assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void serviceDataMaskWithDifferentLength() {
+ // Different lengths for data and mask.
+ byte[] serviceData = {(byte) 0xe0, (byte) 0x00, (byte) 0x10};
+ byte[] mask = {(byte) 0xff, (byte) 0xff};
+
+ //expected.expect(IllegalArgumentException.class);
+
+ mFilterBuilder.setServiceData(mServiceDataUuid, serviceData, mask).build();
+ }
+
+
+ @Test
+ public void deviceNameTest() {
+ // Verify the name filter matches
+ byte[] bleRecord = FastPairTestData.adv_1;
+ BleFilter filter = mFilterBuilder.setDeviceName("Pedometer").build();
+ assertMatches(filter, null, 0, bleRecord);
+ }
+
+ @Test
+ public void deviceNameNoMatch() {
+ // Verify the name filter does not match
+ byte[] bleRecord = FastPairTestData.adv_1;
+ BleFilter filter = mFilterBuilder.setDeviceName("Foo").build();
+ assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+ }
+
+ private static boolean matches(
+ BleFilter filter, BluetoothDevice device, int rssi, byte[] bleRecord) {
+ return filter.matches(new BleSighting(device,
+ bleRecord, rssi, 0 /* timestampNanos */));
+ }
+
+
+ private static void assertMatches(
+ BleFilter filter, BluetoothDevice device, int rssi, byte[] bleRecordBytes) {
+
+ // Device match.
+ if (filter.getDeviceAddress() != null
+ && (device == null || !filter.getDeviceAddress().equals(device.getAddress()))) {
+ fail("Filter specified a device address ("
+ + filter.getDeviceAddress()
+ + ") which doesn't match the actual value: ["
+ + (device == null ? "null device" : device.getAddress())
+ + "]");
+ }
+
+ // BLE record is null but there exist filters on it.
+ BleRecord bleRecord = BleRecord.parseFromBytes(bleRecordBytes);
+ if (bleRecord == null
+ && (filter.getDeviceName() != null
+ || filter.getServiceUuid() != null
+ || filter.getManufacturerData() != null
+ || filter.getServiceData() != null)) {
+ fail(
+ "The bleRecordBytes given parsed to a null bleRecord, but the filter"
+ + "has a non-null field which depends on the scan record");
+ }
+
+ // Local name match.
+ if (filter.getDeviceName() != null
+ && !filter.getDeviceName().equals(bleRecord.getDeviceName())) {
+ fail(
+ "The filter's device name ("
+ + filter.getDeviceName()
+ + ") doesn't match the scan record device name ("
+ + bleRecord.getDeviceName()
+ + ")");
+ }
+
+ // UUID match.
+ if (filter.getServiceUuid() != null
+ && !matchesServiceUuids(filter.getServiceUuid(), filter.getServiceUuidMask(),
+ bleRecord.getServiceUuids())) {
+ fail("The filter specifies a service UUID but it doesn't match "
+ + "what's in the scan record");
+ }
+
+ // Service data match
+ if (filter.getServiceDataUuid() != null
+ && !BleFilter.matchesPartialData(
+ filter.getServiceData(),
+ filter.getServiceDataMask(),
+ bleRecord.getServiceData(filter.getServiceDataUuid()))) {
+ fail(
+ "The filter's service data doesn't match what's in the scan record.\n"
+ + "Service data: "
+ + byteString(filter.getServiceData())
+ + "\n"
+ + "Service data UUID: "
+ + filter.getServiceDataUuid().toString()
+ + "\n"
+ + "Service data mask: "
+ + byteString(filter.getServiceDataMask())
+ + "\n"
+ + "Scan record service data: "
+ + byteString(bleRecord.getServiceData(filter.getServiceDataUuid()))
+ + "\n"
+ + "Scan record data map:\n"
+ + byteString(bleRecord.getServiceData()));
+ }
+
+ // Manufacturer data match.
+ if (filter.getManufacturerId() >= 0
+ && !BleFilter.matchesPartialData(
+ filter.getManufacturerData(),
+ filter.getManufacturerDataMask(),
+ bleRecord.getManufacturerSpecificData(filter.getManufacturerId()))) {
+ fail(
+ "The filter's manufacturer data doesn't match what's in the scan record.\n"
+ + "Manufacturer ID: "
+ + filter.getManufacturerId()
+ + "\n"
+ + "Manufacturer data: "
+ + byteString(filter.getManufacturerData())
+ + "\n"
+ + "Manufacturer data mask: "
+ + byteString(filter.getManufacturerDataMask())
+ + "\n"
+ + "Scan record manufacturer-specific data: "
+ + byteString(bleRecord.getManufacturerSpecificData(
+ filter.getManufacturerId()))
+ + "\n"
+ + "Manufacturer data array:\n"
+ + byteString(bleRecord.getManufacturerSpecificData()));
+ }
+
+ // All filters match.
+ assertThat(
+ matches(filter, device, rssi, bleRecordBytes)).isTrue();
+ }
+
+
+ private static String byteString(byte[] bytes) {
+ if (bytes == null) {
+ return "[null]";
+ } else {
+ final char[] hexArray = "0123456789ABCDEF".toCharArray();
+ char[] hexChars = new char[bytes.length * 2];
+ for (int i = 0; i < bytes.length; i++) {
+ int v = bytes[i] & 0xFF;
+ hexChars[i * 2] = hexArray[v >>> 4];
+ hexChars[i * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+ }
+
+ // Ref to beacon.decode.AppleBeaconDecoder.getFilterData
+ private static byte[] getFilterData(ParcelUuid uuid) {
+ byte[] data = new byte[18];
+ data[0] = (byte) 0x02;
+ data[1] = (byte) 0x15;
+ // Check if UUID is needed in data
+ if (uuid != null) {
+ // Convert UUID to array in big endian order
+ byte[] uuidBytes = uuidToByteArray(uuid);
+ for (int i = 0; i < 16; i++) {
+ // Adding uuid bytes in big-endian order to match iBeacon format
+ data[i + 2] = uuidBytes[i];
+ }
+ }
+ return data;
+ }
+
+ // Ref to beacon.decode.AppleBeaconDecoder.uuidToByteArray
+ private static byte[] uuidToByteArray(ParcelUuid uuid) {
+ ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
+ bb.putLong(uuid.getUuid().getMostSignificantBits());
+ bb.putLong(uuid.getUuid().getLeastSignificantBits());
+ return bb.array();
+ }
+
+ private static boolean matchesServiceUuids(
+ ParcelUuid uuid, ParcelUuid parcelUuidMask, List<ParcelUuid> uuids) {
+ if (uuid == null) {
+ return true;
+ }
+
+ for (ParcelUuid parcelUuid : uuids) {
+ UUID uuidMask = parcelUuidMask == null ? null : parcelUuidMask.getUuid();
+ if (matchesServiceUuid(uuid.getUuid(), uuidMask, parcelUuid.getUuid())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Check if the uuid pattern matches the particular service uuid.
+ private static boolean matchesServiceUuid(UUID uuid, UUID mask, UUID data) {
+ if (mask == null) {
+ return uuid.equals(data);
+ }
+ if ((uuid.getLeastSignificantBits() & mask.getLeastSignificantBits())
+ != (data.getLeastSignificantBits() & mask.getLeastSignificantBits())) {
+ return false;
+ }
+ return ((uuid.getMostSignificantBits() & mask.getMostSignificantBits())
+ == (data.getMostSignificantBits() & mask.getMostSignificantBits()));
+ }
+
+ private static String byteString(Map<ParcelUuid, byte[]> bytesMap) {
+ StringBuilder builder = new StringBuilder();
+ for (Map.Entry<ParcelUuid, byte[]> entry : bytesMap.entrySet()) {
+ builder.append(builder.toString().isEmpty() ? " " : "\n ");
+ builder.append(entry.getKey().toString());
+ builder.append(" --> ");
+ builder.append(byteString(entry.getValue()));
+ }
+ return builder.toString();
+ }
+
+ private static String byteString(SparseArray<byte[]> bytesArray) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < bytesArray.size(); i++) {
+ builder.append(builder.toString().isEmpty() ? " " : "\n ");
+ builder.append(byteString(bytesArray.valueAt(i)));
+ }
+ return builder.toString();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java
new file mode 100644
index 0000000..5da98e2
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java
@@ -0,0 +1,250 @@
+/*
+ * 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.
+ */
+
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+
+/** Test for Bluetooth LE {@link BleRecord}. */
+public class BleRecordTest {
+
+ // iBeacon (Apple) Packet 1
+ private static final byte[] BEACON = {
+ // Flags
+ (byte) 0x02,
+ (byte) 0x01,
+ (byte) 0x06,
+ // Manufacturer-specific data header
+ (byte) 0x1a,
+ (byte) 0xff,
+ (byte) 0x4c,
+ (byte) 0x00,
+ // iBeacon Type
+ (byte) 0x02,
+ // Frame length
+ (byte) 0x15,
+ // iBeacon Proximity UUID
+ (byte) 0xf7,
+ (byte) 0x82,
+ (byte) 0x6d,
+ (byte) 0xa6,
+ (byte) 0x4f,
+ (byte) 0xa2,
+ (byte) 0x4e,
+ (byte) 0x98,
+ (byte) 0x80,
+ (byte) 0x24,
+ (byte) 0xbc,
+ (byte) 0x5b,
+ (byte) 0x71,
+ (byte) 0xe0,
+ (byte) 0x89,
+ (byte) 0x3e,
+ // iBeacon Instance ID (Major/Minor)
+ (byte) 0x44,
+ (byte) 0xd0,
+ (byte) 0x25,
+ (byte) 0x22,
+ // Tx Power
+ (byte) 0xb3,
+ // RSP
+ (byte) 0x08,
+ (byte) 0x09,
+ (byte) 0x4b,
+ (byte) 0x6f,
+ (byte) 0x6e,
+ (byte) 0x74,
+ (byte) 0x61,
+ (byte) 0x6b,
+ (byte) 0x74,
+ (byte) 0x02,
+ (byte) 0x0a,
+ (byte) 0xf4,
+ (byte) 0x0a,
+ (byte) 0x16,
+ (byte) 0x0d,
+ (byte) 0xd0,
+ (byte) 0x74,
+ (byte) 0x6d,
+ (byte) 0x4d,
+ (byte) 0x6b,
+ (byte) 0x32,
+ (byte) 0x36,
+ (byte) 0x64,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00
+ };
+
+ // iBeacon (Apple) Packet 1
+ private static final byte[] SAME_BEACON = {
+ // Flags
+ (byte) 0x02,
+ (byte) 0x01,
+ (byte) 0x06,
+ // Manufacturer-specific data header
+ (byte) 0x1a,
+ (byte) 0xff,
+ (byte) 0x4c,
+ (byte) 0x00,
+ // iBeacon Type
+ (byte) 0x02,
+ // Frame length
+ (byte) 0x15,
+ // iBeacon Proximity UUID
+ (byte) 0xf7,
+ (byte) 0x82,
+ (byte) 0x6d,
+ (byte) 0xa6,
+ (byte) 0x4f,
+ (byte) 0xa2,
+ (byte) 0x4e,
+ (byte) 0x98,
+ (byte) 0x80,
+ (byte) 0x24,
+ (byte) 0xbc,
+ (byte) 0x5b,
+ (byte) 0x71,
+ (byte) 0xe0,
+ (byte) 0x89,
+ (byte) 0x3e,
+ // iBeacon Instance ID (Major/Minor)
+ (byte) 0x44,
+ (byte) 0xd0,
+ (byte) 0x25,
+ (byte) 0x22,
+ // Tx Power
+ (byte) 0xb3,
+ // RSP
+ (byte) 0x08,
+ (byte) 0x09,
+ (byte) 0x4b,
+ (byte) 0x6f,
+ (byte) 0x6e,
+ (byte) 0x74,
+ (byte) 0x61,
+ (byte) 0x6b,
+ (byte) 0x74,
+ (byte) 0x02,
+ (byte) 0x0a,
+ (byte) 0xf4,
+ (byte) 0x0a,
+ (byte) 0x16,
+ (byte) 0x0d,
+ (byte) 0xd0,
+ (byte) 0x74,
+ (byte) 0x6d,
+ (byte) 0x4d,
+ (byte) 0x6b,
+ (byte) 0x32,
+ (byte) 0x36,
+ (byte) 0x64,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00
+ };
+
+ // iBeacon (Apple) Packet 1 with a modified second field.
+ private static final byte[] OTHER_BEACON = {
+ (byte) 0x02, // Length of this Data
+ (byte) 0x02, // <<Flags>>
+ (byte) 0x04, // BR/EDR Not Supported.
+ // Apple Specific Data
+ 26, // length of data that follows
+ (byte) 0xff, // <<Manufacturer Specific Data>>
+ // Company Identifier Code = Apple
+ (byte) 0x4c, // LSB
+ (byte) 0x00, // MSB
+ // iBeacon Header
+ 0x02,
+ // iBeacon Length
+ 0x15,
+ // UUID = PROXIMITY_NOW
+ // IEEE 128-bit UUID represented as UUID[15]: msb To UUID[0]: lsb
+ (byte) 0x14,
+ (byte) 0xe4,
+ (byte) 0xfd,
+ (byte) 0x9f, // UUID[15] - UUID[12]
+ (byte) 0x66,
+ (byte) 0x67,
+ (byte) 0x4c,
+ (byte) 0xcb, // UUID[11] - UUID[08]
+ (byte) 0xa6,
+ (byte) 0x1b,
+ (byte) 0x24,
+ (byte) 0xd0, // UUID[07] - UUID[04]
+ (byte) 0x9a,
+ (byte) 0xb1,
+ (byte) 0x7e,
+ (byte) 0x93, // UUID[03] - UUID[00]
+ // ID as an int (decimal) = 1297482358
+ (byte) 0x76, // Major H
+ (byte) 0x02, // Major L
+ (byte) 0x56, // Minor H
+ (byte) 0x4d, // Minor L
+ // Normalized Tx Power of -77dbm
+ (byte) 0xb3,
+ 0x00, // Zero padding for testing
+ };
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEquals() {
+ BleRecord record = BleRecord.parseFromBytes(BEACON);
+ BleRecord record2 = BleRecord.parseFromBytes(SAME_BEACON);
+
+
+ assertThat(record).isEqualTo(record2);
+
+ // Different items.
+ record2 = BleRecord.parseFromBytes(OTHER_BEACON);
+ assertThat(record).isNotEqualTo(record2);
+ assertThat(record.hashCode()).isNotEqualTo(record2.hashCode());
+ }
+}
+
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/FastPairDecoderTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/FastPairDecoderTest.java
new file mode 100644
index 0000000..1ad04f8
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/FastPairDecoderTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.ble.decode;
+
+import static com.android.server.nearby.common.ble.BleRecord.parseFromBytes;
+import static com.android.server.nearby.common.ble.testing.FastPairTestData.FAST_PAIR_MODEL_ID;
+import static com.android.server.nearby.common.ble.testing.FastPairTestData.getFastPairRecord;
+import static com.android.server.nearby.common.ble.testing.FastPairTestData.newFastPairRecord;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.nearby.common.ble.BleRecord;
+import com.android.server.nearby.util.Hex;
+
+import com.google.common.primitives.Bytes;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class FastPairDecoderTest {
+ private static final String LONG_MODEL_ID = "1122334455667788";
+ private final FastPairDecoder mDecoder = new FastPairDecoder();
+ // Bits 3-6 are model ID length bits = 0b1000 = 8
+ private static final byte LONG_MODEL_ID_HEADER = 0b00010000;
+ private static final String PADDED_LONG_MODEL_ID = "00001111";
+ // Bits 3-6 are model ID length bits = 0b0100 = 4
+ private static final byte PADDED_LONG_MODEL_ID_HEADER = 0b00001000;
+ private static final String TRIMMED_LONG_MODEL_ID = "001111";
+ private static final byte MODEL_ID_HEADER = 0b00000110;
+ private static final String MODEL_ID = "112233";
+ private static final byte BLOOM_FILTER_HEADER = 0b01100000;
+ private static final String BLOOM_FILTER = "112233445566";
+ private static final byte BLOOM_FILTER_SALT_HEADER = 0b00010001;
+ private static final String BLOOM_FILTER_SALT = "01";
+ private static final byte RANDOM_RESOLVABLE_DATA_HEADER = 0b01000110;
+ private static final String RANDOM_RESOLVABLE_DATA = "11223344";
+ private static final byte BLOOM_FILTER_NO_NOTIFICATION_HEADER = 0b01100010;
+
+
+ @Test
+ public void getModelId() {
+ assertThat(mDecoder.getBeaconIdBytes(parseFromBytes(getFastPairRecord())))
+ .isEqualTo(FAST_PAIR_MODEL_ID);
+ FastPairServiceData fastPairServiceData1 =
+ new FastPairServiceData(LONG_MODEL_ID_HEADER,
+ LONG_MODEL_ID);
+ assertThat(
+ mDecoder.getBeaconIdBytes(
+ newBleRecord(fastPairServiceData1.createServiceData())))
+ .isEqualTo(Hex.stringToBytes(LONG_MODEL_ID));
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(PADDED_LONG_MODEL_ID_HEADER,
+ PADDED_LONG_MODEL_ID);
+ assertThat(
+ mDecoder.getBeaconIdBytes(
+ newBleRecord(fastPairServiceData.createServiceData())))
+ .isEqualTo(Hex.stringToBytes(TRIMMED_LONG_MODEL_ID));
+ }
+
+ @Test
+ public void getBloomFilter() {
+ FastPairServiceData fastPairServiceData = new FastPairServiceData(MODEL_ID_HEADER,
+ MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+ assertThat(FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+ }
+
+ @Test
+ public void getBloomFilter_smallModelId() {
+ FastPairServiceData fastPairServiceData = new FastPairServiceData(null, MODEL_ID);
+ assertThat(FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+ .isNull();
+ }
+
+ @Test
+ public void getBloomFilterSalt_modelIdAndMultipleExtraFields() {
+ FastPairServiceData fastPairServiceData = new FastPairServiceData(MODEL_ID_HEADER,
+ MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_SALT_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER_SALT);
+ assertThat(
+ FastPairDecoder.getBloomFilterSalt(fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(BLOOM_FILTER_SALT));
+ }
+
+ @Test
+ public void getRandomResolvableData_whenContainConnectionState() {
+ FastPairServiceData fastPairServiceData = new FastPairServiceData(MODEL_ID_HEADER,
+ MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(RANDOM_RESOLVABLE_DATA_HEADER);
+ fastPairServiceData.mExtraFields.add(RANDOM_RESOLVABLE_DATA);
+ assertThat(
+ FastPairDecoder.getRandomResolvableData(fastPairServiceData
+ .createServiceData()))
+ .isEqualTo(Hex.stringToBytes(RANDOM_RESOLVABLE_DATA));
+ }
+
+ @Test
+ public void getBloomFilterNoNotification() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_NO_NOTIFICATION_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+ assertThat(FastPairDecoder.getBloomFilterNoNotification(fastPairServiceData
+ .createServiceData())).isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+ }
+
+ private static BleRecord newBleRecord(byte[] serviceDataBytes) {
+ return parseFromBytes(newFastPairRecord(serviceDataBytes));
+ }
+ class FastPairServiceData {
+ private Byte mHeader;
+ private String mModelId;
+ List<Byte> mExtraFieldHeaders = new ArrayList<>();
+ List<String> mExtraFields = new ArrayList<>();
+
+ FastPairServiceData(Byte header, String modelId) {
+ this.mHeader = header;
+ this.mModelId = modelId;
+ }
+ private byte[] createServiceData() {
+ if (mExtraFieldHeaders.size() != mExtraFields.size()) {
+ throw new RuntimeException("Number of headers and extra fields must match.");
+ }
+ byte[] serviceData =
+ Bytes.concat(
+ mHeader == null ? new byte[0] : new byte[] {mHeader},
+ mModelId == null ? new byte[0] : Hex.stringToBytes(mModelId));
+ for (int i = 0; i < mExtraFieldHeaders.size(); i++) {
+ serviceData =
+ Bytes.concat(
+ serviceData,
+ mExtraFieldHeaders.get(i) != null
+ ? new byte[] {mExtraFieldHeaders.get(i)}
+ : new byte[0],
+ mExtraFields.get(i) != null
+ ? Hex.stringToBytes(mExtraFields.get(i))
+ : new byte[0]);
+ }
+ return serviceData;
+ }
+ }
+
+
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/RangingUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/RangingUtilsTest.java
new file mode 100644
index 0000000..ebe72b3
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/RangingUtilsTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.ble.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class RangingUtilsTest {
+ // relative error to be used in comparing doubles
+ private static final double DELTA = 1e-5;
+
+ @Test
+ public void distanceFromRssi_getCorrectValue() {
+ // Distance expected to be 1.0 meters based on an RSSI/TxPower of -41dBm
+ // Using params: int rssi (dBm), int calibratedTxPower (dBm)
+ double distance = RangingUtils.distanceFromRssiAndTxPower(-82, -41);
+ assertThat(distance).isWithin(DELTA).of(1.0);
+
+ double distance2 = RangingUtils.distanceFromRssiAndTxPower(-111, -50);
+ assertThat(distance2).isWithin(DELTA).of(10.0);
+
+ //rssi txpower
+ double distance4 = RangingUtils.distanceFromRssiAndTxPower(-50, -29);
+ assertThat(distance4).isWithin(DELTA).of(0.1);
+ }
+
+ @Test
+ public void testRssiFromDistance() {
+ // RSSI expected at 1 meter based on the calibrated tx field of -41dBm
+ // Using params: distance (m), int calibratedTxPower (dBm),
+ int rssi = RangingUtils.rssiFromTargetDistance(1.0, -41);
+
+ assertThat(rssi).isEqualTo(-82);
+ }
+
+ @Test
+ public void testOutOfRange() {
+ double distance = RangingUtils.distanceFromRssiAndTxPower(-200, -41);
+ assertThat(distance).isWithin(DELTA).of(177.82794);
+
+ distance = RangingUtils.distanceFromRssiAndTxPower(200, -41);
+ assertThat(distance).isWithin(DELTA).of(0);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGeneratorTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGeneratorTest.java
new file mode 100644
index 0000000..35a45c0
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AccountKeyGeneratorTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AccountKeyCharacteristic;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Unit tests for {@link AccountKeyGenerator}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AccountKeyGeneratorTest {
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void createAccountKey() throws NoSuchAlgorithmException {
+ byte[] accountKey = AccountKeyGenerator.createAccountKey();
+
+ assertThat(accountKey).hasLength(16);
+ assertThat(accountKey[0]).isEqualTo(AccountKeyCharacteristic.TYPE);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AdditionalDataEncoderTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AdditionalDataEncoderTest.java
new file mode 100644
index 0000000..28d2fca
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AdditionalDataEncoderTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.AdditionalDataEncoder.MAX_LENGTH_OF_DATA;
+import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.KEY_LENGTH;
+import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.NONCE_SIZE;
+import static com.android.server.nearby.common.bluetooth.fastpair.NamingEncoder.EXTRACT_HMAC_SIZE;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * Unit tests for {@link AdditionalDataEncoder}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AdditionalDataEncoderTest {
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void decodeEncodedAdditionalDataPacket_mustGetSameRawData()
+ throws GeneralSecurityException {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ byte[] rawData = base16().decode("00112233445566778899AABBCCDDEEFF");
+
+ byte[] encodedAdditionalDataPacket =
+ AdditionalDataEncoder.encodeAdditionalDataPacket(secret, rawData);
+ byte[] additionalData =
+ AdditionalDataEncoder
+ .decodeAdditionalDataPacket(secret, encodedAdditionalDataPacket);
+
+ assertThat(additionalData).isEqualTo(rawData);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputIncorrectKeySizeToEncode_mustThrowException() {
+ byte[] secret = new byte[KEY_LENGTH - 1];
+ byte[] rawData = base16().decode("00112233445566778899AABBCCDDEEFF");
+
+ GeneralSecurityException exception =
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> AdditionalDataEncoder.encodeAdditionalDataPacket(secret, rawData));
+
+ assertThat(exception)
+ .hasMessageThat()
+ .contains("Incorrect secret for encoding additional data packet");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputIncorrectKeySizeToDecode_mustThrowException() {
+ byte[] secret = new byte[KEY_LENGTH - 1];
+ byte[] packet = base16().decode("01234567890123456789");
+
+ GeneralSecurityException exception =
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> AdditionalDataEncoder.decodeAdditionalDataPacket(secret, packet));
+
+ assertThat(exception)
+ .hasMessageThat()
+ .contains("Incorrect secret for decoding additional data packet");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputTooSmallPacketSize_mustThrowException() {
+ byte[] secret = new byte[KEY_LENGTH];
+ byte[] packet = new byte[EXTRACT_HMAC_SIZE - 1];
+
+ GeneralSecurityException exception =
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> AdditionalDataEncoder.decodeAdditionalDataPacket(secret, packet));
+
+ assertThat(exception).hasMessageThat().contains("Additional data packet size is incorrect");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputTooLargePacketSize_mustThrowException() throws GeneralSecurityException {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ byte[] packet = new byte[MAX_LENGTH_OF_DATA + EXTRACT_HMAC_SIZE + NONCE_SIZE + 1];
+
+ GeneralSecurityException exception =
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> AdditionalDataEncoder.decodeAdditionalDataPacket(secret, packet));
+
+ assertThat(exception).hasMessageThat().contains("Additional data packet size is incorrect");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputIncorrectHmacToDecode_mustThrowException() throws GeneralSecurityException {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ byte[] rawData = base16().decode("00112233445566778899AABBCCDDEEFF");
+
+ byte[] additionalDataPacket = AdditionalDataEncoder
+ .encodeAdditionalDataPacket(secret, rawData);
+ additionalDataPacket[0] = (byte) ~additionalDataPacket[0];
+
+ GeneralSecurityException exception =
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> AdditionalDataEncoder
+ .decodeAdditionalDataPacket(secret, additionalDataPacket));
+
+ assertThat(exception)
+ .hasMessageThat()
+ .contains("Verify HMAC failed, could be incorrect key or packet.");
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryptionTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryptionTest.java
new file mode 100644
index 0000000..7d86037
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryptionTest.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.NONCE_SIZE;
+import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.KEY_LENGTH;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+/** Unit tests for {@link AesCtrMultpleBlockEncryption}. */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AesCtrMultipleBlockEncryptionTest {
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void decryptEncryptedData_nonBlockSizeAligned_mustEqualToPlaintext() throws Exception {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8); // The length is 31.
+
+ byte[] encrypted = AesCtrMultipleBlockEncryption.encrypt(secret, plaintext);
+ byte[] decrypted = AesCtrMultipleBlockEncryption.decrypt(secret, encrypted);
+
+ assertThat(decrypted).isEqualTo(plaintext);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void decryptEncryptedData_blockSizeAligned_mustEqualToPlaintext() throws Exception {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ byte[] plaintext =
+ // The length is 32.
+ base16().decode("0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF");
+
+ byte[] encrypted = AesCtrMultipleBlockEncryption.encrypt(secret, plaintext);
+ byte[] decrypted = AesCtrMultipleBlockEncryption.decrypt(secret, encrypted);
+
+ assertThat(decrypted).isEqualTo(plaintext);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void generateNonceTwice_mustBeDifferent() {
+ byte[] nonce1 = AesCtrMultipleBlockEncryption.generateNonce();
+ byte[] nonce2 = AesCtrMultipleBlockEncryption.generateNonce();
+
+ assertThat(nonce1).isNotEqualTo(nonce2);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void encryptedSamePlaintext_mustBeDifferentEncryptedResult() throws Exception {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8);
+
+ byte[] encrypted1 = AesCtrMultipleBlockEncryption.encrypt(secret, plaintext);
+ byte[] encrypted2 = AesCtrMultipleBlockEncryption.encrypt(secret, plaintext);
+
+ assertThat(encrypted1).isNotEqualTo(encrypted2);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void encryptData_mustBeDifferentToUnencrypted() throws Exception {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8);
+
+ byte[] encrypted = AesCtrMultipleBlockEncryption.encrypt(secret, plaintext);
+
+ assertThat(encrypted).isNotEqualTo(plaintext);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputIncorrectKeySizeToEncrypt_mustThrowException() {
+ byte[] secret = new byte[KEY_LENGTH + 1];
+ byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8);
+
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> AesCtrMultipleBlockEncryption.encrypt(secret, plaintext));
+
+ assertThat(exception)
+ .hasMessageThat()
+ .contains("Incorrect key length for encryption, only supports 16-byte AES Key.");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputIncorrectKeySizeToDecrypt_mustThrowException() {
+ byte[] secret = new byte[KEY_LENGTH - 1];
+ byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8);
+
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> AesCtrMultipleBlockEncryption.decrypt(secret, plaintext));
+
+ assertThat(exception)
+ .hasMessageThat()
+ .contains("Incorrect key length for encryption, only supports 16-byte AES Key.");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputIncorrectDataSizeToDecrypt_mustThrowException()
+ throws GeneralSecurityException {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8);
+
+ byte[] encryptedData = Arrays.copyOfRange(
+ AesCtrMultipleBlockEncryption.encrypt(secret, plaintext), /*from=*/ 0, NONCE_SIZE);
+
+ GeneralSecurityException exception =
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> AesCtrMultipleBlockEncryption.decrypt(secret, encryptedData));
+
+ assertThat(exception).hasMessageThat().contains("Incorrect data length");
+ }
+
+ // Add some random tests that for a certain amount of random plaintext of random length to prove
+ // our encryption/decryption is correct. This is suggested by security team.
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void decryptEncryptedRandomDataForCertainAmount_mustEqualToOriginalData()
+ throws Exception {
+ SecureRandom random = new SecureRandom();
+ for (int i = 0; i < 1000; i++) {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ int dataLength = random.nextInt(64) + 1;
+ byte[] data = new byte[dataLength];
+ random.nextBytes(data);
+
+ byte[] encrypted = AesCtrMultipleBlockEncryption.encrypt(secret, data);
+ byte[] decrypted = AesCtrMultipleBlockEncryption.decrypt(secret, encrypted);
+
+ assertThat(decrypted).isEqualTo(data);
+ }
+ }
+
+ // Add some random tests that for a certain amount of random plaintext of random length to prove
+ // our encryption is correct. This is suggested by security team.
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void twoDistinctEncryptionOnSameRandomData_mustBeDifferentResult() throws Exception {
+ SecureRandom random = new SecureRandom();
+ for (int i = 0; i < 1000; i++) {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ int dataLength = random.nextInt(64) + 1;
+ byte[] data = new byte[dataLength];
+ random.nextBytes(data);
+
+ byte[] encrypted1 = AesCtrMultipleBlockEncryption.encrypt(secret, data);
+ byte[] encrypted2 = AesCtrMultipleBlockEncryption.encrypt(secret, data);
+
+ assertThat(encrypted1).isNotEqualTo(encrypted2);
+ }
+ }
+
+ // Adds this test example on spec. Also we can easily change the parameters(e.g. secret, data,
+ // nonce) to clarify test results with partners.
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputTestExampleToEncrypt_getCorrectResult() throws GeneralSecurityException {
+ byte[] secret = base16().decode("0123456789ABCDEF0123456789ABCDEF");
+ byte[] nonce = base16().decode("0001020304050607");
+
+ // "Someone's Google Headphone".getBytes(UTF_8) is
+ // base16().decode("536F6D656F6E65277320476F6F676C65204865616470686F6E65");
+ byte[] encryptedData =
+ AesCtrMultipleBlockEncryption.doAesCtr(
+ secret,
+ "Someone's Google Headphone".getBytes(UTF_8),
+ nonce);
+
+ assertThat(encryptedData)
+ .isEqualTo(base16().decode("EE4A2483738052E44E9B2A145E5DDFAA44B9E5536AF438E1E5C6"));
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AesEcbSingleBlockEncryptionTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AesEcbSingleBlockEncryptionTest.java
new file mode 100644
index 0000000..eccbd01
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AesEcbSingleBlockEncryptionTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.primitives.Bytes;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link AesEcbSingleBlockEncryption}. */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AesEcbSingleBlockEncryptionTest {
+
+ private static final byte[] PLAINTEXT = base16().decode("F30F4E786C59A7BBF3873B5A49BA97EA");
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void encryptDecryptSuccessful() throws Exception {
+ byte[] secret = AesEcbSingleBlockEncryption.generateKey();
+ byte[] encrypted = AesEcbSingleBlockEncryption.encrypt(secret, PLAINTEXT);
+ assertThat(encrypted).isNotEqualTo(PLAINTEXT);
+ byte[] decrypted = AesEcbSingleBlockEncryption.decrypt(secret, encrypted);
+ assertThat(decrypted).isEqualTo(PLAINTEXT);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void encryptionSizeLimitationEnforced() throws Exception {
+ byte[] secret = AesEcbSingleBlockEncryption.generateKey();
+ byte[] largePacket = Bytes.concat(PLAINTEXT, PLAINTEXT);
+ AesEcbSingleBlockEncryption.encrypt(secret, largePacket);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void decryptionSizeLimitationEnforced() throws Exception {
+ byte[] secret = AesEcbSingleBlockEncryption.generateKey();
+ byte[] largePacket = Bytes.concat(PLAINTEXT, PLAINTEXT);
+ AesEcbSingleBlockEncryption.decrypt(secret, largePacket);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAddressTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAddressTest.java
new file mode 100644
index 0000000..6c95558
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAddressTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.bluetooth.BluetoothAdapter;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link BluetoothAddress}. */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothAddressTest {
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void maskBluetoothAddress_whenInputIsNull() {
+ assertThat(BluetoothAddress.maskBluetoothAddress(null)).isEqualTo("");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void maskBluetoothAddress_whenInputStringNotMatchFormat() {
+ assertThat(BluetoothAddress.maskBluetoothAddress("AA:BB:CC")).isEqualTo("AA:BB:CC");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void maskBluetoothAddress_whenInputStringMatchFormat() {
+ assertThat(BluetoothAddress.maskBluetoothAddress("AA:BB:CC:DD:EE:FF"))
+ .isEqualTo("XX:XX:XX:XX:EE:FF");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void maskBluetoothAddress_whenInputStringContainLowerCaseMatchFormat() {
+ assertThat(BluetoothAddress.maskBluetoothAddress("Aa:Bb:cC:dD:eE:Ff"))
+ .isEqualTo("XX:XX:XX:XX:EE:FF");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void maskBluetoothAddress_whenInputBluetoothDevice() {
+ assertThat(
+ BluetoothAddress.maskBluetoothAddress(
+ BluetoothAdapter.getDefaultAdapter().getRemoteDevice("FF:EE:DD:CC:BB:AA")))
+ .isEqualTo("XX:XX:XX:XX:BB:AA");
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairerTest.java
new file mode 100644
index 0000000..0a56f2f
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairerTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.google.common.collect.Iterables;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+/** Unit tests for {@link BluetoothAudioPairer}. */
+@Presubmit
+@SmallTest
+public class BluetoothAudioPairerTest extends TestCase {
+
+ private static final byte[] SECRET = new byte[]{3, 0};
+ private static final boolean PRIVATE_INITIAL_PAIRING = false;
+ private static final String EVENT_NAME = "EVENT_NAME";
+ private static final BluetoothDevice BLUETOOTH_DEVICE = BluetoothAdapter.getDefaultAdapter()
+ .getRemoteDevice("11:22:33:44:55:66");
+ private static final int BOND_TIMEOUT_SECONDS = 1;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ initMocks(this);
+ BluetoothAudioPairer.enableTestMode();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testKeyBasedPairingInfoConstructor() {
+ assertThat(new BluetoothAudioPairer.KeyBasedPairingInfo(
+ SECRET,
+ null /* GattConnectionManager */,
+ PRIVATE_INITIAL_PAIRING)).isNotNull();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBluetoothAudioPairerConstructor() {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ try {
+ assertThat(new BluetoothAudioPairer(
+ context,
+ BLUETOOTH_DEVICE,
+ Preferences.builder().build(),
+ new EventLoggerWrapper(new TestEventLogger()),
+ null /* KeyBasePairingInfo */,
+ null /*PasskeyConfirmationHandler */,
+ new TimingLogger(EVENT_NAME, Preferences.builder().build()))).isNotNull();
+ } catch (PairingException e) {
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBluetoothAudioPairerUnpairNoCrash() {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ try {
+ new BluetoothAudioPairer(
+ context,
+ BLUETOOTH_DEVICE,
+ Preferences.builder().build(),
+ new EventLoggerWrapper(new TestEventLogger()),
+ null /* KeyBasePairingInfo */,
+ null /*PasskeyConfirmationHandler */,
+ new TimingLogger(EVENT_NAME, Preferences.builder().build())).unpair();
+ } catch (PairingException | InterruptedException | ExecutionException
+ | TimeoutException e) {
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBluetoothAudioPairerPairNoCrash() {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ try {
+ new BluetoothAudioPairer(
+ context,
+ BLUETOOTH_DEVICE,
+ Preferences.builder().setCreateBondTimeoutSeconds(BOND_TIMEOUT_SECONDS).build(),
+ new EventLoggerWrapper(new TestEventLogger()),
+ null /* KeyBasePairingInfo */,
+ null /*PasskeyConfirmationHandler */,
+ new TimingLogger(EVENT_NAME, Preferences.builder().build())).pair();
+ } catch (PairingException | InterruptedException | ExecutionException
+ | TimeoutException e) {
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBluetoothAudioPairerConnectNoCrash() {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ try {
+ new BluetoothAudioPairer(
+ context,
+ BLUETOOTH_DEVICE,
+ Preferences.builder().setCreateBondTimeoutSeconds(BOND_TIMEOUT_SECONDS).build(),
+ new EventLoggerWrapper(new TestEventLogger()),
+ null /* KeyBasePairingInfo */,
+ null /*PasskeyConfirmationHandler */,
+ new TimingLogger(EVENT_NAME, Preferences.builder().build()))
+ .connect(Constants.A2DP_SINK_SERVICE_UUID, true /* enable pairing behavior */);
+ } catch (PairingException | InterruptedException | ExecutionException
+ | TimeoutException | ReflectionException e) {
+ }
+ }
+
+ static class TestEventLogger implements EventLogger {
+
+ private List<Item> mLogs = new ArrayList<>();
+
+ @Override
+ public void logEventSucceeded(Event event) {
+ mLogs.add(new Item(event));
+ }
+
+ @Override
+ public void logEventFailed(Event event, Exception e) {
+ mLogs.add(new ItemFailed(event, e));
+ }
+
+ List<Item> getErrorLogs() {
+ return mLogs.stream().filter(item -> item instanceof ItemFailed)
+ .collect(Collectors.toList());
+ }
+
+ List<Item> getLogs() {
+ return mLogs;
+ }
+
+ List<Item> getLast() {
+ return mLogs.subList(mLogs.size() - 1, mLogs.size());
+ }
+
+ BluetoothDevice getDevice() {
+ return Iterables.getLast(mLogs).mEvent.getBluetoothDevice();
+ }
+
+ public static class Item {
+
+ final Event mEvent;
+
+ Item(Event event) {
+ this.mEvent = event;
+ }
+
+ @Override
+ public String toString() {
+ return "Item{" + "event=" + mEvent + '}';
+ }
+ }
+
+ public static class ItemFailed extends Item {
+
+ final Exception mException;
+
+ ItemFailed(Event event, Exception e) {
+ super(event);
+ this.mException = e;
+ }
+
+ @Override
+ public String toString() {
+ return "ItemFailed{" + "event=" + mEvent + ", exception=" + mException + '}';
+ }
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothUuidsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothUuidsTest.java
new file mode 100644
index 0000000..fa977ed
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothUuidsTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.UUID;
+
+/** Unit tests for {@link BluetoothUuids}. */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothUuidsTest {
+
+ // According to {@code android.bluetooth.BluetoothUuid}
+ private static final short A2DP_SINK_SHORT_UUID = (short) 0x110B;
+ private static final UUID A2DP_SINK_CHARACTERISTICS =
+ UUID.fromString("0000110B-0000-1000-8000-00805F9B34FB");
+
+ // According to {go/fastpair-128bit-gatt}, the short uuid locates at the 3rd and 4th bytes based
+ // on the Fast Pair custom GATT characteristics 128-bit UUIDs base -
+ // "FE2C0000-8366-4814-8EB0-01DE32100BEA".
+ private static final short CUSTOM_SHORT_UUID = (short) 0x9487;
+ private static final UUID CUSTOM_CHARACTERISTICS =
+ UUID.fromString("FE2C9487-8366-4814-8EB0-01DE32100BEA");
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void get16BitUuid() {
+ assertThat(BluetoothUuids.get16BitUuid(A2DP_SINK_CHARACTERISTICS))
+ .isEqualTo(A2DP_SINK_SHORT_UUID);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void is16BitUuid() {
+ assertThat(BluetoothUuids.is16BitUuid(A2DP_SINK_CHARACTERISTICS)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void to128BitUuid() {
+ assertThat(BluetoothUuids.to128BitUuid(A2DP_SINK_SHORT_UUID))
+ .isEqualTo(A2DP_SINK_CHARACTERISTICS);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void toFastPair128BitUuid() {
+ assertThat(BluetoothUuids.toFastPair128BitUuid(CUSTOM_SHORT_UUID))
+ .isEqualTo(CUSTOM_CHARACTERISTICS);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/ConstantsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/ConstantsTest.java
new file mode 100644
index 0000000..f7ffa24
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/ConstantsTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.to128BitUuid;
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.toFastPair128BitUuid;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+
+import androidx.test.filters.SdkSuppress;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
+
+import junit.framework.TestCase;
+
+import org.mockito.Mock;
+
+import java.util.UUID;
+
+/**
+ * Unit tests for {@link Constants}.
+ */
+public class ConstantsTest extends TestCase {
+
+ @Mock
+ private BluetoothGattConnection mMockGattConnection;
+
+ private static final UUID OLD_KEY_BASE_PAIRING_CHARACTERISTICS = to128BitUuid((short) 0x1234);
+
+ private static final UUID NEW_KEY_BASE_PAIRING_CHARACTERISTICS =
+ toFastPair128BitUuid((short) 0x1234);
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ initMocks(this);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getId_whenSupportNewCharacteristics() throws BluetoothException {
+ when(mMockGattConnection.getCharacteristic(any(UUID.class), any(UUID.class)))
+ .thenReturn(new BluetoothGattCharacteristic(NEW_KEY_BASE_PAIRING_CHARACTERISTICS, 0,
+ 0));
+
+ assertThat(KeyBasedPairingCharacteristic.getId(mMockGattConnection))
+ .isEqualTo(NEW_KEY_BASE_PAIRING_CHARACTERISTICS);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getId_whenNotSupportNewCharacteristics() throws BluetoothException {
+ // {@link BluetoothGattConnection#getCharacteristic(UUID, UUID)} throws {@link
+ // BluetoothException} if the characteristic not found .
+ when(mMockGattConnection.getCharacteristic(any(UUID.class), any(UUID.class)))
+ .thenThrow(new BluetoothException(""));
+
+ assertThat(KeyBasedPairingCharacteristic.getId(mMockGattConnection))
+ .isEqualTo(OLD_KEY_BASE_PAIRING_CHARACTERISTICS);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EllipticCurveDiffieHellmanExchangeTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EllipticCurveDiffieHellmanExchangeTest.java
new file mode 100644
index 0000000..3719783
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EllipticCurveDiffieHellmanExchangeTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.io.BaseEncoding.base64;
+import static com.google.common.primitives.Bytes.concat;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link EllipticCurveDiffieHellmanExchange}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class EllipticCurveDiffieHellmanExchangeTest {
+
+ public static final byte[] ANTI_SPOOF_PUBLIC_KEY = base64().decode(
+ "d2JTfvfdS6u7LmGfMOmco3C7ra3lW1k17AOly0LrBydDZURacfTYIMmo5K1ejfD9e8b6qHs"
+ + "DTNzselhifi10kQ==");
+ public static final byte[] ANTI_SPOOF_PRIVATE_KEY =
+ base64().decode("Rn9GbLRPQTFc2O7WFVGkydzcUS9Tuj7R9rLh6EpLtuU=");
+
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void generateCommonKey() throws Exception {
+ EllipticCurveDiffieHellmanExchange bob = EllipticCurveDiffieHellmanExchange.create();
+ EllipticCurveDiffieHellmanExchange alice = EllipticCurveDiffieHellmanExchange.create();
+
+ assertThat(bob.getPublicKey()).isNotEqualTo(alice.getPublicKey());
+ assertThat(bob.getPrivateKey()).isNotEqualTo(alice.getPrivateKey());
+
+ assertThat(bob.generateSecret(alice.getPublicKey()))
+ .isEqualTo(alice.generateSecret(bob.getPublicKey()));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void generateCommonKey_withExistingPrivateKey() throws Exception {
+ EllipticCurveDiffieHellmanExchange bob = EllipticCurveDiffieHellmanExchange.create();
+ EllipticCurveDiffieHellmanExchange alice =
+ EllipticCurveDiffieHellmanExchange.create(ANTI_SPOOF_PRIVATE_KEY);
+
+ assertThat(alice.generateSecret(bob.getPublicKey()))
+ .isEqualTo(bob.generateSecret(ANTI_SPOOF_PUBLIC_KEY));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void generateCommonKey_soundcoreAntiSpoofingKey_generatedTooShort() throws Exception {
+ // This soundcore device has a public key that was generated which starts with 0x0. This was
+ // stripped out in our database, but this test confirms that adding that byte back fixes the
+ // issue and allows the generated secrets to match each other.
+ byte[] soundCorePublicKey = concat(new byte[]{0}, base64().decode(
+ "EYapuIsyw/nwHAdMxr12FCtAi4gY3EtuW06JuKDg4SA76IoIDVeol2vsGKy0Ea2Z00"
+ + "ArOTiBDsk0L+4Xo9AA"));
+ byte[] soundCorePrivateKey = base64()
+ .decode("lW5idsrfX7cBC8kO/kKn3w3GXirqt9KnJoqXUcOMhjM=");
+ EllipticCurveDiffieHellmanExchange bob = EllipticCurveDiffieHellmanExchange.create();
+ EllipticCurveDiffieHellmanExchange alice =
+ EllipticCurveDiffieHellmanExchange.create(soundCorePrivateKey);
+
+ assertThat(alice.generateSecret(bob.getPublicKey()))
+ .isEqualTo(bob.generateSecret(soundCorePublicKey));
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java
new file mode 100644
index 0000000..28e925f
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link Event}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class EventTest {
+
+ private static final String EXCEPTION_MESSAGE = "Test exception";
+ private static final long TIMESTAMP = 1234L;
+ private static final @EventCode int EVENT_CODE = EventCode.CREATE_BOND;
+ private static final BluetoothDevice BLUETOOTH_DEVICE = BluetoothAdapter.getDefaultAdapter()
+ .getRemoteDevice("11:22:33:44:55:66");
+ private static final Short PROFILE = (short) 1;
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void createAndReadFromParcel() {
+ Event event =
+ Event.builder()
+ .setException(new Exception(EXCEPTION_MESSAGE))
+ .setTimestamp(TIMESTAMP)
+ .setEventCode(EVENT_CODE)
+ .setBluetoothDevice(BLUETOOTH_DEVICE)
+ .setProfile(PROFILE)
+ .build();
+
+ Parcel parcel = Parcel.obtain();
+ event.writeToParcel(parcel, event.describeContents());
+ parcel.setDataPosition(0);
+ Event result = Event.CREATOR.createFromParcel(parcel);
+
+ assertThat(result.getException()).hasMessageThat()
+ .isEqualTo(event.getException().getMessage());
+ assertThat(result.getTimestamp()).isEqualTo(event.getTimestamp());
+ assertThat(result.getEventCode()).isEqualTo(event.getEventCode());
+ assertThat(result.getBluetoothDevice()).isEqualTo(event.getBluetoothDevice());
+ assertThat(result.getProfile()).isEqualTo(event.getProfile());
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnectionTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnectionTest.java
new file mode 100644
index 0000000..a103a72
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnectionTest.java
@@ -0,0 +1,370 @@
+/*
+ * 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.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection.GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED;
+import static com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection.GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST;
+import static com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection.GATT_ERROR_CODE_TIMEOUT;
+import static com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection.appendMoreErrorCode;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyShort;
+import static org.mockito.Mockito.doNothing;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.BluetoothGattException;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.protobuf.ByteString;
+
+import junit.framework.TestCase;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+/**
+ * Unit tests for {@link FastPairDualConnection}.
+ */
+@Presubmit
+@SmallTest
+public class FastPairDualConnectionTest extends TestCase {
+
+ private static final String BLE_ADDRESS = "00:11:22:33:FF:EE";
+ private static final String MASKED_BLE_ADDRESS = "MASKED_BLE_ADDRESS";
+ private static final short[] PROFILES = {Constants.A2DP_SINK_SERVICE_UUID};
+ private static final int NUM_CONNECTION_ATTEMPTS = 1;
+ private static final boolean ENABLE_PAIRING_BEHAVIOR = true;
+ private static final BluetoothDevice BLUETOOTH_DEVICE = BluetoothAdapter.getDefaultAdapter()
+ .getRemoteDevice("11:22:33:44:55:66");
+ private static final String DEVICE_NAME = "DEVICE_NAME";
+ private static final byte[] ACCOUNT_KEY = new byte[]{1, 3};
+ private static final byte[] HASH_VALUE = new byte[]{7};
+
+ private TestEventLogger mEventLogger;
+ @Mock private TimingLogger mTimingLogger;
+ @Mock private BluetoothAudioPairer mBluetoothAudioPairer;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ BluetoothAudioPairer.enableTestMode();
+ FastPairDualConnection.enableTestMode();
+ MockitoAnnotations.initMocks(this);
+
+ doNothing().when(mBluetoothAudioPairer).connect(anyShort(), anyBoolean());
+ mEventLogger = new TestEventLogger();
+ }
+
+ private FastPairDualConnection newFastPairDualConnection(
+ String bleAddress, Preferences.Builder prefsBuilder) {
+ return new FastPairDualConnection(
+ ApplicationProvider.getApplicationContext(),
+ bleAddress,
+ prefsBuilder.build(),
+ mEventLogger,
+ mTimingLogger);
+ }
+
+ private FastPairDualConnection newFastPairDualConnection2(
+ String bleAddress, Preferences.Builder prefsBuilder) {
+ return new FastPairDualConnection(
+ ApplicationProvider.getApplicationContext(),
+ bleAddress,
+ prefsBuilder.build(),
+ mEventLogger);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testFastPairDualConnectionConstructor() {
+ assertThat(newFastPairDualConnection(BLE_ADDRESS, Preferences.builder())).isNotNull();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testFastPairDualConnectionConstructor2() {
+ assertThat(newFastPairDualConnection2(BLE_ADDRESS, Preferences.builder())).isNotNull();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testAttemptConnectProfiles() {
+ try {
+ new FastPairDualConnection(
+ ApplicationProvider.getApplicationContext(),
+ BLE_ADDRESS,
+ Preferences.builder().build(),
+ mEventLogger,
+ mTimingLogger)
+ .attemptConnectProfiles(
+ mBluetoothAudioPairer,
+ MASKED_BLE_ADDRESS,
+ PROFILES,
+ NUM_CONNECTION_ATTEMPTS,
+ ENABLE_PAIRING_BEHAVIOR);
+ } catch (PairingException e) {
+ // Mocked pair doesn't throw Pairing Exception.
+ }
+ }
+
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testAppendMoreErrorCode_gattError() {
+ assertThat(
+ appendMoreErrorCode(
+ GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED,
+ new BluetoothGattException("Test", 133)))
+ .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED + 133);
+ assertThat(
+ appendMoreErrorCode(
+ GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED,
+ new BluetoothGattException("Test", 257)))
+ .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED + 257);
+ assertThat(
+ appendMoreErrorCode(
+ GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED, new BluetoothException("Test")))
+ .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED);
+ assertThat(
+ appendMoreErrorCode(
+ GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED,
+ new BluetoothOperationTimeoutException("Test")))
+ .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED + GATT_ERROR_CODE_TIMEOUT);
+ assertThat(
+ appendMoreErrorCode(
+ GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST,
+ new BluetoothGattException("Test", 41)))
+ .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST + 41);
+ assertThat(
+ appendMoreErrorCode(
+ GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST,
+ new BluetoothGattException("Test", 788)))
+ .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST + 788);
+ assertThat(
+ appendMoreErrorCode(
+ GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST, new BluetoothException("Test")))
+ .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST);
+ assertThat(
+ appendMoreErrorCode(
+ GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST,
+ new BluetoothOperationTimeoutException("Test")))
+ .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST + GATT_ERROR_CODE_TIMEOUT);
+ assertThat(appendMoreErrorCode(GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST, /* cause= */ null))
+ .isEqualTo(GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testUnpairNotCrash() {
+ try {
+ new FastPairDualConnection(
+ ApplicationProvider.getApplicationContext(),
+ BLE_ADDRESS,
+ Preferences.builder().build(),
+ mEventLogger,
+ mTimingLogger).unpair(BLUETOOTH_DEVICE);
+ } catch (ExecutionException | InterruptedException | ReflectionException
+ | TimeoutException | PairingException e) {
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSetFastPairHistory() {
+ new FastPairDualConnection(
+ ApplicationProvider.getApplicationContext(),
+ BLE_ADDRESS,
+ Preferences.builder().build(),
+ mEventLogger,
+ mTimingLogger).setFastPairHistory(ImmutableList.of());
+ }
+
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSetGetProviderDeviceName() {
+ FastPairDualConnection connection = new FastPairDualConnection(
+ ApplicationProvider.getApplicationContext(),
+ BLE_ADDRESS,
+ Preferences.builder().build(),
+ mEventLogger,
+ mTimingLogger);
+ connection.setProviderDeviceName(DEVICE_NAME);
+ connection.getProviderDeviceName();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetExistingAccountKey() {
+ FastPairDualConnection connection = new FastPairDualConnection(
+ ApplicationProvider.getApplicationContext(),
+ BLE_ADDRESS,
+ Preferences.builder().build(),
+ mEventLogger,
+ mTimingLogger);
+ connection.getExistingAccountKey();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testPair() {
+ FastPairDualConnection connection = new FastPairDualConnection(
+ ApplicationProvider.getApplicationContext(),
+ BLE_ADDRESS,
+ Preferences.builder().setNumSdpAttempts(0)
+ .setLogPairWithCachedModelId(false).build(),
+ mEventLogger,
+ mTimingLogger);
+ try {
+ connection.pair();
+ } catch (BluetoothException | InterruptedException | ReflectionException
+ | ExecutionException | TimeoutException | PairingException e) {
+ }
+ }
+
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetPublicAddress() {
+ FastPairDualConnection connection = new FastPairDualConnection(
+ ApplicationProvider.getApplicationContext(),
+ BLE_ADDRESS,
+ Preferences.builder().setNumSdpAttempts(0)
+ .setLogPairWithCachedModelId(false).build(),
+ mEventLogger,
+ mTimingLogger);
+ connection.getPublicAddress();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testShouldWriteAccountKeyForExistingCase() {
+ FastPairDualConnection connection = new FastPairDualConnection(
+ ApplicationProvider.getApplicationContext(),
+ BLE_ADDRESS,
+ Preferences.builder().setNumSdpAttempts(0)
+ .setLogPairWithCachedModelId(false).build(),
+ mEventLogger,
+ mTimingLogger);
+ connection.shouldWriteAccountKeyForExistingCase(ACCOUNT_KEY);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testReadFirmwareVersion() {
+ FastPairDualConnection connection = new FastPairDualConnection(
+ ApplicationProvider.getApplicationContext(),
+ BLE_ADDRESS,
+ Preferences.builder().setNumSdpAttempts(0)
+ .setLogPairWithCachedModelId(false).build(),
+ mEventLogger,
+ mTimingLogger);
+ try {
+ connection.readFirmwareVersion();
+ } catch (BluetoothException | InterruptedException | ExecutionException
+ | TimeoutException e) {
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testHistoryItem() {
+ FastPairDualConnection connection = new FastPairDualConnection(
+ ApplicationProvider.getApplicationContext(),
+ BLE_ADDRESS,
+ Preferences.builder().setNumSdpAttempts(0)
+ .setLogPairWithCachedModelId(false).build(),
+ mEventLogger,
+ mTimingLogger);
+ ImmutableList.Builder<FastPairHistoryItem> historyBuilder = ImmutableList.builder();
+ FastPairHistoryItem historyItem1 =
+ FastPairHistoryItem.create(
+ ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(HASH_VALUE));
+ historyBuilder.add(historyItem1);
+
+ connection.setFastPairHistory(historyBuilder.build());
+ assertThat(connection.mPairedHistoryFinder.isInPairedHistory("11:22:33:44:55:88"))
+ .isFalse();
+ }
+
+ static class TestEventLogger implements EventLogger {
+
+ private List<Item> mLogs = new ArrayList<>();
+
+ @Override
+ public void logEventSucceeded(Event event) {
+ mLogs.add(new Item(event));
+ }
+
+ @Override
+ public void logEventFailed(Event event, Exception e) {
+ mLogs.add(new ItemFailed(event, e));
+ }
+
+ List<Item> getErrorLogs() {
+ return mLogs.stream().filter(item -> item instanceof ItemFailed)
+ .collect(Collectors.toList());
+ }
+
+ List<Item> getLogs() {
+ return mLogs;
+ }
+
+ List<Item> getLast() {
+ return mLogs.subList(mLogs.size() - 1, mLogs.size());
+ }
+
+ BluetoothDevice getDevice() {
+ return Iterables.getLast(mLogs).mEvent.getBluetoothDevice();
+ }
+
+ public static class Item {
+
+ final Event mEvent;
+
+ Item(Event event) {
+ this.mEvent = event;
+ }
+
+ @Override
+ public String toString() {
+ return "Item{" + "event=" + mEvent + '}';
+ }
+ }
+
+ public static class ItemFailed extends Item {
+
+ final Exception mException;
+
+ ItemFailed(Event event, Exception e) {
+ super(event);
+ this.mException = e;
+ }
+
+ @Override
+ public String toString() {
+ return "ItemFailed{" + "event=" + mEvent + ", exception=" + mException + '}';
+ }
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairHistoryItemTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairHistoryItemTest.java
new file mode 100644
index 0000000..b47fd89
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairHistoryItemTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.primitives.Bytes.concat;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.hash.Hashing;
+import com.google.protobuf.ByteString;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link FastPairHistoryItem}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class FastPairHistoryItemTest {
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputMatchedPublicAddress_isMatchedReturnTrue() {
+ final byte[] accountKey = base16().decode("0123456789ABCDEF");
+ final byte[] publicAddress = BluetoothAddress.decode("11:22:33:44:55:66");
+ final byte[] hashValue =
+ Hashing.sha256().hashBytes(concat(accountKey, publicAddress)).asBytes();
+
+ FastPairHistoryItem historyItem =
+ FastPairHistoryItem
+ .create(ByteString.copyFrom(accountKey), ByteString.copyFrom(hashValue));
+
+ assertThat(historyItem.isMatched(publicAddress)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputNotMatchedPublicAddress_isMatchedReturnFalse() {
+ final byte[] accountKey = base16().decode("0123456789ABCDEF");
+ final byte[] publicAddress1 = BluetoothAddress.decode("11:22:33:44:55:66");
+ final byte[] publicAddress2 = BluetoothAddress.decode("11:22:33:44:55:77");
+ final byte[] hashValue =
+ Hashing.sha256().hashBytes(concat(accountKey, publicAddress1)).asBytes();
+
+ FastPairHistoryItem historyItem =
+ FastPairHistoryItem
+ .create(ByteString.copyFrom(accountKey), ByteString.copyFrom(hashValue));
+
+ assertThat(historyItem.isMatched(publicAddress2)).isFalse();
+ }
+}
+
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManagerTest.java
new file mode 100644
index 0000000..2f80a30
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManagerTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.BluetoothGattException;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
+
+import com.google.common.collect.ImmutableSet;
+
+import junit.framework.TestCase;
+
+import java.time.Duration;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Unit tests for {@link GattConnectionManager}.
+ */
+@Presubmit
+@SmallTest
+public class GattConnectionManagerTest extends TestCase {
+
+ private static final String FAST_PAIR_ADDRESS = "BB:BB:BB:BB:BB:1E";
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ GattConnectionManager.enableTestMode();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGattConnectionManagerConstructor() throws Exception {
+ GattConnectionManager manager = createManager(Preferences.builder());
+ try {
+ manager.getConnection();
+ } catch (ExecutionException e) {
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testIsNoRetryError() {
+ Preferences preferences =
+ Preferences.builder()
+ .setGattConnectionAndSecretHandshakeNoRetryGattError(
+ ImmutableSet.of(257, 999))
+ .build();
+
+ assertThat(
+ GattConnectionManager.isNoRetryError(
+ preferences, new BluetoothGattException("Test", 133)))
+ .isFalse();
+ assertThat(
+ GattConnectionManager.isNoRetryError(
+ preferences, new BluetoothGattException("Test", 257)))
+ .isTrue();
+ assertThat(
+ GattConnectionManager.isNoRetryError(
+ preferences, new BluetoothGattException("Test", 999)))
+ .isTrue();
+ assertThat(GattConnectionManager.isNoRetryError(
+ preferences, new BluetoothException("Test")))
+ .isFalse();
+ assertThat(
+ GattConnectionManager.isNoRetryError(
+ preferences, new BluetoothOperationTimeoutException("Test")))
+ .isFalse();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetTimeoutNotOverShortRetryMaxSpentTimeGetShort() {
+ Preferences preferences = Preferences.builder().build();
+
+ assertThat(
+ createManager(Preferences.builder(), () -> {})
+ .getTimeoutMs(
+ preferences.getGattConnectShortTimeoutRetryMaxSpentTimeMs() - 1))
+ .isEqualTo(preferences.getGattConnectShortTimeoutMs());
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetTimeoutOverShortRetryMaxSpentTimeGetLong() {
+ Preferences preferences = Preferences.builder().build();
+
+ assertThat(
+ createManager(Preferences.builder(), () -> {})
+ .getTimeoutMs(
+ preferences.getGattConnectShortTimeoutRetryMaxSpentTimeMs() + 1))
+ .isEqualTo(preferences.getGattConnectLongTimeoutMs());
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetTimeoutRetryNotEnabledGetOrigin() {
+ Preferences preferences = Preferences.builder().build();
+
+ assertThat(
+ createManager(
+ Preferences.builder().setRetryGattConnectionAndSecretHandshake(false),
+ () -> {})
+ .getTimeoutMs(0))
+ .isEqualTo(Duration.ofSeconds(
+ preferences.getGattConnectionTimeoutSeconds()).toMillis());
+ }
+
+ private GattConnectionManager createManager(Preferences.Builder prefs) {
+ return createManager(prefs, () -> {});
+ }
+
+ private GattConnectionManager createManager(
+ Preferences.Builder prefs, ToggleBluetoothTask toggleBluetooth) {
+ return createManager(prefs, toggleBluetooth,
+ /* fastPairSignalChecker= */ null);
+ }
+
+ private GattConnectionManager createManager(
+ Preferences.Builder prefs,
+ ToggleBluetoothTask toggleBluetooth,
+ @Nullable FastPairConnection.FastPairSignalChecker fastPairSignalChecker) {
+ return new GattConnectionManager(
+ ApplicationProvider.getApplicationContext(),
+ prefs.build(),
+ new EventLoggerWrapper(null),
+ BluetoothAdapter.getDefaultAdapter(),
+ toggleBluetooth,
+ FAST_PAIR_ADDRESS,
+ new TimingLogger("GattConnectionManager", prefs.build()),
+ fastPairSignalChecker,
+ /* setMtu= */ false);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HeadsetPieceTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HeadsetPieceTest.java
new file mode 100644
index 0000000..670b2ca
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HeadsetPieceTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link HeadsetPiece}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class HeadsetPieceTest {
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void parcelAndUnparcel() {
+ HeadsetPiece headsetPiece = createDefaultHeadset().build();
+ Parcel expectedParcel = Parcel.obtain();
+ headsetPiece.writeToParcel(expectedParcel, 0);
+ expectedParcel.setDataPosition(0);
+
+ HeadsetPiece fromParcel = HeadsetPiece.CREATOR.createFromParcel(expectedParcel);
+
+ assertThat(fromParcel).isEqualTo(headsetPiece);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void parcelAndUnparcel_nullImageContentUri() {
+ HeadsetPiece headsetPiece = createDefaultHeadset().setImageContentUri(null).build();
+ Parcel expectedParcel = Parcel.obtain();
+ headsetPiece.writeToParcel(expectedParcel, 0);
+ expectedParcel.setDataPosition(0);
+
+ HeadsetPiece fromParcel = HeadsetPiece.CREATOR.createFromParcel(expectedParcel);
+
+ assertThat(fromParcel).isEqualTo(headsetPiece);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void equals() {
+ HeadsetPiece headsetPiece = createDefaultHeadset().build();
+
+ HeadsetPiece compareTo = createDefaultHeadset().build();
+
+ assertThat(headsetPiece).isEqualTo(compareTo);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void equals_nullImageContentUri() {
+ HeadsetPiece headsetPiece = createDefaultHeadset().setImageContentUri(null).build();
+
+ HeadsetPiece compareTo = createDefaultHeadset().setImageContentUri(null).build();
+
+ assertThat(headsetPiece).isEqualTo(compareTo);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void notEquals_differentLowLevelThreshold() {
+ HeadsetPiece headsetPiece = createDefaultHeadset().build();
+
+ HeadsetPiece compareTo = createDefaultHeadset().setLowLevelThreshold(1).build();
+
+ assertThat(headsetPiece).isNotEqualTo(compareTo);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void notEquals_differentBatteryLevel() {
+ HeadsetPiece headsetPiece = createDefaultHeadset().build();
+
+ HeadsetPiece compareTo = createDefaultHeadset().setBatteryLevel(99).build();
+
+ assertThat(headsetPiece).isNotEqualTo(compareTo);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void notEquals_differentImageUrl() {
+ HeadsetPiece headsetPiece = createDefaultHeadset().build();
+
+ HeadsetPiece compareTo =
+ createDefaultHeadset().setImageUrl("http://fake.image.path/different.png").build();
+
+ assertThat(headsetPiece).isNotEqualTo(compareTo);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void notEquals_differentChargingState() {
+ HeadsetPiece headsetPiece = createDefaultHeadset().build();
+
+ HeadsetPiece compareTo = createDefaultHeadset().setCharging(false).build();
+
+ assertThat(headsetPiece).isNotEqualTo(compareTo);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void notEquals_differentImageContentUri() {
+ HeadsetPiece headsetPiece = createDefaultHeadset().build();
+
+ HeadsetPiece compareTo =
+ createDefaultHeadset().setImageContentUri(Uri.parse("content://different.png"))
+ .build();
+
+ assertThat(headsetPiece).isNotEqualTo(compareTo);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void notEquals_nullImageContentUri() {
+ HeadsetPiece headsetPiece = createDefaultHeadset().build();
+
+ HeadsetPiece compareTo = createDefaultHeadset().setImageContentUri(null).build();
+
+ assertThat(headsetPiece).isNotEqualTo(compareTo);
+ }
+
+ private static HeadsetPiece.Builder createDefaultHeadset() {
+ return HeadsetPiece.builder()
+ .setLowLevelThreshold(30)
+ .setBatteryLevel(18)
+ .setImageUrl("http://fake.image.path/image.png")
+ .setImageContentUri(Uri.parse("content://image.png"))
+ .setCharging(true);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void isLowBattery() {
+ HeadsetPiece headsetPiece =
+ HeadsetPiece.builder()
+ .setLowLevelThreshold(30)
+ .setBatteryLevel(18)
+ .setImageUrl("http://fake.image.path/image.png")
+ .setCharging(false)
+ .build();
+
+ assertThat(headsetPiece.isBatteryLow()).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void isNotLowBattery() {
+ HeadsetPiece headsetPiece =
+ HeadsetPiece.builder()
+ .setLowLevelThreshold(30)
+ .setBatteryLevel(31)
+ .setImageUrl("http://fake.image.path/image.png")
+ .setCharging(false)
+ .build();
+
+ assertThat(headsetPiece.isBatteryLow()).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void isNotLowBattery_whileCharging() {
+ HeadsetPiece headsetPiece =
+ HeadsetPiece.builder()
+ .setLowLevelThreshold(30)
+ .setBatteryLevel(18)
+ .setImageUrl("http://fake.image.path/image.png")
+ .setCharging(true)
+ .build();
+
+ assertThat(headsetPiece.isBatteryLow()).isFalse();
+ }
+}
+
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HmacSha256Test.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HmacSha256Test.java
new file mode 100644
index 0000000..8db3b97
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HmacSha256Test.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.KEY_LENGTH;
+import static com.android.server.nearby.common.bluetooth.fastpair.HmacSha256.HMAC_SHA256_BLOCK_SIZE;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.primitives.Bytes.concat;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.base.Preconditions;
+import com.google.common.hash.Hashing;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * Unit tests for {@link HmacSha256}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class HmacSha256Test {
+
+ private static final int EXTRACT_HMAC_SIZE = 8;
+ private static final byte OUTER_PADDING_BYTE = 0x5c;
+ private static final byte INNER_PADDING_BYTE = 0x36;
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void compareResultWithOurImplementation_mustBeIdentical()
+ throws GeneralSecurityException {
+ Random random = new Random(0xFE2C);
+
+ for (int i = 0; i < 1000; i++) {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ // Avoid too small data size that may cause false alarm.
+ int dataLength = random.nextInt(64);
+ byte[] data = new byte[dataLength];
+ random.nextBytes(data);
+
+ assertThat(HmacSha256.build(secret, data)).isEqualTo(doHmacSha256(secret, data));
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputIncorrectKeySizeToDecrypt_mustThrowException() {
+ byte[] secret = new byte[KEY_LENGTH - 1];
+ byte[] data = base16().decode("1234567890ABCDEF1234567890ABCDEF1234567890ABCD");
+
+ GeneralSecurityException exception =
+ assertThrows(GeneralSecurityException.class, () -> HmacSha256.build(secret, data));
+
+ assertThat(exception)
+ .hasMessageThat()
+ .contains("Incorrect key length, should be the AES-128 key.");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputTwoIdenticalArrays_compareTwoHmacMustReturnTrue() {
+ Random random = new Random(0x1237);
+ byte[] array1 = new byte[EXTRACT_HMAC_SIZE];
+ random.nextBytes(array1);
+ byte[] array2 = Arrays.copyOf(array1, array1.length);
+
+ assertThat(HmacSha256.compareTwoHMACs(array1, array2)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputTwoRandomArrays_compareTwoHmacMustReturnFalse() {
+ Random random = new Random(0xff);
+ byte[] array1 = new byte[EXTRACT_HMAC_SIZE];
+ random.nextBytes(array1);
+ byte[] array2 = new byte[EXTRACT_HMAC_SIZE];
+ random.nextBytes(array2);
+
+ assertThat(HmacSha256.compareTwoHMACs(array1, array2)).isFalse();
+ }
+
+ // HMAC-SHA256 may not be previously defined on Bluetooth platforms, so we explicitly create
+ // the code on test case. This will allow us to easily detect where partner implementation might
+ // have gone wrong or where our spec isn't clear enough.
+ static byte[] doHmacSha256(byte[] key, byte[] data) {
+
+ Preconditions.checkArgument(
+ key != null && key.length == KEY_LENGTH && data != null,
+ "Parameters can't be null.");
+
+ // Performs SHA256(concat((key ^ opad),SHA256(concat((key ^ ipad), data)))), where
+ // key is the given secret extended to 64 bytes by concat(secret, ZEROS).
+ // opad is 64 bytes outer padding, consisting of repeated bytes valued 0x5c.
+ // ipad is 64 bytes inner padding, consisting of repeated bytes valued 0x36.
+ byte[] keyIpad = new byte[HMAC_SHA256_BLOCK_SIZE];
+ byte[] keyOpad = new byte[HMAC_SHA256_BLOCK_SIZE];
+
+ for (int i = 0; i < KEY_LENGTH; i++) {
+ keyIpad[i] = (byte) (key[i] ^ INNER_PADDING_BYTE);
+ keyOpad[i] = (byte) (key[i] ^ OUTER_PADDING_BYTE);
+ }
+ Arrays.fill(keyIpad, KEY_LENGTH, HMAC_SHA256_BLOCK_SIZE, INNER_PADDING_BYTE);
+ Arrays.fill(keyOpad, KEY_LENGTH, HMAC_SHA256_BLOCK_SIZE, OUTER_PADDING_BYTE);
+
+ byte[] innerSha256Result = Hashing.sha256().hashBytes(concat(keyIpad, data)).asBytes();
+ return Hashing.sha256().hashBytes(concat(keyOpad, innerSha256Result)).asBytes();
+ }
+
+ // Adds this test example on spec. Also we can easily change the parameters(e.g. secret, data)
+ // to clarify test results with partners.
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputTestExampleToHmacSha256_getCorrectResult() {
+ byte[] secret = base16().decode("0123456789ABCDEF0123456789ABCDEF");
+ byte[] data =
+ base16().decode(
+ "0001020304050607EE4A2483738052E44E9B2A145E5DDFAA44B9E5536AF438E1E5C6");
+
+ byte[] hmacResult = doHmacSha256(secret, data);
+
+ assertThat(hmacResult)
+ .isEqualTo(base16().decode(
+ "55EC5E6055AF6E92618B7D8710D4413709AB5DA27CA26A66F52E5AD4E8209052"));
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoderTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoderTest.java
new file mode 100644
index 0000000..d4c3342
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoderTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.MessageStreamHmacEncoder.EXTRACT_HMAC_SIZE;
+import static com.android.server.nearby.common.bluetooth.fastpair.MessageStreamHmacEncoder.SECTION_NONCE_LENGTH;
+
+import static com.google.common.primitives.Bytes.concat;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+/**
+ * Unit tests for {@link MessageStreamHmacEncoder}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MessageStreamHmacEncoderTest {
+
+ private static final int ACCOUNT_KEY_LENGTH = 16;
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void encodeMessagePacket() throws GeneralSecurityException {
+ int messageLength = 2;
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] accountKey = new byte[ACCOUNT_KEY_LENGTH];
+ secureRandom.nextBytes(accountKey);
+ byte[] data = new byte[messageLength];
+ secureRandom.nextBytes(data);
+ byte[] sectionNonce = new byte[SECTION_NONCE_LENGTH];
+ secureRandom.nextBytes(sectionNonce);
+
+ byte[] result = MessageStreamHmacEncoder
+ .encodeMessagePacket(accountKey, sectionNonce, data);
+
+ assertThat(result).hasLength(messageLength + SECTION_NONCE_LENGTH + EXTRACT_HMAC_SIZE);
+ // First bytes are raw message bytes.
+ assertThat(Arrays.copyOf(result, messageLength)).isEqualTo(data);
+ // Following by message nonce.
+ byte[] messageNonce =
+ Arrays.copyOfRange(result, messageLength, messageLength + SECTION_NONCE_LENGTH);
+ byte[] extractedHmac =
+ Arrays.copyOf(
+ HmacSha256.buildWith64BytesKey(accountKey,
+ concat(sectionNonce, messageNonce, data)),
+ EXTRACT_HMAC_SIZE);
+ // Finally hash mac.
+ assertThat(Arrays.copyOfRange(result, messageLength + SECTION_NONCE_LENGTH, result.length))
+ .isEqualTo(extractedHmac);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void verifyHmac() throws GeneralSecurityException {
+ int messageLength = 2;
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] accountKey = new byte[ACCOUNT_KEY_LENGTH];
+ secureRandom.nextBytes(accountKey);
+ byte[] data = new byte[messageLength];
+ secureRandom.nextBytes(data);
+ byte[] sectionNonce = new byte[SECTION_NONCE_LENGTH];
+ secureRandom.nextBytes(sectionNonce);
+ byte[] result = MessageStreamHmacEncoder
+ .encodeMessagePacket(accountKey, sectionNonce, data);
+
+ assertThat(MessageStreamHmacEncoder.verifyHmac(accountKey, sectionNonce, result)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void verifyHmac_failedByAccountKey() throws GeneralSecurityException {
+ int messageLength = 2;
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] accountKey = new byte[ACCOUNT_KEY_LENGTH];
+ secureRandom.nextBytes(accountKey);
+ byte[] data = new byte[messageLength];
+ secureRandom.nextBytes(data);
+ byte[] sectionNonce = new byte[SECTION_NONCE_LENGTH];
+ secureRandom.nextBytes(sectionNonce);
+ byte[] result = MessageStreamHmacEncoder
+ .encodeMessagePacket(accountKey, sectionNonce, data);
+ secureRandom.nextBytes(accountKey);
+
+ assertThat(MessageStreamHmacEncoder.verifyHmac(accountKey, sectionNonce, result)).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void verifyHmac_failedBySectionNonce() throws GeneralSecurityException {
+ int messageLength = 2;
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] accountKey = new byte[ACCOUNT_KEY_LENGTH];
+ secureRandom.nextBytes(accountKey);
+ byte[] data = new byte[messageLength];
+ secureRandom.nextBytes(data);
+ byte[] sectionNonce = new byte[SECTION_NONCE_LENGTH];
+ secureRandom.nextBytes(sectionNonce);
+ byte[] result = MessageStreamHmacEncoder
+ .encodeMessagePacket(accountKey, sectionNonce, data);
+ secureRandom.nextBytes(sectionNonce);
+
+ assertThat(MessageStreamHmacEncoder.verifyHmac(accountKey, sectionNonce, result)).isFalse();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/NamingEncoderTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/NamingEncoderTest.java
new file mode 100644
index 0000000..d66d209
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/NamingEncoderTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.KEY_LENGTH;
+import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.NONCE_SIZE;
+import static com.android.server.nearby.common.bluetooth.fastpair.NamingEncoder.EXTRACT_HMAC_SIZE;
+import static com.android.server.nearby.common.bluetooth.fastpair.NamingEncoder.MAX_LENGTH_OF_NAME;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * Unit tests for {@link NamingEncoder}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NamingEncoderTest {
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void decodeEncodedNamingPacket_mustGetSameName() throws GeneralSecurityException {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ String name = "Someone's Google Headphone";
+
+ byte[] encodedNamingPacket = NamingEncoder.encodeNamingPacket(secret, name);
+
+ assertThat(NamingEncoder.decodeNamingPacket(secret, encodedNamingPacket)).isEqualTo(name);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputIncorrectKeySizeToEncode_mustThrowException() {
+ byte[] secret = new byte[KEY_LENGTH - 1];
+ String data = "Someone's Google Headphone";
+
+ GeneralSecurityException exception =
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> NamingEncoder.encodeNamingPacket(secret, data));
+
+ assertThat(exception).hasMessageThat()
+ .contains("Incorrect secret for encoding name packet");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputIncorrectKeySizeToDecode_mustThrowException() {
+ byte[] secret = new byte[KEY_LENGTH - 1];
+ byte[] data = new byte[50];
+
+ GeneralSecurityException exception =
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> NamingEncoder.decodeNamingPacket(secret, data));
+
+ assertThat(exception).hasMessageThat()
+ .contains("Incorrect secret for decoding name packet");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputTooSmallPacketSize_mustThrowException() {
+ byte[] secret = new byte[KEY_LENGTH];
+ byte[] data = new byte[EXTRACT_HMAC_SIZE - 1];
+
+ GeneralSecurityException exception =
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> NamingEncoder.decodeNamingPacket(secret, data));
+
+ assertThat(exception).hasMessageThat().contains("Naming packet size is incorrect");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputTooLargePacketSize_mustThrowException() throws GeneralSecurityException {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ byte[] namingPacket = new byte[MAX_LENGTH_OF_NAME + EXTRACT_HMAC_SIZE + NONCE_SIZE + 1];
+
+ GeneralSecurityException exception =
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> NamingEncoder.decodeNamingPacket(secret, namingPacket));
+
+ assertThat(exception).hasMessageThat().contains("Naming packet size is incorrect");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputIncorrectHmacToDecode_mustThrowException() throws GeneralSecurityException {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ String name = "Someone's Google Headphone";
+
+ byte[] encodedNamingPacket = NamingEncoder.encodeNamingPacket(secret, name);
+ encodedNamingPacket[0] = (byte) ~encodedNamingPacket[0];
+
+ GeneralSecurityException exception =
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> NamingEncoder.decodeNamingPacket(secret, encodedNamingPacket));
+
+ assertThat(exception)
+ .hasMessageThat()
+ .contains("Verify HMAC failed, could be incorrect key or naming packet.");
+ }
+
+ // Adds this test example on spec. Also we can easily change the parameters(e.g. secret, naming
+ // packet) to clarify test results with partners.
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void decodeTestNamingPacket_mustGetSameName() throws GeneralSecurityException {
+ byte[] secret = base16().decode("0123456789ABCDEF0123456789ABCDEF");
+ byte[] namingPacket = base16().decode(
+ "55EC5E6055AF6E920001020304050607EE4A2483738052E44E9B2A145E5DDFAA44B9E5536AF438"
+ + "E1E5C6");
+
+ assertThat(NamingEncoder.decodeNamingPacket(secret, namingPacket))
+ .isEqualTo("Someone's Google Headphone");
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/PreferencesTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/PreferencesTest.java
new file mode 100644
index 0000000..b40a5a5
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/PreferencesTest.java
@@ -0,0 +1,1277 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link Preferences}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PreferencesTest {
+
+ private static final int FIRST_INT = 1505;
+ private static final int SECOND_INT = 1506;
+ private static final boolean FIRST_BOOL = true;
+ private static final boolean SECOND_BOOL = false;
+ private static final short FIRST_SHORT = 32;
+ private static final short SECOND_SHORT = 73;
+ private static final long FIRST_LONG = 9838L;
+ private static final long SECOND_LONG = 93935L;
+ private static final String FIRST_STRING = "FIRST_STRING";
+ private static final String SECOND_STRING = "SECOND_STRING";
+ private static final byte[] FIRST_BYTES = new byte[] {7, 9};
+ private static final byte[] SECOND_BYTES = new byte[] {2};
+ private static final ImmutableSet<Integer> FIRST_INT_SETS = ImmutableSet.of(6, 8);
+ private static final ImmutableSet<Integer> SECOND_INT_SETS = ImmutableSet.of(6, 8);
+ private static final Preferences.ExtraLoggingInformation FIRST_EXTRA_LOGGING_INFO =
+ Preferences.ExtraLoggingInformation.builder().setModelId("000006").build();
+ private static final Preferences.ExtraLoggingInformation SECOND_EXTRA_LOGGING_INFO =
+ Preferences.ExtraLoggingInformation.builder().setModelId("000007").build();
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGattOperationTimeoutSeconds() {
+ Preferences prefs =
+ Preferences.builder().setGattOperationTimeoutSeconds(FIRST_INT).build();
+ assertThat(prefs.getGattOperationTimeoutSeconds()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getGattOperationTimeoutSeconds())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setGattOperationTimeoutSeconds(SECOND_INT).build();
+ assertThat(prefs2.getGattOperationTimeoutSeconds()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGattConnectionTimeoutSeconds() {
+ Preferences prefs =
+ Preferences.builder().setGattConnectionTimeoutSeconds(FIRST_INT).build();
+ assertThat(prefs.getGattConnectionTimeoutSeconds()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getGattConnectionTimeoutSeconds())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setGattConnectionTimeoutSeconds(SECOND_INT).build();
+ assertThat(prefs2.getGattConnectionTimeoutSeconds()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBluetoothToggleTimeoutSeconds() {
+ Preferences prefs =
+ Preferences.builder().setBluetoothToggleTimeoutSeconds(FIRST_INT).build();
+ assertThat(prefs.getBluetoothToggleTimeoutSeconds()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getBluetoothToggleTimeoutSeconds())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setBluetoothToggleTimeoutSeconds(SECOND_INT).build();
+ assertThat(prefs2.getBluetoothToggleTimeoutSeconds()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBluetoothToggleSleepSeconds() {
+ Preferences prefs =
+ Preferences.builder().setBluetoothToggleSleepSeconds(FIRST_INT).build();
+ assertThat(prefs.getBluetoothToggleSleepSeconds()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getBluetoothToggleSleepSeconds())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setBluetoothToggleSleepSeconds(SECOND_INT).build();
+ assertThat(prefs2.getBluetoothToggleSleepSeconds()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testClassicDiscoveryTimeoutSeconds() {
+ Preferences prefs =
+ Preferences.builder().setClassicDiscoveryTimeoutSeconds(FIRST_INT).build();
+ assertThat(prefs.getClassicDiscoveryTimeoutSeconds()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getClassicDiscoveryTimeoutSeconds())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setClassicDiscoveryTimeoutSeconds(SECOND_INT).build();
+ assertThat(prefs2.getClassicDiscoveryTimeoutSeconds()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testNumDiscoverAttempts() {
+ Preferences prefs =
+ Preferences.builder().setNumDiscoverAttempts(FIRST_INT).build();
+ assertThat(prefs.getNumDiscoverAttempts()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getNumDiscoverAttempts())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setNumDiscoverAttempts(SECOND_INT).build();
+ assertThat(prefs2.getNumDiscoverAttempts()).isEqualTo(SECOND_INT);
+ }
+
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testDiscoveryRetrySleepSeconds() {
+ Preferences prefs =
+ Preferences.builder().setDiscoveryRetrySleepSeconds(FIRST_INT).build();
+ assertThat(prefs.getDiscoveryRetrySleepSeconds()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getDiscoveryRetrySleepSeconds())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setDiscoveryRetrySleepSeconds(SECOND_INT).build();
+ assertThat(prefs2.getDiscoveryRetrySleepSeconds()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSdpTimeoutSeconds() {
+ Preferences prefs =
+ Preferences.builder().setSdpTimeoutSeconds(FIRST_INT).build();
+ assertThat(prefs.getSdpTimeoutSeconds()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getSdpTimeoutSeconds())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setSdpTimeoutSeconds(SECOND_INT).build();
+ assertThat(prefs2.getSdpTimeoutSeconds()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testNumSdpAttempts() {
+ Preferences prefs =
+ Preferences.builder().setNumSdpAttempts(FIRST_INT).build();
+ assertThat(prefs.getNumSdpAttempts()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getNumSdpAttempts())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setNumSdpAttempts(SECOND_INT).build();
+ assertThat(prefs2.getNumSdpAttempts()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testNumCreateBondAttempts() {
+ Preferences prefs =
+ Preferences.builder().setNumCreateBondAttempts(FIRST_INT).build();
+ assertThat(prefs.getNumCreateBondAttempts()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getNumCreateBondAttempts())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setNumCreateBondAttempts(SECOND_INT).build();
+ assertThat(prefs2.getNumCreateBondAttempts()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testNumConnectAttempts() {
+ Preferences prefs =
+ Preferences.builder().setNumConnectAttempts(FIRST_INT).build();
+ assertThat(prefs.getNumConnectAttempts()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getNumConnectAttempts())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setNumConnectAttempts(SECOND_INT).build();
+ assertThat(prefs2.getNumConnectAttempts()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testNumWriteAccountKeyAttempts() {
+ Preferences prefs =
+ Preferences.builder().setNumWriteAccountKeyAttempts(FIRST_INT).build();
+ assertThat(prefs.getNumWriteAccountKeyAttempts()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getNumWriteAccountKeyAttempts())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setNumWriteAccountKeyAttempts(SECOND_INT).build();
+ assertThat(prefs2.getNumWriteAccountKeyAttempts()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBluetoothStatePollingMillis() {
+ Preferences prefs =
+ Preferences.builder().setBluetoothStatePollingMillis(FIRST_INT).build();
+ assertThat(prefs.getBluetoothStatePollingMillis()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getBluetoothStatePollingMillis())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setBluetoothStatePollingMillis(SECOND_INT).build();
+ assertThat(prefs2.getBluetoothStatePollingMillis()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testNumAttempts() {
+ Preferences prefs =
+ Preferences.builder().setNumAttempts(FIRST_INT).build();
+ assertThat(prefs.getNumAttempts()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getNumAttempts())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setNumAttempts(SECOND_INT).build();
+ assertThat(prefs2.getNumAttempts()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testRemoveBondTimeoutSeconds() {
+ Preferences prefs =
+ Preferences.builder().setRemoveBondTimeoutSeconds(FIRST_INT).build();
+ assertThat(prefs.getRemoveBondTimeoutSeconds()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getRemoveBondTimeoutSeconds())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setRemoveBondTimeoutSeconds(SECOND_INT).build();
+ assertThat(prefs2.getRemoveBondTimeoutSeconds()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testRemoveBondSleepMillis() {
+ Preferences prefs =
+ Preferences.builder().setRemoveBondSleepMillis(FIRST_INT).build();
+ assertThat(prefs.getRemoveBondSleepMillis()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getRemoveBondSleepMillis())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setRemoveBondSleepMillis(SECOND_INT).build();
+ assertThat(prefs2.getRemoveBondSleepMillis()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreateBondTimeoutSeconds() {
+ Preferences prefs =
+ Preferences.builder().setCreateBondTimeoutSeconds(FIRST_INT).build();
+ assertThat(prefs.getCreateBondTimeoutSeconds()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getCreateBondTimeoutSeconds())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setCreateBondTimeoutSeconds(SECOND_INT).build();
+ assertThat(prefs2.getCreateBondTimeoutSeconds()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testHidCreateBondTimeoutSeconds() {
+ Preferences prefs =
+ Preferences.builder().setHidCreateBondTimeoutSeconds(FIRST_INT).build();
+ assertThat(prefs.getHidCreateBondTimeoutSeconds()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getHidCreateBondTimeoutSeconds())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setHidCreateBondTimeoutSeconds(SECOND_INT).build();
+ assertThat(prefs2.getHidCreateBondTimeoutSeconds()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testProxyTimeoutSeconds() {
+ Preferences prefs =
+ Preferences.builder().setProxyTimeoutSeconds(FIRST_INT).build();
+ assertThat(prefs.getProxyTimeoutSeconds()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getProxyTimeoutSeconds())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setProxyTimeoutSeconds(SECOND_INT).build();
+ assertThat(prefs2.getProxyTimeoutSeconds()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWriteAccountKeySleepMillis() {
+ Preferences prefs =
+ Preferences.builder().setWriteAccountKeySleepMillis(FIRST_INT).build();
+ assertThat(prefs.getWriteAccountKeySleepMillis()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getWriteAccountKeySleepMillis())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setWriteAccountKeySleepMillis(SECOND_INT).build();
+ assertThat(prefs2.getWriteAccountKeySleepMillis()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testPairFailureCounts() {
+ Preferences prefs =
+ Preferences.builder().setPairFailureCounts(FIRST_INT).build();
+ assertThat(prefs.getPairFailureCounts()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getPairFailureCounts())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setPairFailureCounts(SECOND_INT).build();
+ assertThat(prefs2.getPairFailureCounts()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreateBondTransportType() {
+ Preferences prefs =
+ Preferences.builder().setCreateBondTransportType(FIRST_INT).build();
+ assertThat(prefs.getCreateBondTransportType()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getCreateBondTransportType())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setCreateBondTransportType(SECOND_INT).build();
+ assertThat(prefs2.getCreateBondTransportType()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGattConnectRetryTimeoutMillis() {
+ Preferences prefs =
+ Preferences.builder().setGattConnectRetryTimeoutMillis(FIRST_INT).build();
+ assertThat(prefs.getGattConnectRetryTimeoutMillis()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getGattConnectRetryTimeoutMillis())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setGattConnectRetryTimeoutMillis(SECOND_INT).build();
+ assertThat(prefs2.getGattConnectRetryTimeoutMillis()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testNumSdpAttemptsAfterBonded() {
+ Preferences prefs =
+ Preferences.builder().setNumSdpAttemptsAfterBonded(FIRST_INT).build();
+ assertThat(prefs.getNumSdpAttemptsAfterBonded()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getNumSdpAttemptsAfterBonded())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setNumSdpAttemptsAfterBonded(SECOND_INT).build();
+ assertThat(prefs2.getNumSdpAttemptsAfterBonded()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSameModelIdPairedDeviceCount() {
+ Preferences prefs =
+ Preferences.builder().setSameModelIdPairedDeviceCount(FIRST_INT).build();
+ assertThat(prefs.getSameModelIdPairedDeviceCount()).isEqualTo(FIRST_INT);
+ assertThat(prefs.toBuilder().build().getSameModelIdPairedDeviceCount())
+ .isEqualTo(FIRST_INT);
+
+ Preferences prefs2 =
+ Preferences.builder().setSameModelIdPairedDeviceCount(SECOND_INT).build();
+ assertThat(prefs2.getSameModelIdPairedDeviceCount()).isEqualTo(SECOND_INT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testIgnoreDiscoveryError() {
+ Preferences prefs =
+ Preferences.builder().setIgnoreDiscoveryError(FIRST_BOOL).build();
+ assertThat(prefs.getIgnoreDiscoveryError()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getIgnoreDiscoveryError())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setIgnoreDiscoveryError(SECOND_BOOL).build();
+ assertThat(prefs2.getIgnoreDiscoveryError()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testToggleBluetoothOnFailure() {
+ Preferences prefs =
+ Preferences.builder().setToggleBluetoothOnFailure(FIRST_BOOL).build();
+ assertThat(prefs.getToggleBluetoothOnFailure()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getToggleBluetoothOnFailure())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setToggleBluetoothOnFailure(SECOND_BOOL).build();
+ assertThat(prefs2.getToggleBluetoothOnFailure()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBluetoothStateUsesPolling() {
+ Preferences prefs =
+ Preferences.builder().setBluetoothStateUsesPolling(FIRST_BOOL).build();
+ assertThat(prefs.getBluetoothStateUsesPolling()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getBluetoothStateUsesPolling())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setBluetoothStateUsesPolling(SECOND_BOOL).build();
+ assertThat(prefs2.getBluetoothStateUsesPolling()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEnableBrEdrHandover() {
+ Preferences prefs =
+ Preferences.builder().setEnableBrEdrHandover(FIRST_BOOL).build();
+ assertThat(prefs.getEnableBrEdrHandover()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getEnableBrEdrHandover())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setEnableBrEdrHandover(SECOND_BOOL).build();
+ assertThat(prefs2.getEnableBrEdrHandover()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWaitForUuidsAfterBonding() {
+ Preferences prefs =
+ Preferences.builder().setWaitForUuidsAfterBonding(FIRST_BOOL).build();
+ assertThat(prefs.getWaitForUuidsAfterBonding()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getWaitForUuidsAfterBonding())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setWaitForUuidsAfterBonding(SECOND_BOOL).build();
+ assertThat(prefs2.getWaitForUuidsAfterBonding()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testReceiveUuidsAndBondedEventBeforeClose() {
+ Preferences prefs =
+ Preferences.builder().setReceiveUuidsAndBondedEventBeforeClose(FIRST_BOOL).build();
+ assertThat(prefs.getReceiveUuidsAndBondedEventBeforeClose()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getReceiveUuidsAndBondedEventBeforeClose())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setReceiveUuidsAndBondedEventBeforeClose(SECOND_BOOL).build();
+ assertThat(prefs2.getReceiveUuidsAndBondedEventBeforeClose()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testRejectPhonebookAccess() {
+ Preferences prefs =
+ Preferences.builder().setRejectPhonebookAccess(FIRST_BOOL).build();
+ assertThat(prefs.getRejectPhonebookAccess()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getRejectPhonebookAccess())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setRejectPhonebookAccess(SECOND_BOOL).build();
+ assertThat(prefs2.getRejectPhonebookAccess()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testRejectMessageAccess() {
+ Preferences prefs =
+ Preferences.builder().setRejectMessageAccess(FIRST_BOOL).build();
+ assertThat(prefs.getRejectMessageAccess()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getRejectMessageAccess())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setRejectMessageAccess(SECOND_BOOL).build();
+ assertThat(prefs2.getRejectMessageAccess()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testRejectSimAccess() {
+ Preferences prefs =
+ Preferences.builder().setRejectSimAccess(FIRST_BOOL).build();
+ assertThat(prefs.getRejectSimAccess()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getRejectSimAccess())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setRejectSimAccess(SECOND_BOOL).build();
+ assertThat(prefs2.getRejectSimAccess()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSkipDisconnectingGattBeforeWritingAccountKey() {
+ Preferences prefs =
+ Preferences.builder().setSkipDisconnectingGattBeforeWritingAccountKey(FIRST_BOOL)
+ .build();
+ assertThat(prefs.getSkipDisconnectingGattBeforeWritingAccountKey()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getSkipDisconnectingGattBeforeWritingAccountKey())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setSkipDisconnectingGattBeforeWritingAccountKey(SECOND_BOOL)
+ .build();
+ assertThat(prefs2.getSkipDisconnectingGattBeforeWritingAccountKey()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testMoreEventLogForQuality() {
+ Preferences prefs =
+ Preferences.builder().setMoreEventLogForQuality(FIRST_BOOL).build();
+ assertThat(prefs.getMoreEventLogForQuality()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getMoreEventLogForQuality())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setMoreEventLogForQuality(SECOND_BOOL).build();
+ assertThat(prefs2.getMoreEventLogForQuality()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testRetryGattConnectionAndSecretHandshake() {
+ Preferences prefs =
+ Preferences.builder().setRetryGattConnectionAndSecretHandshake(FIRST_BOOL).build();
+ assertThat(prefs.getRetryGattConnectionAndSecretHandshake()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getRetryGattConnectionAndSecretHandshake())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setRetryGattConnectionAndSecretHandshake(SECOND_BOOL).build();
+ assertThat(prefs2.getRetryGattConnectionAndSecretHandshake()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testRetrySecretHandshakeTimeout() {
+ Preferences prefs =
+ Preferences.builder().setRetrySecretHandshakeTimeout(FIRST_BOOL).build();
+ assertThat(prefs.getRetrySecretHandshakeTimeout()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getRetrySecretHandshakeTimeout())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setRetrySecretHandshakeTimeout(SECOND_BOOL).build();
+ assertThat(prefs2.getRetrySecretHandshakeTimeout()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testLogUserManualRetry() {
+ Preferences prefs =
+ Preferences.builder().setLogUserManualRetry(FIRST_BOOL).build();
+ assertThat(prefs.getLogUserManualRetry()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getLogUserManualRetry())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setLogUserManualRetry(SECOND_BOOL).build();
+ assertThat(prefs2.getLogUserManualRetry()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testIsDeviceFinishCheckAddressFromCache() {
+ Preferences prefs =
+ Preferences.builder().setIsDeviceFinishCheckAddressFromCache(FIRST_BOOL).build();
+ assertThat(prefs.getIsDeviceFinishCheckAddressFromCache()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getIsDeviceFinishCheckAddressFromCache())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setIsDeviceFinishCheckAddressFromCache(SECOND_BOOL).build();
+ assertThat(prefs2.getIsDeviceFinishCheckAddressFromCache()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testLogPairWithCachedModelId() {
+ Preferences prefs =
+ Preferences.builder().setLogPairWithCachedModelId(FIRST_BOOL).build();
+ assertThat(prefs.getLogPairWithCachedModelId()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getLogPairWithCachedModelId())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setLogPairWithCachedModelId(SECOND_BOOL).build();
+ assertThat(prefs2.getLogPairWithCachedModelId()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testDirectConnectProfileIfModelIdInCache() {
+ Preferences prefs =
+ Preferences.builder().setDirectConnectProfileIfModelIdInCache(FIRST_BOOL).build();
+ assertThat(prefs.getDirectConnectProfileIfModelIdInCache()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getDirectConnectProfileIfModelIdInCache())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setDirectConnectProfileIfModelIdInCache(SECOND_BOOL).build();
+ assertThat(prefs2.getDirectConnectProfileIfModelIdInCache()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testAcceptPasskey() {
+ Preferences prefs =
+ Preferences.builder().setAcceptPasskey(FIRST_BOOL).build();
+ assertThat(prefs.getAcceptPasskey()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getAcceptPasskey())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setAcceptPasskey(SECOND_BOOL).build();
+ assertThat(prefs2.getAcceptPasskey()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testProviderInitiatesBondingIfSupported() {
+ Preferences prefs =
+ Preferences.builder().setProviderInitiatesBondingIfSupported(FIRST_BOOL).build();
+ assertThat(prefs.getProviderInitiatesBondingIfSupported()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getProviderInitiatesBondingIfSupported())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setProviderInitiatesBondingIfSupported(SECOND_BOOL).build();
+ assertThat(prefs2.getProviderInitiatesBondingIfSupported()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testAttemptDirectConnectionWhenPreviouslyBonded() {
+ Preferences prefs =
+ Preferences.builder()
+ .setAttemptDirectConnectionWhenPreviouslyBonded(FIRST_BOOL).build();
+ assertThat(prefs.getAttemptDirectConnectionWhenPreviouslyBonded()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getAttemptDirectConnectionWhenPreviouslyBonded())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder()
+ .setAttemptDirectConnectionWhenPreviouslyBonded(SECOND_BOOL).build();
+ assertThat(prefs2.getAttemptDirectConnectionWhenPreviouslyBonded()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testAutomaticallyReconnectGattWhenNeeded() {
+ Preferences prefs =
+ Preferences.builder().setAutomaticallyReconnectGattWhenNeeded(FIRST_BOOL).build();
+ assertThat(prefs.getAutomaticallyReconnectGattWhenNeeded()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getAutomaticallyReconnectGattWhenNeeded())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setAutomaticallyReconnectGattWhenNeeded(SECOND_BOOL).build();
+ assertThat(prefs2.getAutomaticallyReconnectGattWhenNeeded()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSkipConnectingProfiles() {
+ Preferences prefs =
+ Preferences.builder().setSkipConnectingProfiles(FIRST_BOOL).build();
+ assertThat(prefs.getSkipConnectingProfiles()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getSkipConnectingProfiles())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setSkipConnectingProfiles(SECOND_BOOL).build();
+ assertThat(prefs2.getSkipConnectingProfiles()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testIgnoreUuidTimeoutAfterBonded() {
+ Preferences prefs =
+ Preferences.builder().setIgnoreUuidTimeoutAfterBonded(FIRST_BOOL).build();
+ assertThat(prefs.getIgnoreUuidTimeoutAfterBonded()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getIgnoreUuidTimeoutAfterBonded())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setIgnoreUuidTimeoutAfterBonded(SECOND_BOOL).build();
+ assertThat(prefs2.getIgnoreUuidTimeoutAfterBonded()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSpecifyCreateBondTransportType() {
+ Preferences prefs =
+ Preferences.builder().setSpecifyCreateBondTransportType(FIRST_BOOL).build();
+ assertThat(prefs.getSpecifyCreateBondTransportType()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getSpecifyCreateBondTransportType())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setSpecifyCreateBondTransportType(SECOND_BOOL).build();
+ assertThat(prefs2.getSpecifyCreateBondTransportType()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testIncreaseIntentFilterPriority() {
+ Preferences prefs =
+ Preferences.builder().setIncreaseIntentFilterPriority(FIRST_BOOL).build();
+ assertThat(prefs.getIncreaseIntentFilterPriority()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getIncreaseIntentFilterPriority())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setIncreaseIntentFilterPriority(SECOND_BOOL).build();
+ assertThat(prefs2.getIncreaseIntentFilterPriority()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEvaluatePerformance() {
+ Preferences prefs =
+ Preferences.builder().setEvaluatePerformance(FIRST_BOOL).build();
+ assertThat(prefs.getEvaluatePerformance()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getEvaluatePerformance())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setEvaluatePerformance(SECOND_BOOL).build();
+ assertThat(prefs2.getEvaluatePerformance()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEnableNamingCharacteristic() {
+ Preferences prefs =
+ Preferences.builder().setEnableNamingCharacteristic(FIRST_BOOL).build();
+ assertThat(prefs.getEnableNamingCharacteristic()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getEnableNamingCharacteristic())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setEnableNamingCharacteristic(SECOND_BOOL).build();
+ assertThat(prefs2.getEnableNamingCharacteristic()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEnableFirmwareVersionCharacteristic() {
+ Preferences prefs =
+ Preferences.builder().setEnableFirmwareVersionCharacteristic(FIRST_BOOL).build();
+ assertThat(prefs.getEnableFirmwareVersionCharacteristic()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getEnableFirmwareVersionCharacteristic())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setEnableFirmwareVersionCharacteristic(SECOND_BOOL).build();
+ assertThat(prefs2.getEnableFirmwareVersionCharacteristic()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testKeepSameAccountKeyWrite() {
+ Preferences prefs =
+ Preferences.builder().setKeepSameAccountKeyWrite(FIRST_BOOL).build();
+ assertThat(prefs.getKeepSameAccountKeyWrite()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getKeepSameAccountKeyWrite())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setKeepSameAccountKeyWrite(SECOND_BOOL).build();
+ assertThat(prefs2.getKeepSameAccountKeyWrite()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testIsRetroactivePairing() {
+ Preferences prefs =
+ Preferences.builder().setIsRetroactivePairing(FIRST_BOOL).build();
+ assertThat(prefs.getIsRetroactivePairing()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getIsRetroactivePairing())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setIsRetroactivePairing(SECOND_BOOL).build();
+ assertThat(prefs2.getIsRetroactivePairing()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSupportHidDevice() {
+ Preferences prefs =
+ Preferences.builder().setSupportHidDevice(FIRST_BOOL).build();
+ assertThat(prefs.getSupportHidDevice()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getSupportHidDevice())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setSupportHidDevice(SECOND_BOOL).build();
+ assertThat(prefs2.getSupportHidDevice()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEnablePairingWhileDirectlyConnecting() {
+ Preferences prefs =
+ Preferences.builder().setEnablePairingWhileDirectlyConnecting(FIRST_BOOL).build();
+ assertThat(prefs.getEnablePairingWhileDirectlyConnecting()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getEnablePairingWhileDirectlyConnecting())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setEnablePairingWhileDirectlyConnecting(SECOND_BOOL).build();
+ assertThat(prefs2.getEnablePairingWhileDirectlyConnecting()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testAcceptConsentForFastPairOne() {
+ Preferences prefs =
+ Preferences.builder().setAcceptConsentForFastPairOne(FIRST_BOOL).build();
+ assertThat(prefs.getAcceptConsentForFastPairOne()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getAcceptConsentForFastPairOne())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setAcceptConsentForFastPairOne(SECOND_BOOL).build();
+ assertThat(prefs2.getAcceptConsentForFastPairOne()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEnable128BitCustomGattCharacteristicsId() {
+ Preferences prefs =
+ Preferences.builder().setEnable128BitCustomGattCharacteristicsId(FIRST_BOOL)
+ .build();
+ assertThat(prefs.getEnable128BitCustomGattCharacteristicsId()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getEnable128BitCustomGattCharacteristicsId())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setEnable128BitCustomGattCharacteristicsId(SECOND_BOOL)
+ .build();
+ assertThat(prefs2.getEnable128BitCustomGattCharacteristicsId()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEnableSendExceptionStepToValidator() {
+ Preferences prefs =
+ Preferences.builder().setEnableSendExceptionStepToValidator(FIRST_BOOL).build();
+ assertThat(prefs.getEnableSendExceptionStepToValidator()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getEnableSendExceptionStepToValidator())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setEnableSendExceptionStepToValidator(SECOND_BOOL).build();
+ assertThat(prefs2.getEnableSendExceptionStepToValidator()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEnableAdditionalDataTypeWhenActionOverBle() {
+ Preferences prefs =
+ Preferences.builder().setEnableAdditionalDataTypeWhenActionOverBle(FIRST_BOOL)
+ .build();
+ assertThat(prefs.getEnableAdditionalDataTypeWhenActionOverBle()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getEnableAdditionalDataTypeWhenActionOverBle())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setEnableAdditionalDataTypeWhenActionOverBle(SECOND_BOOL)
+ .build();
+ assertThat(prefs2.getEnableAdditionalDataTypeWhenActionOverBle()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCheckBondStateWhenSkipConnectingProfiles() {
+ Preferences prefs =
+ Preferences.builder().setCheckBondStateWhenSkipConnectingProfiles(FIRST_BOOL)
+ .build();
+ assertThat(prefs.getCheckBondStateWhenSkipConnectingProfiles()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getCheckBondStateWhenSkipConnectingProfiles())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setCheckBondStateWhenSkipConnectingProfiles(SECOND_BOOL)
+ .build();
+ assertThat(prefs2.getCheckBondStateWhenSkipConnectingProfiles()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testHandlePasskeyConfirmationByUi() {
+ Preferences prefs =
+ Preferences.builder().setHandlePasskeyConfirmationByUi(FIRST_BOOL).build();
+ assertThat(prefs.getHandlePasskeyConfirmationByUi()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getHandlePasskeyConfirmationByUi())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setHandlePasskeyConfirmationByUi(SECOND_BOOL).build();
+ assertThat(prefs2.getHandlePasskeyConfirmationByUi()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEnablePairFlowShowUiWithoutProfileConnection() {
+ Preferences prefs =
+ Preferences.builder().setEnablePairFlowShowUiWithoutProfileConnection(FIRST_BOOL)
+ .build();
+ assertThat(prefs.getEnablePairFlowShowUiWithoutProfileConnection()).isEqualTo(FIRST_BOOL);
+ assertThat(prefs.toBuilder().build().getEnablePairFlowShowUiWithoutProfileConnection())
+ .isEqualTo(FIRST_BOOL);
+
+ Preferences prefs2 =
+ Preferences.builder().setEnablePairFlowShowUiWithoutProfileConnection(SECOND_BOOL)
+ .build();
+ assertThat(prefs2.getEnablePairFlowShowUiWithoutProfileConnection()).isEqualTo(SECOND_BOOL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBrHandoverDataCharacteristicId() {
+ Preferences prefs =
+ Preferences.builder().setBrHandoverDataCharacteristicId(FIRST_SHORT).build();
+ assertThat(prefs.getBrHandoverDataCharacteristicId()).isEqualTo(FIRST_SHORT);
+ assertThat(prefs.toBuilder().build().getBrHandoverDataCharacteristicId())
+ .isEqualTo(FIRST_SHORT);
+
+ Preferences prefs2 =
+ Preferences.builder().setBrHandoverDataCharacteristicId(SECOND_SHORT).build();
+ assertThat(prefs2.getBrHandoverDataCharacteristicId()).isEqualTo(SECOND_SHORT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBluetoothSigDataCharacteristicId() {
+ Preferences prefs =
+ Preferences.builder().setBluetoothSigDataCharacteristicId(FIRST_SHORT).build();
+ assertThat(prefs.getBluetoothSigDataCharacteristicId()).isEqualTo(FIRST_SHORT);
+ assertThat(prefs.toBuilder().build().getBluetoothSigDataCharacteristicId())
+ .isEqualTo(FIRST_SHORT);
+
+ Preferences prefs2 =
+ Preferences.builder().setBluetoothSigDataCharacteristicId(SECOND_SHORT).build();
+ assertThat(prefs2.getBluetoothSigDataCharacteristicId()).isEqualTo(SECOND_SHORT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testFirmwareVersionCharacteristicId() {
+ Preferences prefs =
+ Preferences.builder().setFirmwareVersionCharacteristicId(FIRST_SHORT).build();
+ assertThat(prefs.getFirmwareVersionCharacteristicId()).isEqualTo(FIRST_SHORT);
+ assertThat(prefs.toBuilder().build().getFirmwareVersionCharacteristicId())
+ .isEqualTo(FIRST_SHORT);
+
+ Preferences prefs2 =
+ Preferences.builder().setFirmwareVersionCharacteristicId(SECOND_SHORT).build();
+ assertThat(prefs2.getFirmwareVersionCharacteristicId()).isEqualTo(SECOND_SHORT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBrTransportBlockDataDescriptorId() {
+ Preferences prefs =
+ Preferences.builder().setBrTransportBlockDataDescriptorId(FIRST_SHORT).build();
+ assertThat(prefs.getBrTransportBlockDataDescriptorId()).isEqualTo(FIRST_SHORT);
+ assertThat(prefs.toBuilder().build().getBrTransportBlockDataDescriptorId())
+ .isEqualTo(FIRST_SHORT);
+
+ Preferences prefs2 =
+ Preferences.builder().setBrTransportBlockDataDescriptorId(SECOND_SHORT).build();
+ assertThat(prefs2.getBrTransportBlockDataDescriptorId()).isEqualTo(SECOND_SHORT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGattConnectShortTimeoutMs() {
+ Preferences prefs =
+ Preferences.builder().setGattConnectShortTimeoutMs(FIRST_LONG).build();
+ assertThat(prefs.getGattConnectShortTimeoutMs()).isEqualTo(FIRST_LONG);
+ assertThat(prefs.toBuilder().build().getGattConnectShortTimeoutMs())
+ .isEqualTo(FIRST_LONG);
+
+ Preferences prefs2 =
+ Preferences.builder().setGattConnectShortTimeoutMs(SECOND_LONG).build();
+ assertThat(prefs2.getGattConnectShortTimeoutMs()).isEqualTo(SECOND_LONG);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGattConnectLongTimeoutMs() {
+ Preferences prefs =
+ Preferences.builder().setGattConnectLongTimeoutMs(FIRST_LONG).build();
+ assertThat(prefs.getGattConnectLongTimeoutMs()).isEqualTo(FIRST_LONG);
+ assertThat(prefs.toBuilder().build().getGattConnectLongTimeoutMs())
+ .isEqualTo(FIRST_LONG);
+
+ Preferences prefs2 =
+ Preferences.builder().setGattConnectLongTimeoutMs(SECOND_LONG).build();
+ assertThat(prefs2.getGattConnectLongTimeoutMs()).isEqualTo(SECOND_LONG);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGattConnectShortTimeoutRetryMaxSpentTimeMs() {
+ Preferences prefs =
+ Preferences.builder().setGattConnectShortTimeoutRetryMaxSpentTimeMs(FIRST_LONG)
+ .build();
+ assertThat(prefs.getGattConnectShortTimeoutRetryMaxSpentTimeMs()).isEqualTo(FIRST_LONG);
+ assertThat(prefs.toBuilder().build().getGattConnectShortTimeoutRetryMaxSpentTimeMs())
+ .isEqualTo(FIRST_LONG);
+
+ Preferences prefs2 =
+ Preferences.builder().setGattConnectShortTimeoutRetryMaxSpentTimeMs(SECOND_LONG)
+ .build();
+ assertThat(prefs2.getGattConnectShortTimeoutRetryMaxSpentTimeMs()).isEqualTo(SECOND_LONG);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testAddressRotateRetryMaxSpentTimeMs() {
+ Preferences prefs =
+ Preferences.builder().setAddressRotateRetryMaxSpentTimeMs(FIRST_LONG).build();
+ assertThat(prefs.getAddressRotateRetryMaxSpentTimeMs()).isEqualTo(FIRST_LONG);
+ assertThat(prefs.toBuilder().build().getAddressRotateRetryMaxSpentTimeMs())
+ .isEqualTo(FIRST_LONG);
+
+ Preferences prefs2 =
+ Preferences.builder().setAddressRotateRetryMaxSpentTimeMs(SECOND_LONG).build();
+ assertThat(prefs2.getAddressRotateRetryMaxSpentTimeMs()).isEqualTo(SECOND_LONG);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testPairingRetryDelayMs() {
+ Preferences prefs =
+ Preferences.builder().setPairingRetryDelayMs(FIRST_LONG).build();
+ assertThat(prefs.getPairingRetryDelayMs()).isEqualTo(FIRST_LONG);
+ assertThat(prefs.toBuilder().build().getPairingRetryDelayMs())
+ .isEqualTo(FIRST_LONG);
+
+ Preferences prefs2 =
+ Preferences.builder().setPairingRetryDelayMs(SECOND_LONG).build();
+ assertThat(prefs2.getPairingRetryDelayMs()).isEqualTo(SECOND_LONG);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSecretHandshakeShortTimeoutMs() {
+ Preferences prefs =
+ Preferences.builder().setSecretHandshakeShortTimeoutMs(FIRST_LONG).build();
+ assertThat(prefs.getSecretHandshakeShortTimeoutMs()).isEqualTo(FIRST_LONG);
+ assertThat(prefs.toBuilder().build().getSecretHandshakeShortTimeoutMs())
+ .isEqualTo(FIRST_LONG);
+
+ Preferences prefs2 =
+ Preferences.builder().setSecretHandshakeShortTimeoutMs(SECOND_LONG).build();
+ assertThat(prefs2.getSecretHandshakeShortTimeoutMs()).isEqualTo(SECOND_LONG);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSecretHandshakeLongTimeoutMs() {
+ Preferences prefs =
+ Preferences.builder().setSecretHandshakeLongTimeoutMs(FIRST_LONG).build();
+ assertThat(prefs.getSecretHandshakeLongTimeoutMs()).isEqualTo(FIRST_LONG);
+ assertThat(prefs.toBuilder().build().getSecretHandshakeLongTimeoutMs())
+ .isEqualTo(FIRST_LONG);
+
+ Preferences prefs2 =
+ Preferences.builder().setSecretHandshakeLongTimeoutMs(SECOND_LONG).build();
+ assertThat(prefs2.getSecretHandshakeLongTimeoutMs()).isEqualTo(SECOND_LONG);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSecretHandshakeShortTimeoutRetryMaxSpentTimeMs() {
+ Preferences prefs =
+ Preferences.builder().setSecretHandshakeShortTimeoutRetryMaxSpentTimeMs(FIRST_LONG)
+ .build();
+ assertThat(prefs.getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs()).isEqualTo(FIRST_LONG);
+ assertThat(prefs.toBuilder().build().getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs())
+ .isEqualTo(FIRST_LONG);
+
+ Preferences prefs2 =
+ Preferences.builder().setSecretHandshakeShortTimeoutRetryMaxSpentTimeMs(SECOND_LONG)
+ .build();
+ assertThat(prefs2.getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs())
+ .isEqualTo(SECOND_LONG);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSecretHandshakeLongTimeoutRetryMaxSpentTimeMs() {
+ Preferences prefs =
+ Preferences.builder().setSecretHandshakeLongTimeoutRetryMaxSpentTimeMs(FIRST_LONG)
+ .build();
+ assertThat(prefs.getSecretHandshakeLongTimeoutRetryMaxSpentTimeMs()).isEqualTo(FIRST_LONG);
+ assertThat(prefs.toBuilder().build().getSecretHandshakeLongTimeoutRetryMaxSpentTimeMs())
+ .isEqualTo(FIRST_LONG);
+
+ Preferences prefs2 =
+ Preferences.builder().setSecretHandshakeLongTimeoutRetryMaxSpentTimeMs(SECOND_LONG)
+ .build();
+ assertThat(prefs2.getSecretHandshakeLongTimeoutRetryMaxSpentTimeMs())
+ .isEqualTo(SECOND_LONG);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSecretHandshakeRetryAttempts() {
+ Preferences prefs =
+ Preferences.builder().setSecretHandshakeRetryAttempts(FIRST_LONG).build();
+ assertThat(prefs.getSecretHandshakeRetryAttempts()).isEqualTo(FIRST_LONG);
+ assertThat(prefs.toBuilder().build().getSecretHandshakeRetryAttempts())
+ .isEqualTo(FIRST_LONG);
+
+ Preferences prefs2 =
+ Preferences.builder().setSecretHandshakeRetryAttempts(SECOND_LONG).build();
+ assertThat(prefs2.getSecretHandshakeRetryAttempts()).isEqualTo(SECOND_LONG);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSecretHandshakeRetryGattConnectionMaxSpentTimeMs() {
+ Preferences prefs =
+ Preferences.builder()
+ .setSecretHandshakeRetryGattConnectionMaxSpentTimeMs(FIRST_LONG).build();
+ assertThat(prefs.getSecretHandshakeRetryGattConnectionMaxSpentTimeMs())
+ .isEqualTo(FIRST_LONG);
+ assertThat(prefs.toBuilder().build().getSecretHandshakeRetryGattConnectionMaxSpentTimeMs())
+ .isEqualTo(FIRST_LONG);
+
+ Preferences prefs2 =
+ Preferences.builder().setSecretHandshakeRetryGattConnectionMaxSpentTimeMs(
+ SECOND_LONG).build();
+ assertThat(prefs2.getSecretHandshakeRetryGattConnectionMaxSpentTimeMs())
+ .isEqualTo(SECOND_LONG);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSignalLostRetryMaxSpentTimeMs() {
+ Preferences prefs =
+ Preferences.builder().setSignalLostRetryMaxSpentTimeMs(FIRST_LONG).build();
+ assertThat(prefs.getSignalLostRetryMaxSpentTimeMs()).isEqualTo(FIRST_LONG);
+ assertThat(prefs.toBuilder().build().getSignalLostRetryMaxSpentTimeMs())
+ .isEqualTo(FIRST_LONG);
+
+ Preferences prefs2 =
+ Preferences.builder().setSignalLostRetryMaxSpentTimeMs(SECOND_LONG).build();
+ assertThat(prefs2.getSignalLostRetryMaxSpentTimeMs()).isEqualTo(SECOND_LONG);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCachedDeviceAddress() {
+ Preferences prefs =
+ Preferences.builder().setCachedDeviceAddress(FIRST_STRING).build();
+ assertThat(prefs.getCachedDeviceAddress()).isEqualTo(FIRST_STRING);
+ assertThat(prefs.toBuilder().build().getCachedDeviceAddress())
+ .isEqualTo(FIRST_STRING);
+
+ Preferences prefs2 =
+ Preferences.builder().setCachedDeviceAddress(SECOND_STRING).build();
+ assertThat(prefs2.getCachedDeviceAddress()).isEqualTo(SECOND_STRING);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testPossibleCachedDeviceAddress() {
+ Preferences prefs =
+ Preferences.builder().setPossibleCachedDeviceAddress(FIRST_STRING).build();
+ assertThat(prefs.getPossibleCachedDeviceAddress()).isEqualTo(FIRST_STRING);
+ assertThat(prefs.toBuilder().build().getPossibleCachedDeviceAddress())
+ .isEqualTo(FIRST_STRING);
+
+ Preferences prefs2 =
+ Preferences.builder().setPossibleCachedDeviceAddress(SECOND_STRING).build();
+ assertThat(prefs2.getPossibleCachedDeviceAddress()).isEqualTo(SECOND_STRING);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSupportedProfileUuids() {
+ Preferences prefs =
+ Preferences.builder().setSupportedProfileUuids(FIRST_BYTES).build();
+ assertThat(prefs.getSupportedProfileUuids()).isEqualTo(FIRST_BYTES);
+ assertThat(prefs.toBuilder().build().getSupportedProfileUuids())
+ .isEqualTo(FIRST_BYTES);
+
+ Preferences prefs2 =
+ Preferences.builder().setSupportedProfileUuids(SECOND_BYTES).build();
+ assertThat(prefs2.getSupportedProfileUuids()).isEqualTo(SECOND_BYTES);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGattConnectionAndSecretHandshakeNoRetryGattError() {
+ Preferences prefs =
+ Preferences.builder().setGattConnectionAndSecretHandshakeNoRetryGattError(
+ FIRST_INT_SETS).build();
+ assertThat(prefs.getGattConnectionAndSecretHandshakeNoRetryGattError())
+ .isEqualTo(FIRST_INT_SETS);
+ assertThat(prefs.toBuilder().build().getGattConnectionAndSecretHandshakeNoRetryGattError())
+ .isEqualTo(FIRST_INT_SETS);
+
+ Preferences prefs2 =
+ Preferences.builder().setGattConnectionAndSecretHandshakeNoRetryGattError(
+ SECOND_INT_SETS).build();
+ assertThat(prefs2.getGattConnectionAndSecretHandshakeNoRetryGattError())
+ .isEqualTo(SECOND_INT_SETS);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testExtraLoggingInformation() {
+ Preferences prefs =
+ Preferences.builder().setExtraLoggingInformation(FIRST_EXTRA_LOGGING_INFO).build();
+ assertThat(prefs.getExtraLoggingInformation()).isEqualTo(FIRST_EXTRA_LOGGING_INFO);
+ assertThat(prefs.toBuilder().build().getExtraLoggingInformation())
+ .isEqualTo(FIRST_EXTRA_LOGGING_INFO);
+
+ Preferences prefs2 =
+ Preferences.builder().setExtraLoggingInformation(SECOND_EXTRA_LOGGING_INFO).build();
+ assertThat(prefs2.getExtraLoggingInformation()).isEqualTo(SECOND_EXTRA_LOGGING_INFO);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/TimingLoggerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/TimingLoggerTest.java
new file mode 100644
index 0000000..4672905
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/TimingLoggerTest.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.nearby.common.bluetooth.fastpair.TimingLogger.ScopedTiming;
+import com.android.server.nearby.common.bluetooth.fastpair.TimingLogger.Timing;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link TimingLogger}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TimingLoggerTest {
+
+ private final Preferences mPrefs = Preferences.builder().setEvaluatePerformance(true).build();
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void logPairedTiming() {
+ String label = "start";
+ TimingLogger timingLogger = new TimingLogger("paired", mPrefs);
+ timingLogger.start(label);
+ SystemClock.sleep(1000);
+ timingLogger.end();
+
+ assertThat(timingLogger.getTimings()).hasSize(2);
+
+ // Calculate execution time and only store result at "start" timing.
+ // Expected output:
+ // <pre>
+ // I/FastPair: paired [Exclusive time] / [Total time]
+ // I/FastPair: start 1000ms
+ // I/FastPair: paired end, 1000ms
+ // </pre>
+ timingLogger.dump();
+
+ assertPairedTiming(label, timingLogger.getTimings().get(0),
+ timingLogger.getTimings().get(1));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void logScopedTiming() {
+ String label = "scopedTiming";
+ TimingLogger timingLogger = new TimingLogger("scoped", mPrefs);
+ try (ScopedTiming scopedTiming = new ScopedTiming(timingLogger, label)) {
+ SystemClock.sleep(1000);
+ }
+
+ assertThat(timingLogger.getTimings()).hasSize(2);
+
+ // Calculate execution time and only store result at "start" timings.
+ // Expected output:
+ // <pre>
+ // I/FastPair: scoped [Exclusive time] / [Total time]
+ // I/FastPair: scopedTiming 1000ms
+ // I/FastPair: scoped end, 1000ms
+ // </pre>
+ timingLogger.dump();
+
+ assertPairedTiming(label, timingLogger.getTimings().get(0),
+ timingLogger.getTimings().get(1));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void logOrderedTiming() {
+ String label1 = "t1";
+ String label2 = "t2";
+ TimingLogger timingLogger = new TimingLogger("ordered", mPrefs);
+ try (ScopedTiming t1 = new ScopedTiming(timingLogger, label1)) {
+ SystemClock.sleep(1000);
+ }
+ try (ScopedTiming t2 = new ScopedTiming(timingLogger, label2)) {
+ SystemClock.sleep(1000);
+ }
+
+ assertThat(timingLogger.getTimings()).hasSize(4);
+
+ // Calculate execution time and only store result at "start" timings.
+ // Expected output:
+ // <pre>
+ // I/FastPair: ordered [Exclusive time] / [Total time]
+ // I/FastPair: t1 1000ms
+ // I/FastPair: t2 1000ms
+ // I/FastPair: ordered end, 2000ms
+ // </pre>
+ timingLogger.dump();
+
+ // We expect get timings in this order: t1 start, t1 end, t2 start, t2 end.
+ Timing start1 = timingLogger.getTimings().get(0);
+ Timing end1 = timingLogger.getTimings().get(1);
+ Timing start2 = timingLogger.getTimings().get(2);
+ Timing end2 = timingLogger.getTimings().get(3);
+
+ // Verify the paired timings.
+ assertPairedTiming(label1, start1, end1);
+ assertPairedTiming(label2, start2, end2);
+
+ // Verify the order and total time.
+ assertOrderedTiming(start1, start2);
+ assertThat(start1.getExclusiveTime() + start2.getExclusiveTime())
+ .isEqualTo(timingLogger.getTotalTime());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void logNestedTiming() {
+ String labelOuter = "outer";
+ String labelInner1 = "inner1";
+ String labelInner1Inner1 = "inner1inner1";
+ String labelInner2 = "inner2";
+ TimingLogger timingLogger = new TimingLogger("nested", mPrefs);
+ try (ScopedTiming outer = new ScopedTiming(timingLogger, labelOuter)) {
+ SystemClock.sleep(1000);
+ try (ScopedTiming inner1 = new ScopedTiming(timingLogger, labelInner1)) {
+ SystemClock.sleep(1000);
+ try (ScopedTiming inner1inner1 = new ScopedTiming(timingLogger,
+ labelInner1Inner1)) {
+ SystemClock.sleep(1000);
+ }
+ }
+ try (ScopedTiming inner2 = new ScopedTiming(timingLogger, labelInner2)) {
+ SystemClock.sleep(1000);
+ }
+ }
+
+ assertThat(timingLogger.getTimings()).hasSize(8);
+
+ // Calculate execution time and only store result at "start" timing.
+ // Expected output:
+ // <pre>
+ // I/FastPair: nested [Exclusive time] / [Total time]
+ // I/FastPair: outer 1000ms / 4000ms
+ // I/FastPair: inner1 1000ms / 2000ms
+ // I/FastPair: inner1inner1 1000ms
+ // I/FastPair: inner2 1000ms
+ // I/FastPair: nested end, 4000ms
+ // </pre>
+ timingLogger.dump();
+
+ // We expect get timings in this order: outer start, inner1 start, inner1inner1 start,
+ // inner1inner1 end, inner1 end, inner2 start, inner2 end, outer end.
+ Timing startOuter = timingLogger.getTimings().get(0);
+ Timing startInner1 = timingLogger.getTimings().get(1);
+ Timing startInner1Inner1 = timingLogger.getTimings().get(2);
+ Timing endInner1Inner1 = timingLogger.getTimings().get(3);
+ Timing endInner1 = timingLogger.getTimings().get(4);
+ Timing startInner2 = timingLogger.getTimings().get(5);
+ Timing endInner2 = timingLogger.getTimings().get(6);
+ Timing endOuter = timingLogger.getTimings().get(7);
+
+ // Verify the paired timings.
+ assertPairedTiming(labelOuter, startOuter, endOuter);
+ assertPairedTiming(labelInner1, startInner1, endInner1);
+ assertPairedTiming(labelInner1Inner1, startInner1Inner1, endInner1Inner1);
+ assertPairedTiming(labelInner2, startInner2, endInner2);
+
+ // Verify the order and total time.
+ assertOrderedTiming(startOuter, startInner1);
+ assertOrderedTiming(startInner1, startInner1Inner1);
+ assertOrderedTiming(startInner1Inner1, startInner2);
+ assertThat(
+ startOuter.getExclusiveTime() + startInner1.getTotalTime() + startInner2
+ .getTotalTime())
+ .isEqualTo(timingLogger.getTotalTime());
+
+ // Verify the nested execution time.
+ assertThat(startInner1Inner1.getTotalTime()).isAtMost(startInner1.getTotalTime());
+ assertThat(startInner1.getTotalTime() + startInner2.getTotalTime())
+ .isAtMost(startOuter.getTotalTime());
+ }
+
+ private void assertPairedTiming(String label, Timing start, Timing end) {
+ assertThat(start.isStartTiming()).isTrue();
+ assertThat(start.getName()).isEqualTo(label);
+ assertThat(end.isEndTiming()).isTrue();
+ assertThat(end.getTimestamp()).isAtLeast(start.getTimestamp());
+
+ assertThat(start.getExclusiveTime() > 0).isTrue();
+ assertThat(start.getTotalTime()).isAtLeast(start.getExclusiveTime());
+ assertThat(end.getExclusiveTime() == 0).isTrue();
+ assertThat(end.getTotalTime() == 0).isTrue();
+ }
+
+ private void assertOrderedTiming(Timing t1, Timing t2) {
+ assertThat(t1.isStartTiming()).isTrue();
+ assertThat(t2.isStartTiming()).isTrue();
+ assertThat(t2.getTimestamp()).isAtLeast(t1.getTimestamp());
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnectionTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnectionTest.java
new file mode 100644
index 0000000..80bde63
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattConnectionTest.java
@@ -0,0 +1,848 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.gatt;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothStatusCodes;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+
+import androidx.test.filters.SdkSuppress;
+
+import com.android.server.nearby.common.bluetooth.BluetoothConsts;
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.BluetoothGattException;
+import com.android.server.nearby.common.bluetooth.ReservedUuids;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection.ChangeObserver;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.OperationType;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattWrapper;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.SynchronousOperation;
+
+import junit.framework.TestCase;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+import java.util.Arrays;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Unit tests for {@link BluetoothGattConnection}.
+ */
+public class BluetoothGattConnectionTest extends TestCase {
+
+ private static final UUID SERVICE_UUID = UUID.randomUUID();
+ private static final UUID CHARACTERISTIC_UUID = UUID.randomUUID();
+ private static final UUID DESCRIPTOR_UUID = UUID.randomUUID();
+ private static final byte[] DATA = "data".getBytes();
+ private static final int RSSI = -63;
+ private static final int CONNECTION_PRIORITY = 128;
+ private static final int MTU_REQUEST = 512;
+ private static final BluetoothGattHelper.ConnectionOptions CONNECTION_OPTIONS =
+ BluetoothGattHelper.ConnectionOptions.builder().build();
+
+ @Mock
+ private BluetoothGattWrapper mMockBluetoothGattWrapper;
+ @Mock
+ private BluetoothDevice mMockBluetoothDevice;
+ @Mock
+ private BluetoothOperationExecutor mMockBluetoothOperationExecutor;
+ @Mock
+ private BluetoothGattService mMockBluetoothGattService;
+ @Mock
+ private BluetoothGattService mMockBluetoothGattService2;
+ @Mock
+ private BluetoothGattCharacteristic mMockBluetoothGattCharacteristic;
+ @Mock
+ private BluetoothGattCharacteristic mMockBluetoothGattCharacteristic2;
+ @Mock
+ private BluetoothGattDescriptor mMockBluetoothGattDescriptor;
+ @Mock
+ private BluetoothGattConnection.CharacteristicChangeListener mMockCharChangeListener;
+ @Mock
+ private BluetoothGattConnection.ChangeObserver mMockChangeObserver;
+ @Mock
+ private BluetoothGattConnection.ConnectionCloseListener mMockConnectionCloseListener;
+
+ @Captor
+ private ArgumentCaptor<Operation<?>> mOperationCaptor;
+ @Captor
+ private ArgumentCaptor<SynchronousOperation<?>> mSynchronousOperationCaptor;
+ @Captor
+ private ArgumentCaptor<BluetoothGattCharacteristic> mCharacteristicCaptor;
+ @Captor
+ private ArgumentCaptor<BluetoothGattDescriptor> mDescriptorCaptor;
+
+ private BluetoothGattConnection mBluetoothGattConnection;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ initMocks(this);
+
+ mBluetoothGattConnection = new BluetoothGattConnection(
+ mMockBluetoothGattWrapper,
+ mMockBluetoothOperationExecutor,
+ CONNECTION_OPTIONS);
+ mBluetoothGattConnection.onConnected();
+
+ when(mMockBluetoothGattWrapper.getDevice()).thenReturn(mMockBluetoothDevice);
+ when(mMockBluetoothGattWrapper.discoverServices()).thenReturn(true);
+ when(mMockBluetoothGattWrapper.refresh()).thenReturn(true);
+ when(mMockBluetoothGattWrapper.readCharacteristic(mMockBluetoothGattCharacteristic))
+ .thenReturn(true);
+ when(mMockBluetoothGattWrapper
+ .writeCharacteristic(ArgumentMatchers.<BluetoothGattCharacteristic>any(), any(),
+ anyInt()))
+ .thenReturn(BluetoothStatusCodes.SUCCESS);
+ when(mMockBluetoothGattWrapper.readDescriptor(mMockBluetoothGattDescriptor))
+ .thenReturn(true);
+ when(mMockBluetoothGattWrapper.writeDescriptor(
+ ArgumentMatchers.<BluetoothGattDescriptor>any(), any()))
+ .thenReturn(BluetoothStatusCodes.SUCCESS);
+ when(mMockBluetoothGattWrapper.readRemoteRssi()).thenReturn(true);
+ when(mMockBluetoothGattWrapper.requestConnectionPriority(CONNECTION_PRIORITY))
+ .thenReturn(true);
+ when(mMockBluetoothGattWrapper.requestMtu(MTU_REQUEST)).thenReturn(true);
+ when(mMockBluetoothGattWrapper.getServices())
+ .thenReturn(Arrays.asList(mMockBluetoothGattService));
+ when(mMockBluetoothGattService.getUuid()).thenReturn(SERVICE_UUID);
+ when(mMockBluetoothGattService.getCharacteristics())
+ .thenReturn(Arrays.asList(mMockBluetoothGattCharacteristic));
+ when(mMockBluetoothGattCharacteristic.getUuid()).thenReturn(CHARACTERISTIC_UUID);
+ when(mMockBluetoothGattCharacteristic.getProperties())
+ .thenReturn(
+ BluetoothGattCharacteristic.PROPERTY_NOTIFY
+ | BluetoothGattCharacteristic.PROPERTY_WRITE);
+ BluetoothGattDescriptor clientConfigDescriptor =
+ new BluetoothGattDescriptor(
+ ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION,
+ BluetoothGattDescriptor.PERMISSION_WRITE);
+ when(mMockBluetoothGattCharacteristic.getDescriptor(
+ ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION))
+ .thenReturn(clientConfigDescriptor);
+ when(mMockBluetoothGattCharacteristic.getDescriptors())
+ .thenReturn(Arrays.asList(mMockBluetoothGattDescriptor, clientConfigDescriptor));
+ when(mMockBluetoothGattDescriptor.getUuid()).thenReturn(DESCRIPTOR_UUID);
+ when(mMockBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getDevice() {
+ BluetoothDevice result = mBluetoothGattConnection.getDevice();
+
+ assertThat(result).isEqualTo(mMockBluetoothDevice);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getConnectionOptions() {
+ BluetoothGattHelper.ConnectionOptions result = mBluetoothGattConnection
+ .getConnectionOptions();
+
+ assertThat(result).isSameInstanceAs(CONNECTION_OPTIONS);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_isConnected_false_beforeConnection() {
+ mBluetoothGattConnection = new BluetoothGattConnection(
+ mMockBluetoothGattWrapper,
+ mMockBluetoothOperationExecutor,
+ CONNECTION_OPTIONS);
+
+ boolean result = mBluetoothGattConnection.isConnected();
+
+ assertThat(result).isFalse();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_isConnected_true_afterConnection() {
+ boolean result = mBluetoothGattConnection.isConnected();
+
+ assertThat(result).isTrue();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_isConnected_false_afterDisconnection() {
+ mBluetoothGattConnection.onClosed();
+
+ boolean result = mBluetoothGattConnection.isConnected();
+
+ assertThat(result).isFalse();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getService_notDiscovered() throws Exception {
+ BluetoothGattService result = mBluetoothGattConnection.getService(SERVICE_UUID);
+ verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
+ mSynchronousOperationCaptor.getValue().call();
+ verify(mMockBluetoothOperationExecutor)
+ .execute(
+ mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+
+ assertThat(result).isEqualTo(mMockBluetoothGattService);
+ verify(mMockBluetoothGattWrapper).discoverServices();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getService_alreadyDiscovered() throws Exception {
+ mBluetoothGattConnection.getService(SERVICE_UUID);
+ verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
+ mSynchronousOperationCaptor.getValue().call();
+ reset(mMockBluetoothOperationExecutor);
+
+ BluetoothGattService result = mBluetoothGattConnection.getService(SERVICE_UUID);
+
+ assertThat(result).isEqualTo(mMockBluetoothGattService);
+ // Verify that service discovery has been done only once
+ verifyNoMoreInteractions(mMockBluetoothOperationExecutor);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getService_notFound() throws Exception {
+ when(mMockBluetoothGattWrapper.getServices()).thenReturn(
+ Arrays.<BluetoothGattService>asList());
+
+ try {
+ mBluetoothGattConnection.getService(SERVICE_UUID);
+ fail("Expected BluetoothException");
+ } catch (BluetoothException expected) {
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getService_moreThanOne() throws Exception {
+ when(mMockBluetoothGattWrapper.getServices())
+ .thenReturn(Arrays.asList(mMockBluetoothGattService, mMockBluetoothGattService));
+
+ try {
+ mBluetoothGattConnection.getService(SERVICE_UUID);
+ fail("Expected BluetoothException");
+ } catch (BluetoothException expected) {
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getCharacteristic() throws Exception {
+ BluetoothGattCharacteristic result =
+ mBluetoothGattConnection.getCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID);
+
+ assertThat(result).isEqualTo(mMockBluetoothGattCharacteristic);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getCharacteristic_notFound() throws Exception {
+ when(mMockBluetoothGattService.getCharacteristics())
+ .thenReturn(Arrays.<BluetoothGattCharacteristic>asList());
+
+ try {
+ mBluetoothGattConnection.getCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID);
+ fail("Expected BluetoothException");
+ } catch (BluetoothException expected) {
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getCharacteristic_moreThanOne() throws Exception {
+ when(mMockBluetoothGattService.getCharacteristics())
+ .thenReturn(
+ Arrays.asList(mMockBluetoothGattCharacteristic,
+ mMockBluetoothGattCharacteristic));
+
+ try {
+ mBluetoothGattConnection.getCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID);
+ fail("Expected BluetoothException");
+ } catch (BluetoothException expected) {
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getCharacteristic_moreThanOneService() throws Exception {
+ // Add a new service with the same service UUID as our existing one, but add a different
+ // characteristic inside of it.
+ when(mMockBluetoothGattWrapper.getServices())
+ .thenReturn(Arrays.asList(mMockBluetoothGattService, mMockBluetoothGattService2));
+ when(mMockBluetoothGattService2.getUuid()).thenReturn(SERVICE_UUID);
+ when(mMockBluetoothGattService2.getCharacteristics())
+ .thenReturn(Arrays.asList(mMockBluetoothGattCharacteristic2));
+ when(mMockBluetoothGattCharacteristic2.getUuid())
+ .thenReturn(
+ new UUID(
+ CHARACTERISTIC_UUID.getMostSignificantBits(),
+ CHARACTERISTIC_UUID.getLeastSignificantBits() + 1));
+ when(mMockBluetoothGattCharacteristic2.getProperties())
+ .thenReturn(
+ BluetoothGattCharacteristic.PROPERTY_NOTIFY
+ | BluetoothGattCharacteristic.PROPERTY_WRITE);
+
+ mBluetoothGattConnection.getCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getDescriptor() throws Exception {
+ when(mMockBluetoothGattCharacteristic.getDescriptors())
+ .thenReturn(Arrays.asList(mMockBluetoothGattDescriptor));
+
+ BluetoothGattDescriptor result =
+ mBluetoothGattConnection
+ .getDescriptor(SERVICE_UUID, CHARACTERISTIC_UUID, DESCRIPTOR_UUID);
+
+ assertThat(result).isEqualTo(mMockBluetoothGattDescriptor);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getDescriptor_notFound() throws Exception {
+ when(mMockBluetoothGattCharacteristic.getDescriptors())
+ .thenReturn(Arrays.<BluetoothGattDescriptor>asList());
+
+ try {
+ mBluetoothGattConnection
+ .getDescriptor(SERVICE_UUID, CHARACTERISTIC_UUID, DESCRIPTOR_UUID);
+ fail("Expected BluetoothException");
+ } catch (BluetoothException expected) {
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getDescriptor_moreThanOne() throws Exception {
+ when(mMockBluetoothGattCharacteristic.getDescriptors())
+ .thenReturn(
+ Arrays.asList(mMockBluetoothGattDescriptor, mMockBluetoothGattDescriptor));
+
+ try {
+ mBluetoothGattConnection
+ .getDescriptor(SERVICE_UUID, CHARACTERISTIC_UUID, DESCRIPTOR_UUID);
+ fail("Expected BluetoothException");
+ } catch (BluetoothException expected) {
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_discoverServices() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new SynchronousOperation<>(
+ mMockBluetoothOperationExecutor, OperationType.NOTIFICATION_CHANGE)))
+ .thenReturn(mMockChangeObserver);
+
+ mBluetoothGattConnection.discoverServices();
+
+ verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
+ mSynchronousOperationCaptor.getValue().call();
+ verify(mMockBluetoothOperationExecutor)
+ .execute(
+ mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGattWrapper).discoverServices();
+ verify(mMockBluetoothGattWrapper, never()).refresh();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_discoverServices_serviceChange() throws Exception {
+ when(mMockBluetoothGattWrapper.getService(ReservedUuids.Services.GENERIC_ATTRIBUTE))
+ .thenReturn(mMockBluetoothGattService);
+ when(mMockBluetoothGattService
+ .getCharacteristic(ReservedUuids.Characteristics.SERVICE_CHANGE))
+ .thenReturn(mMockBluetoothGattCharacteristic);
+
+ mBluetoothGattConnection.discoverServices();
+
+ verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
+ mSynchronousOperationCaptor.getValue().call();
+ verify(mMockBluetoothOperationExecutor, times(2))
+ .execute(
+ mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
+ verify(mMockBluetoothGattWrapper).refresh();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_discoverServices_SelfDefinedServiceDynamic() throws Exception {
+ when(mMockBluetoothGattWrapper.getService(BluetoothConsts.SERVICE_DYNAMIC_SERVICE))
+ .thenReturn(mMockBluetoothGattService);
+ when(mMockBluetoothGattService
+ .getCharacteristic(BluetoothConsts.SERVICE_DYNAMIC_CHARACTERISTIC))
+ .thenReturn(mMockBluetoothGattCharacteristic);
+
+ mBluetoothGattConnection.discoverServices();
+
+ verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
+ mSynchronousOperationCaptor.getValue().call();
+ verify(mMockBluetoothOperationExecutor, times(2))
+ .execute(
+ mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
+ verify(mMockBluetoothGattWrapper).refresh();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_discoverServices_refreshWithGattErrorOnMncAbove() throws Exception {
+ if (VERSION.SDK_INT <= VERSION_CODES.LOLLIPOP_MR1) {
+ return;
+ }
+ mBluetoothGattConnection.discoverServices();
+ verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
+
+ doThrow(new BluetoothGattException("fail", BluetoothGattConnection.GATT_ERROR))
+ .doReturn(null)
+ .when(mMockBluetoothOperationExecutor)
+ .execute(isA(Operation.class),
+ eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
+ mSynchronousOperationCaptor.getValue().call();
+ verify(mMockBluetoothOperationExecutor, times(2))
+ .execute(
+ mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
+ verify(mMockBluetoothGattWrapper).refresh();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_discoverServices_refreshWithGattInternalErrorOnMncAbove() throws Exception {
+ if (VERSION.SDK_INT <= VERSION_CODES.LOLLIPOP_MR1) {
+ return;
+ }
+ mBluetoothGattConnection.discoverServices();
+ verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
+
+ doThrow(new BluetoothGattException("fail", BluetoothGattConnection.GATT_INTERNAL_ERROR))
+ .doReturn(null)
+ .when(mMockBluetoothOperationExecutor)
+ .execute(isA(Operation.class),
+ eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
+ mSynchronousOperationCaptor.getValue().call();
+ verify(mMockBluetoothOperationExecutor, times(2))
+ .execute(
+ mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.SLOW_OPERATION_TIMEOUT_MILLIS));
+ verify(mMockBluetoothGattWrapper).refresh();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_discoverServices_dynamicServices_notBonded() throws Exception {
+ when(mMockBluetoothGattWrapper.getService(ReservedUuids.Services.GENERIC_ATTRIBUTE))
+ .thenReturn(mMockBluetoothGattService);
+ when(mMockBluetoothGattService
+ .getCharacteristic(ReservedUuids.Characteristics.SERVICE_CHANGE))
+ .thenReturn(mMockBluetoothGattCharacteristic);
+ when(mMockBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
+
+ mBluetoothGattConnection.discoverServices();
+
+ verify(mMockBluetoothGattWrapper, never()).refresh();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_readCharacteristic() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<byte[]>(
+ OperationType.READ_CHARACTERISTIC,
+ mMockBluetoothGattWrapper,
+ mMockBluetoothGattCharacteristic),
+ BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS))
+ .thenReturn(DATA);
+
+ byte[] result = mBluetoothGattConnection
+ .readCharacteristic(mMockBluetoothGattCharacteristic);
+
+ assertThat(result).isEqualTo(DATA);
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mOperationCaptor.capture(), anyLong());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGattWrapper).readCharacteristic(mMockBluetoothGattCharacteristic);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_readCharacteristic_by_uuid() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<byte[]>(
+ OperationType.READ_CHARACTERISTIC,
+ mMockBluetoothGattWrapper,
+ mMockBluetoothGattCharacteristic),
+ BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS))
+ .thenReturn(DATA);
+
+ byte[] result = mBluetoothGattConnection
+ .readCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID);
+
+ assertThat(result).isEqualTo(DATA);
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mOperationCaptor.capture(), anyLong());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGattWrapper).readCharacteristic(mMockBluetoothGattCharacteristic);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_writeCharacteristic() throws Exception {
+ BluetoothGattCharacteristic characteristic =
+ new BluetoothGattCharacteristic(
+ CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_WRITE, 0);
+ mBluetoothGattConnection.writeCharacteristic(characteristic, DATA);
+
+ verify(mMockBluetoothOperationExecutor)
+ .execute(mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGattWrapper).writeCharacteristic(mCharacteristicCaptor.capture(),
+ eq(DATA), eq(characteristic.getWriteType()));
+ BluetoothGattCharacteristic writtenCharacteristic = mCharacteristicCaptor.getValue();
+ assertThat(writtenCharacteristic.getUuid()).isEqualTo(CHARACTERISTIC_UUID);
+ assertThat(writtenCharacteristic).isEqualTo(characteristic);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_writeCharacteristic_by_uuid() throws Exception {
+ mBluetoothGattConnection.writeCharacteristic(SERVICE_UUID, CHARACTERISTIC_UUID, DATA);
+
+ verify(mMockBluetoothOperationExecutor)
+ .execute(mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGattWrapper).writeCharacteristic(mCharacteristicCaptor.capture(),
+ eq(DATA), anyInt());
+ BluetoothGattCharacteristic writtenCharacteristic = mCharacteristicCaptor.getValue();
+ assertThat(writtenCharacteristic.getUuid()).isEqualTo(CHARACTERISTIC_UUID);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_readDescriptor() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<byte[]>(
+ OperationType.READ_DESCRIPTOR, mMockBluetoothGattWrapper,
+ mMockBluetoothGattDescriptor),
+ BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS))
+ .thenReturn(DATA);
+
+ byte[] result = mBluetoothGattConnection.readDescriptor(mMockBluetoothGattDescriptor);
+
+ assertThat(result).isEqualTo(DATA);
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mOperationCaptor.capture(), anyLong());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGattWrapper).readDescriptor(mMockBluetoothGattDescriptor);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_readDescriptor_by_uuid() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<byte[]>(
+ OperationType.READ_DESCRIPTOR, mMockBluetoothGattWrapper,
+ mMockBluetoothGattDescriptor),
+ BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS))
+ .thenReturn(DATA);
+
+ byte[] result =
+ mBluetoothGattConnection
+ .readDescriptor(SERVICE_UUID, CHARACTERISTIC_UUID, DESCRIPTOR_UUID);
+
+ assertThat(result).isEqualTo(DATA);
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mOperationCaptor.capture(), anyLong());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGattWrapper).readDescriptor(mMockBluetoothGattDescriptor);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_writeDescriptor() throws Exception {
+ BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(DESCRIPTOR_UUID, 0);
+ mBluetoothGattConnection.writeDescriptor(descriptor, DATA);
+
+ verify(mMockBluetoothOperationExecutor)
+ .execute(mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGattWrapper).writeDescriptor(mDescriptorCaptor.capture(), eq(DATA));
+ BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
+ assertThat(writtenDescriptor.getUuid()).isEqualTo(DESCRIPTOR_UUID);
+ assertThat(writtenDescriptor).isEqualTo(descriptor);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_writeDescriptor_by_uuid() throws Exception {
+ mBluetoothGattConnection.writeDescriptor(
+ SERVICE_UUID, CHARACTERISTIC_UUID, DESCRIPTOR_UUID, DATA);
+
+ verify(mMockBluetoothOperationExecutor)
+ .execute(mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGattWrapper).writeDescriptor(mDescriptorCaptor.capture(), eq(DATA));
+ BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
+ assertThat(writtenDescriptor.getUuid()).isEqualTo(DESCRIPTOR_UUID);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_readRemoteRssi() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<Integer>(OperationType.READ_RSSI, mMockBluetoothGattWrapper),
+ BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS))
+ .thenReturn(RSSI);
+
+ int result = mBluetoothGattConnection.readRemoteRssi();
+
+ assertThat(result).isEqualTo(RSSI);
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(
+ mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGattWrapper).readRemoteRssi();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getMaxDataPacketSize() throws Exception {
+ int result = mBluetoothGattConnection.getMaxDataPacketSize();
+
+ assertThat(result).isEqualTo(mBluetoothGattConnection.getMtu() - 3);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSetNotificationEnabled_indication_enable() throws Exception {
+ when(mMockBluetoothGattCharacteristic.getProperties())
+ .thenReturn(BluetoothGattCharacteristic.PROPERTY_INDICATE);
+
+ mBluetoothGattConnection.setNotificationEnabled(mMockBluetoothGattCharacteristic, true);
+
+ verify(mMockBluetoothGattWrapper)
+ .setCharacteristicNotification(mMockBluetoothGattCharacteristic, true);
+ verify(mMockBluetoothOperationExecutor).execute(mOperationCaptor.capture(), anyLong());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGattWrapper).writeDescriptor(mDescriptorCaptor.capture(),
+ eq(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE));
+ BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
+ assertThat(writtenDescriptor.getUuid())
+ .isEqualTo(ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getNotificationEnabled_notification_enable() throws Exception {
+ mBluetoothGattConnection.setNotificationEnabled(mMockBluetoothGattCharacteristic, true);
+
+ verify(mMockBluetoothGattWrapper)
+ .setCharacteristicNotification(mMockBluetoothGattCharacteristic, true);
+ verify(mMockBluetoothOperationExecutor).execute(mOperationCaptor.capture(), anyLong());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGattWrapper).writeDescriptor(mDescriptorCaptor.capture(),
+ eq(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE));
+ BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
+ assertThat(writtenDescriptor.getUuid())
+ .isEqualTo(ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_setNotificationEnabled_indication_disable() throws Exception {
+ when(mMockBluetoothGattCharacteristic.getProperties())
+ .thenReturn(BluetoothGattCharacteristic.PROPERTY_INDICATE);
+
+ mBluetoothGattConnection.setNotificationEnabled(mMockBluetoothGattCharacteristic, false);
+
+ verify(mMockBluetoothGattWrapper)
+ .setCharacteristicNotification(mMockBluetoothGattCharacteristic, false);
+ verify(mMockBluetoothOperationExecutor).execute(mOperationCaptor.capture(), anyLong());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGattWrapper).writeDescriptor(mDescriptorCaptor.capture(),
+ eq(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE));
+ BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
+ assertThat(writtenDescriptor.getUuid())
+ .isEqualTo(ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_setNotificationEnabled_notification_disable() throws Exception {
+ mBluetoothGattConnection.setNotificationEnabled(mMockBluetoothGattCharacteristic, false);
+
+ verify(mMockBluetoothGattWrapper)
+ .setCharacteristicNotification(mMockBluetoothGattCharacteristic, false);
+ verify(mMockBluetoothOperationExecutor).execute(mOperationCaptor.capture(), anyLong());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGattWrapper).writeDescriptor(mDescriptorCaptor.capture(),
+ eq(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE));
+ BluetoothGattDescriptor writtenDescriptor = mDescriptorCaptor.getValue();
+ assertThat(writtenDescriptor.getUuid())
+ .isEqualTo(ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_setNotificationEnabled_failure() throws Exception {
+ when(mMockBluetoothGattCharacteristic.getProperties())
+ .thenReturn(BluetoothGattCharacteristic.PROPERTY_READ);
+
+ try {
+ mBluetoothGattConnection.setNotificationEnabled(mMockBluetoothGattCharacteristic,
+ true);
+ fail("BluetoothException was expected");
+ } catch (BluetoothException expected) {
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_enableNotification_Uuid() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new SynchronousOperation<>(
+ mMockBluetoothOperationExecutor,
+ OperationType.NOTIFICATION_CHANGE,
+ mMockBluetoothGattCharacteristic)))
+ .thenReturn(mMockChangeObserver);
+ mBluetoothGattConnection.enableNotification(SERVICE_UUID, CHARACTERISTIC_UUID);
+
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mSynchronousOperationCaptor.capture());
+ ((ChangeObserver) mSynchronousOperationCaptor.getValue().call())
+ .setListener(mMockCharChangeListener);
+ mBluetoothGattConnection.onCharacteristicChanged(mMockBluetoothGattCharacteristic, DATA);
+ verify(mMockCharChangeListener).onValueChange(DATA);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_enableNotification() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new SynchronousOperation<>(
+ mMockBluetoothOperationExecutor,
+ OperationType.NOTIFICATION_CHANGE,
+ mMockBluetoothGattCharacteristic)))
+ .thenReturn(mMockChangeObserver);
+ mBluetoothGattConnection.enableNotification(mMockBluetoothGattCharacteristic);
+
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mSynchronousOperationCaptor.capture());
+ ((ChangeObserver) mSynchronousOperationCaptor.getValue().call())
+ .setListener(mMockCharChangeListener);
+
+ mBluetoothGattConnection.onCharacteristicChanged(mMockBluetoothGattCharacteristic, DATA);
+
+ verify(mMockCharChangeListener).onValueChange(DATA);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_enableNotification_observe() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new SynchronousOperation<>(
+ mMockBluetoothOperationExecutor,
+ OperationType.NOTIFICATION_CHANGE,
+ mMockBluetoothGattCharacteristic)))
+ .thenReturn(mMockChangeObserver);
+ mBluetoothGattConnection.enableNotification(mMockBluetoothGattCharacteristic);
+
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mSynchronousOperationCaptor.capture());
+ ChangeObserver changeObserver = (ChangeObserver) mSynchronousOperationCaptor.getValue()
+ .call();
+ mBluetoothGattConnection.onCharacteristicChanged(mMockBluetoothGattCharacteristic, DATA);
+ assertThat(changeObserver.waitForUpdate(TimeUnit.SECONDS.toMillis(1))).isEqualTo(DATA);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_disableNotification_Uuid() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new SynchronousOperation<>(
+ OperationType.NOTIFICATION_CHANGE, mMockBluetoothGattCharacteristic)))
+ .thenReturn(mMockChangeObserver);
+ mBluetoothGattConnection
+ .enableNotification(SERVICE_UUID, CHARACTERISTIC_UUID)
+ .setListener(mMockCharChangeListener);
+
+ mBluetoothGattConnection.disableNotification(SERVICE_UUID, CHARACTERISTIC_UUID);
+
+ mBluetoothGattConnection.onCharacteristicChanged(mMockBluetoothGattCharacteristic, DATA);
+ verify(mMockCharChangeListener, never()).onValueChange(DATA);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_disableNotification() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new SynchronousOperation<ChangeObserver>(
+ OperationType.NOTIFICATION_CHANGE, mMockBluetoothGattCharacteristic)))
+ .thenReturn(mMockChangeObserver);
+ mBluetoothGattConnection
+ .enableNotification(mMockBluetoothGattCharacteristic)
+ .setListener(mMockCharChangeListener);
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mSynchronousOperationCaptor.capture());
+ mSynchronousOperationCaptor.getValue().call();
+
+ mBluetoothGattConnection.disableNotification(mMockBluetoothGattCharacteristic);
+ verify(mMockBluetoothOperationExecutor).execute(mSynchronousOperationCaptor.capture());
+ mSynchronousOperationCaptor.getValue().call();
+
+ mBluetoothGattConnection.onCharacteristicChanged(mMockBluetoothGattCharacteristic, DATA);
+ verify(mMockCharChangeListener, never()).onValueChange(DATA);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_addCloseListener() throws Exception {
+ mBluetoothGattConnection.addCloseListener(mMockConnectionCloseListener);
+
+ mBluetoothGattConnection.onClosed();
+ verify(mMockConnectionCloseListener).onClose();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_removeCloseListener() throws Exception {
+ mBluetoothGattConnection.addCloseListener(mMockConnectionCloseListener);
+
+ mBluetoothGattConnection.removeCloseListener(mMockConnectionCloseListener);
+
+ mBluetoothGattConnection.onClosed();
+ verify(mMockConnectionCloseListener, never()).onClose();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_close() throws Exception {
+ mBluetoothGattConnection.close();
+
+ verify(mMockBluetoothOperationExecutor)
+ .execute(mOperationCaptor.capture(),
+ eq(BluetoothGattConnection.OPERATION_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothGattWrapper).disconnect();
+ verify(mMockBluetoothGattWrapper).close();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_onClosed() throws Exception {
+ mBluetoothGattConnection.onClosed();
+
+ verify(mMockBluetoothOperationExecutor, never())
+ .execute(mOperationCaptor.capture(), anyLong());
+ verify(mMockBluetoothGattWrapper).close();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelperTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelperTest.java
new file mode 100644
index 0000000..7c20be1
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/gatt/BluetoothGattHelperTest.java
@@ -0,0 +1,675 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.gatt;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanSettings;
+import android.content.Context;
+import android.os.ParcelUuid;
+import android.test.mock.MockContext;
+
+import androidx.test.filters.SdkSuppress;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.ConnectionOptions;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.OperationType;
+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.BluetoothGattWrapper;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.BluetoothLeScanner;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.ScanCallback;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.ScanResult;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
+
+import junit.framework.TestCase;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+import java.util.Arrays;
+import java.util.UUID;
+
+/**
+ * Unit tests for {@link BluetoothGattHelper}.
+ */
+public class BluetoothGattHelperTest extends TestCase {
+
+ private static final UUID SERVICE_UUID = UUID.randomUUID();
+ private static final int GATT_STATUS = 1234;
+ private static final Operation<BluetoothDevice> SCANNING_OPERATION =
+ new Operation<BluetoothDevice>(OperationType.SCAN);
+ private static final byte[] CHARACTERISTIC_VALUE = "characteristic_value".getBytes();
+ private static final byte[] DESCRIPTOR_VALUE = "descriptor_value".getBytes();
+ private static final int RSSI = -63;
+ private static final int MTU = 50;
+ private static final long CONNECT_TIMEOUT_MILLIS = 5000;
+
+ private Context mMockApplicationContext = new MockContext();
+ @Mock
+ private BluetoothAdapter mMockBluetoothAdapter;
+ @Mock
+ private BluetoothLeScanner mMockBluetoothLeScanner;
+ @Mock
+ private BluetoothOperationExecutor mMockBluetoothOperationExecutor;
+ @Mock
+ private BluetoothDevice mMockBluetoothDevice;
+ @Mock
+ private BluetoothGattConnection mMockBluetoothGattConnection;
+ @Mock
+ private BluetoothGattWrapper mMockBluetoothGattWrapper;
+ @Mock
+ private BluetoothGattCharacteristic mMockBluetoothGattCharacteristic;
+ @Mock
+ private BluetoothGattDescriptor mMockBluetoothGattDescriptor;
+ @Mock
+ private ScanResult mMockScanResult;
+
+ @Captor
+ private ArgumentCaptor<Operation<?>> mOperationCaptor;
+ @Captor
+ private ArgumentCaptor<ScanSettings> mScanSettingsCaptor;
+
+ private BluetoothGattHelper mBluetoothGattHelper;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ initMocks(this);
+
+ mBluetoothGattHelper = new BluetoothGattHelper(
+ mMockApplicationContext,
+ mMockBluetoothAdapter,
+ mMockBluetoothOperationExecutor);
+
+ when(mMockBluetoothAdapter.getBluetoothLeScanner()).thenReturn(mMockBluetoothLeScanner);
+ when(mMockBluetoothOperationExecutor.executeNonnull(SCANNING_OPERATION,
+ BluetoothGattHelper.LOW_LATENCY_SCAN_MILLIS)).thenReturn(mMockBluetoothDevice);
+ when(mMockBluetoothOperationExecutor.executeNonnull(SCANNING_OPERATION)).thenReturn(
+ mMockBluetoothDevice);
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<BluetoothGattConnection>(OperationType.CONNECT, mMockBluetoothDevice),
+ CONNECT_TIMEOUT_MILLIS))
+ .thenReturn(mMockBluetoothGattConnection);
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<BluetoothGattConnection>(OperationType.CONNECT,
+ mMockBluetoothDevice)))
+ .thenReturn(mMockBluetoothGattConnection);
+ when(mMockBluetoothGattCharacteristic.getValue()).thenReturn(CHARACTERISTIC_VALUE);
+ when(mMockBluetoothGattDescriptor.getValue()).thenReturn(DESCRIPTOR_VALUE);
+ when(mMockScanResult.getDevice()).thenReturn(mMockBluetoothDevice);
+ when(mMockBluetoothGattWrapper.getDevice()).thenReturn(mMockBluetoothDevice);
+ when(mMockBluetoothDevice.connectGatt(eq(mMockApplicationContext), anyBoolean(),
+ eq(mBluetoothGattHelper.mBluetoothGattCallback))).thenReturn(
+ mMockBluetoothGattWrapper);
+ when(mMockBluetoothDevice.connectGatt(eq(mMockApplicationContext), anyBoolean(),
+ eq(mBluetoothGattHelper.mBluetoothGattCallback), anyInt()))
+ .thenReturn(mMockBluetoothGattWrapper);
+ when(mMockBluetoothGattConnection.getConnectionOptions())
+ .thenReturn(ConnectionOptions.builder().build());
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_autoConnect_uuid_success_lowLatency() throws Exception {
+ BluetoothGattConnection result = mBluetoothGattHelper.autoConnect(SERVICE_UUID);
+
+ assertThat(result).isEqualTo(mMockBluetoothGattConnection);
+ verify(mMockBluetoothOperationExecutor, atLeastOnce())
+ .executeNonnull(mOperationCaptor.capture(),
+ anyLong());
+ for (Operation<?> operation : mOperationCaptor.getAllValues()) {
+ operation.run();
+ }
+ verify(mMockBluetoothLeScanner).startScan(eq(Arrays.asList(
+ new ScanFilter.Builder().setServiceUuid(new ParcelUuid(SERVICE_UUID)).build())),
+ mScanSettingsCaptor.capture(), eq(mBluetoothGattHelper.mScanCallback));
+ assertThat(mScanSettingsCaptor.getValue().getScanMode()).isEqualTo(
+ ScanSettings.SCAN_MODE_LOW_LATENCY);
+ verify(mMockBluetoothLeScanner).stopScan(mBluetoothGattHelper.mScanCallback);
+ verifyNoMoreInteractions(mMockBluetoothLeScanner);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_autoConnect_uuid_success_lowPower() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(SCANNING_OPERATION,
+ BluetoothGattHelper.LOW_LATENCY_SCAN_MILLIS)).thenThrow(
+ new BluetoothOperationTimeoutException("Timeout"));
+
+ BluetoothGattConnection result = mBluetoothGattHelper.autoConnect(SERVICE_UUID);
+
+ assertThat(result).isEqualTo(mMockBluetoothGattConnection);
+ verify(mMockBluetoothOperationExecutor).executeNonnull(mOperationCaptor.capture());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothLeScanner).startScan(eq(Arrays.asList(
+ new ScanFilter.Builder().setServiceUuid(new ParcelUuid(SERVICE_UUID)).build())),
+ mScanSettingsCaptor.capture(), eq(mBluetoothGattHelper.mScanCallback));
+ assertThat(mScanSettingsCaptor.getValue().getScanMode()).isEqualTo(
+ ScanSettings.SCAN_MODE_LOW_POWER);
+ verify(mMockBluetoothLeScanner, times(2)).stopScan(mBluetoothGattHelper.mScanCallback);
+ verifyNoMoreInteractions(mMockBluetoothLeScanner);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_autoConnect_uuid_success_afterRetry() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<BluetoothGattConnection>(OperationType.CONNECT, mMockBluetoothDevice),
+ BluetoothGattHelper.LOW_LATENCY_SCAN_MILLIS))
+ .thenThrow(new BluetoothException("first attempt fails!"))
+ .thenReturn(mMockBluetoothGattConnection);
+
+ BluetoothGattConnection result = mBluetoothGattHelper.autoConnect(SERVICE_UUID);
+
+ assertThat(result).isEqualTo(mMockBluetoothGattConnection);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_autoConnect_uuid_failure_scanning() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(SCANNING_OPERATION,
+ BluetoothGattHelper.LOW_LATENCY_SCAN_MILLIS)).thenThrow(
+ new BluetoothException("Scanning failed"));
+
+ try {
+ mBluetoothGattHelper.autoConnect(SERVICE_UUID);
+ fail("BluetoothException expected");
+ } catch (BluetoothException e) {
+ // expected
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_autoConnect_uuid_failure_connecting() throws Exception {
+ when(mMockBluetoothOperationExecutor.executeNonnull(
+ new Operation<BluetoothGattConnection>(OperationType.CONNECT, mMockBluetoothDevice),
+ CONNECT_TIMEOUT_MILLIS))
+ .thenThrow(new BluetoothException("Connect failed"));
+
+ try {
+ mBluetoothGattHelper.autoConnect(SERVICE_UUID);
+ fail("BluetoothException expected");
+ } catch (BluetoothException e) {
+ // expected
+ }
+ verify(mMockBluetoothOperationExecutor, times(3))
+ .executeNonnull(
+ new Operation<BluetoothGattConnection>(OperationType.CONNECT,
+ mMockBluetoothDevice),
+ CONNECT_TIMEOUT_MILLIS);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_autoConnect_uuid_failure_noBle() throws Exception {
+ when(mMockBluetoothAdapter.getBluetoothLeScanner()).thenReturn(null);
+
+ try {
+ mBluetoothGattHelper.autoConnect(SERVICE_UUID);
+ fail("BluetoothException expected");
+ } catch (BluetoothException e) {
+ // expected
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_connect() throws Exception {
+ BluetoothGattConnection result = mBluetoothGattHelper.connect(mMockBluetoothDevice);
+
+ assertThat(result).isEqualTo(mMockBluetoothGattConnection);
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mOperationCaptor.capture(), eq(CONNECT_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothDevice).connectGatt(mMockApplicationContext, false,
+ mBluetoothGattHelper.mBluetoothGattCallback,
+ android.bluetooth.BluetoothDevice.TRANSPORT_LE);
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper).getDevice())
+ .isEqualTo(mMockBluetoothDevice);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_connect_withOptionAutoConnect_success() throws Exception {
+ BluetoothGattConnection result = mBluetoothGattHelper
+ .connect(
+ mMockBluetoothDevice,
+ ConnectionOptions.builder()
+ .setAutoConnect(true)
+ .build());
+
+ assertThat(result).isEqualTo(mMockBluetoothGattConnection);
+ verify(mMockBluetoothOperationExecutor).executeNonnull(mOperationCaptor.capture());
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothDevice).connectGatt(mMockApplicationContext, true,
+ mBluetoothGattHelper.mBluetoothGattCallback,
+ android.bluetooth.BluetoothDevice.TRANSPORT_LE);
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)
+ .getConnectionOptions())
+ .isEqualTo(ConnectionOptions.builder()
+ .setAutoConnect(true)
+ .build());
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_connect_withOptionAutoConnect_failure_nullResult() throws Exception {
+ when(mMockBluetoothDevice.connectGatt(eq(mMockApplicationContext), anyBoolean(),
+ eq(mBluetoothGattHelper.mBluetoothGattCallback),
+ eq(android.bluetooth.BluetoothDevice.TRANSPORT_LE))).thenReturn(null);
+
+ try {
+ mBluetoothGattHelper.connect(
+ mMockBluetoothDevice,
+ ConnectionOptions.builder()
+ .setAutoConnect(true)
+ .build());
+ verify(mMockBluetoothOperationExecutor).executeNonnull(mOperationCaptor.capture());
+ mOperationCaptor.getValue().run();
+ fail("BluetoothException expected");
+ } catch (BluetoothException e) {
+ // expected
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_connect_withOptionRequestConnectionPriority_success() throws Exception {
+ // Operation succeeds on the 3rd try.
+ when(mMockBluetoothGattWrapper
+ .requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH))
+ .thenReturn(false)
+ .thenReturn(false)
+ .thenReturn(true);
+
+ BluetoothGattConnection result = mBluetoothGattHelper
+ .connect(
+ mMockBluetoothDevice,
+ ConnectionOptions.builder()
+ .setConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
+ .build());
+
+ assertThat(result).isEqualTo(mMockBluetoothGattConnection);
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mOperationCaptor.capture(), eq(CONNECT_TIMEOUT_MILLIS));
+ mOperationCaptor.getValue().run();
+ verify(mMockBluetoothDevice).connectGatt(mMockApplicationContext, false,
+ mBluetoothGattHelper.mBluetoothGattCallback,
+ android.bluetooth.BluetoothDevice.TRANSPORT_LE);
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)
+ .getConnectionOptions())
+ .isEqualTo(ConnectionOptions.builder()
+ .setConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
+ .build());
+ verify(mMockBluetoothGattWrapper, times(3))
+ .requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_connect_cancel() throws Exception {
+ mBluetoothGattHelper.connect(mMockBluetoothDevice);
+
+ verify(mMockBluetoothOperationExecutor)
+ .executeNonnull(mOperationCaptor.capture(), eq(CONNECT_TIMEOUT_MILLIS));
+ Operation<?> operation = mOperationCaptor.getValue();
+ operation.run();
+ operation.cancel();
+
+ verify(mMockBluetoothGattWrapper).disconnect();
+ verify(mMockBluetoothGattWrapper).close();
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)).isNull();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onConnectionStateChange_connected_success()
+ throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
+ mMockBluetoothGattConnection);
+
+ mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(
+ mMockBluetoothGattWrapper,
+ BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(
+ new Operation<>(OperationType.CONNECT, mMockBluetoothDevice),
+ BluetoothGatt.GATT_SUCCESS,
+ mMockBluetoothGattConnection);
+ verify(mMockBluetoothGattConnection).onConnected();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onConnectionStateChange_connected_success_withMtuOption()
+ throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
+ mMockBluetoothGattConnection);
+ when(mMockBluetoothGattConnection.getConnectionOptions())
+ .thenReturn(BluetoothGattHelper.ConnectionOptions.builder()
+ .setMtu(MTU)
+ .build());
+ when(mMockBluetoothGattWrapper.requestMtu(MTU)).thenReturn(true);
+
+ mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(
+ mMockBluetoothGattWrapper,
+ BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED);
+
+ verifyZeroInteractions(mMockBluetoothOperationExecutor);
+ verify(mMockBluetoothGattConnection, never()).onConnected();
+ verify(mMockBluetoothGattWrapper).requestMtu(MTU);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onConnectionStateChange_connected_success_failMtuOption()
+ throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
+ mMockBluetoothGattConnection);
+ when(mMockBluetoothGattConnection.getConnectionOptions())
+ .thenReturn(BluetoothGattHelper.ConnectionOptions.builder()
+ .setMtu(MTU)
+ .build());
+ when(mMockBluetoothGattWrapper.requestMtu(MTU)).thenReturn(false);
+
+ mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(
+ mMockBluetoothGattWrapper,
+ BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED);
+
+ verify(mMockBluetoothOperationExecutor).notifyFailure(
+ eq(new Operation<>(OperationType.CONNECT, mMockBluetoothDevice)),
+ any(BluetoothException.class));
+ verify(mMockBluetoothGattConnection, never()).onConnected();
+ verify(mMockBluetoothGattWrapper).disconnect();
+ verify(mMockBluetoothGattWrapper).close();
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)).isNull();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onConnectionStateChange_connected_unexpectedSuccess()
+ throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(
+ mMockBluetoothGattWrapper,
+ BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED);
+
+ verifyZeroInteractions(mMockBluetoothOperationExecutor);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onConnectionStateChange_connected_failure()
+ throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
+ mMockBluetoothGattConnection);
+
+ mBluetoothGattHelper.mBluetoothGattCallback
+ .onConnectionStateChange(
+ mMockBluetoothGattWrapper,
+ BluetoothGatt.GATT_FAILURE,
+ BluetoothGatt.STATE_CONNECTED);
+
+ verify(mMockBluetoothOperationExecutor)
+ .notifyCompletion(
+ new Operation<>(OperationType.CONNECT, mMockBluetoothDevice),
+ BluetoothGatt.GATT_FAILURE,
+ null);
+ verify(mMockBluetoothGattWrapper).disconnect();
+ verify(mMockBluetoothGattWrapper).close();
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)).isNull();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onConnectionStateChange_disconnected_unexpectedSuccess()
+ throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback
+ .onConnectionStateChange(
+ mMockBluetoothGattWrapper,
+ BluetoothGatt.GATT_SUCCESS,
+ BluetoothGatt.STATE_DISCONNECTED);
+
+ verifyZeroInteractions(mMockBluetoothOperationExecutor);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onConnectionStateChange_disconnected_notConnected()
+ throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
+ mMockBluetoothGattConnection);
+ when(mMockBluetoothGattConnection.isConnected()).thenReturn(false);
+
+ mBluetoothGattHelper.mBluetoothGattCallback
+ .onConnectionStateChange(
+ mMockBluetoothGattWrapper,
+ GATT_STATUS,
+ BluetoothGatt.STATE_DISCONNECTED);
+
+ verify(mMockBluetoothOperationExecutor)
+ .notifyCompletion(
+ new Operation<>(OperationType.CONNECT, mMockBluetoothDevice),
+ GATT_STATUS,
+ null);
+ verify(mMockBluetoothGattWrapper).disconnect();
+ verify(mMockBluetoothGattWrapper).close();
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)).isNull();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onConnectionStateChange_disconnected_success()
+ throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
+ mMockBluetoothGattConnection);
+ when(mMockBluetoothGattConnection.isConnected()).thenReturn(true);
+
+ mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(
+ mMockBluetoothGattWrapper,
+ BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_DISCONNECTED);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(
+ new Operation<>(OperationType.DISCONNECT, mMockBluetoothDevice),
+ BluetoothGatt.GATT_SUCCESS);
+ verify(mMockBluetoothGattConnection).onClosed();
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)).isNull();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onConnectionStateChange_disconnected_failure()
+ throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
+ mMockBluetoothGattConnection);
+ when(mMockBluetoothGattConnection.isConnected()).thenReturn(true);
+
+ mBluetoothGattHelper.mBluetoothGattCallback.onConnectionStateChange(
+ mMockBluetoothGattWrapper,
+ BluetoothGatt.GATT_FAILURE, BluetoothGatt.STATE_DISCONNECTED);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(
+ new Operation<>(OperationType.DISCONNECT, mMockBluetoothDevice),
+ BluetoothGatt.GATT_FAILURE);
+ verify(mMockBluetoothGattConnection).onClosed();
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)).isNull();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onServicesDiscovered() throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback.onServicesDiscovered(mMockBluetoothGattWrapper,
+ GATT_STATUS);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(
+ new Operation<Void>(OperationType.DISCOVER_SERVICES_INTERNAL,
+ mMockBluetoothGattWrapper),
+ GATT_STATUS);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onCharacteristicRead() throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback.onCharacteristicRead(mMockBluetoothGattWrapper,
+ mMockBluetoothGattCharacteristic, GATT_STATUS);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(new Operation<byte[]>(
+ OperationType.READ_CHARACTERISTIC, mMockBluetoothGattWrapper,
+ mMockBluetoothGattCharacteristic),
+ GATT_STATUS, CHARACTERISTIC_VALUE);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onCharacteristicWrite() throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback.onCharacteristicWrite(mMockBluetoothGattWrapper,
+ mMockBluetoothGattCharacteristic, GATT_STATUS);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(new Operation<Void>(
+ OperationType.WRITE_CHARACTERISTIC, mMockBluetoothGattWrapper,
+ mMockBluetoothGattCharacteristic),
+ GATT_STATUS);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onDescriptorRead() throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback.onDescriptorRead(mMockBluetoothGattWrapper,
+ mMockBluetoothGattDescriptor, GATT_STATUS);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(new Operation<byte[]>(
+ OperationType.READ_DESCRIPTOR, mMockBluetoothGattWrapper,
+ mMockBluetoothGattDescriptor),
+ GATT_STATUS,
+ DESCRIPTOR_VALUE);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onDescriptorWrite() throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback.onDescriptorWrite(mMockBluetoothGattWrapper,
+ mMockBluetoothGattDescriptor, GATT_STATUS);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(new Operation<Void>(
+ OperationType.WRITE_DESCRIPTOR, mMockBluetoothGattWrapper,
+ mMockBluetoothGattDescriptor),
+ GATT_STATUS);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onReadRemoteRssi() throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback.onReadRemoteRssi(mMockBluetoothGattWrapper,
+ RSSI, GATT_STATUS);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(
+ new Operation<Integer>(OperationType.READ_RSSI, mMockBluetoothGattWrapper),
+ GATT_STATUS, RSSI);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onReliableWriteCompleted() throws Exception {
+ mBluetoothGattHelper.mBluetoothGattCallback.onReliableWriteCompleted(
+ mMockBluetoothGattWrapper,
+ GATT_STATUS);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(
+ new Operation<Void>(OperationType.WRITE_RELIABLE, mMockBluetoothGattWrapper),
+ GATT_STATUS);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onMtuChanged() throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
+ mMockBluetoothGattConnection);
+ when(mMockBluetoothGattConnection.isConnected()).thenReturn(true);
+
+ mBluetoothGattHelper.mBluetoothGattCallback
+ .onMtuChanged(mMockBluetoothGattWrapper, MTU, GATT_STATUS);
+
+ verify(mMockBluetoothOperationExecutor).notifyCompletion(
+ new Operation<>(OperationType.CHANGE_MTU, mMockBluetoothGattWrapper), GATT_STATUS,
+ MTU);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBluetoothGattCallback_onMtuChangedDuringConnection_success() throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
+ mMockBluetoothGattConnection);
+ when(mMockBluetoothGattConnection.isConnected()).thenReturn(false);
+
+ mBluetoothGattHelper.mBluetoothGattCallback.onMtuChanged(
+ mMockBluetoothGattWrapper, MTU, BluetoothGatt.GATT_SUCCESS);
+
+ verify(mMockBluetoothGattConnection).onConnected();
+ verify(mMockBluetoothOperationExecutor)
+ .notifyCompletion(
+ new Operation<>(OperationType.CONNECT, mMockBluetoothDevice),
+ BluetoothGatt.GATT_SUCCESS,
+ mMockBluetoothGattConnection);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testBluetoothGattCallback_onMtuChangedDuringConnection_fail() throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
+ mMockBluetoothGattConnection);
+ when(mMockBluetoothGattConnection.isConnected()).thenReturn(false);
+
+ mBluetoothGattHelper.mBluetoothGattCallback
+ .onMtuChanged(mMockBluetoothGattWrapper, MTU, GATT_STATUS);
+
+ verify(mMockBluetoothGattConnection).onConnected();
+ verify(mMockBluetoothOperationExecutor)
+ .notifyCompletion(
+ new Operation<>(OperationType.CONNECT, mMockBluetoothDevice),
+ GATT_STATUS,
+ mMockBluetoothGattConnection);
+ verify(mMockBluetoothGattWrapper).disconnect();
+ verify(mMockBluetoothGattWrapper).close();
+ assertThat(mBluetoothGattHelper.mConnections.get(mMockBluetoothGattWrapper)).isNull();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_BluetoothGattCallback_onCharacteristicChanged() throws Exception {
+ mBluetoothGattHelper.mConnections.put(mMockBluetoothGattWrapper,
+ mMockBluetoothGattConnection);
+
+ mBluetoothGattHelper.mBluetoothGattCallback.onCharacteristicChanged(
+ mMockBluetoothGattWrapper,
+ mMockBluetoothGattCharacteristic);
+
+ verify(mMockBluetoothGattConnection).onCharacteristicChanged(
+ mMockBluetoothGattCharacteristic,
+ CHARACTERISTIC_VALUE);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_ScanCallback_onScanFailed() throws Exception {
+ mBluetoothGattHelper.mScanCallback.onScanFailed(ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
+
+ verify(mMockBluetoothOperationExecutor).notifyFailure(
+ eq(new Operation<BluetoothDevice>(OperationType.SCAN)),
+ isA(BluetoothException.class));
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_ScanCallback_onScanResult() throws Exception {
+ mBluetoothGattHelper.mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES,
+ mMockScanResult);
+
+ verify(mMockBluetoothOperationExecutor).notifySuccess(
+ new Operation<BluetoothDevice>(OperationType.SCAN), mMockBluetoothDevice);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtilsTest.java
new file mode 100644
index 0000000..47182c3
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtilsTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.util;
+
+import static com.android.server.nearby.common.bluetooth.util.BluetoothGattUtils.getMessageForStatusCode;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.bluetooth.BluetoothGatt;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.UUID;
+
+/** Unit tests for {@link BluetoothGattUtils}. */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothGattUtilsTest {
+ private static final UUID TEST_UUID = UUID.randomUUID();
+ private static final ImmutableSet<String> GATT_HIDDEN_CONSTANTS = ImmutableSet.of(
+ "GATT_WRITE_REQUEST_BUSY", "GATT_WRITE_REQUEST_FAIL", "GATT_WRITE_REQUEST_SUCCESS");
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetMessageForStatusCode() throws Exception {
+ Field[] publicFields = BluetoothGatt.class.getFields();
+ for (Field field : publicFields) {
+ if ((field.getModifiers() & Modifier.STATIC) == 0
+ || field.getDeclaringClass() != BluetoothGatt.class) {
+ continue;
+ }
+ String fieldName = field.getName();
+ if (!fieldName.startsWith("GATT_") || GATT_HIDDEN_CONSTANTS.contains(fieldName)) {
+ continue;
+ }
+ int fieldValue = (Integer) field.get(null);
+ assertThat(getMessageForStatusCode(fieldValue)).isEqualTo(fieldName);
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/util/BluetoothOperationExecutorTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/util/BluetoothOperationExecutorTest.java
new file mode 100644
index 0000000..7b3ebab
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/util/BluetoothOperationExecutorTest.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.bluetooth.BluetoothGatt;
+
+import androidx.test.filters.SdkSuppress;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.testability.NonnullProvider;
+import com.android.server.nearby.common.bluetooth.testability.TimeProvider;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.SynchronousOperation;
+
+import junit.framework.TestCase;
+
+import org.mockito.Mock;
+
+import java.util.Arrays;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+
+/**
+ * Unit tests for {@link BluetoothOperationExecutor}.
+ */
+public class BluetoothOperationExecutorTest extends TestCase {
+
+ private static final String OPERATION_RESULT = "result";
+ private static final String EXCEPTION_REASON = "exception";
+ private static final long TIME = 1234;
+ private static final long TIMEOUT = 121212;
+
+ @Mock
+ private NonnullProvider<BlockingQueue<Object>> mMockBlockingQueueProvider;
+ @Mock
+ private TimeProvider mMockTimeProvider;
+ @Mock
+ private BlockingQueue<Object> mMockBlockingQueue;
+ @Mock
+ private Semaphore mMockSemaphore;
+ @Mock
+ private Operation<String> mMockStringOperation;
+ @Mock
+ private Operation<Void> mMockVoidOperation;
+ @Mock
+ private Future<Object> mMockFuture;
+ @Mock
+ private Future<Object> mMockFuture2;
+
+ private BluetoothOperationExecutor mBluetoothOperationExecutor;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ initMocks(this);
+
+ when(mMockBlockingQueueProvider.get()).thenReturn(mMockBlockingQueue);
+ when(mMockSemaphore.tryAcquire()).thenReturn(true);
+ when(mMockTimeProvider.getTimeMillis()).thenReturn(TIME);
+
+ mBluetoothOperationExecutor =
+ new BluetoothOperationExecutor(mMockSemaphore, mMockTimeProvider,
+ mMockBlockingQueueProvider);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testExecute() throws Exception {
+ when(mMockBlockingQueue.take()).thenReturn(OPERATION_RESULT);
+
+ String result = mBluetoothOperationExecutor.execute(mMockStringOperation);
+
+ verify(mMockStringOperation).execute(mBluetoothOperationExecutor);
+ assertThat(result).isEqualTo(OPERATION_RESULT);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testExecuteWithTimeout() throws Exception {
+ when(mMockBlockingQueue.poll(TIMEOUT, TimeUnit.MILLISECONDS)).thenReturn(OPERATION_RESULT);
+
+ String result = mBluetoothOperationExecutor.execute(mMockStringOperation, TIMEOUT);
+
+ verify(mMockStringOperation).execute(mBluetoothOperationExecutor);
+ assertThat(result).isEqualTo(OPERATION_RESULT);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSchedule() throws Exception {
+ when(mMockBlockingQueue.poll(TIMEOUT, TimeUnit.MILLISECONDS)).thenReturn(OPERATION_RESULT);
+
+ Future<String> result = mBluetoothOperationExecutor.schedule(mMockStringOperation);
+
+ verify(mMockStringOperation).execute(mBluetoothOperationExecutor);
+ assertThat(result.get(TIMEOUT, TimeUnit.MILLISECONDS)).isEqualTo(OPERATION_RESULT);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testScheduleOtherOperationInProgress() throws Exception {
+ when(mMockSemaphore.tryAcquire()).thenReturn(false);
+ when(mMockBlockingQueue.poll(TIMEOUT, TimeUnit.MILLISECONDS)).thenReturn(OPERATION_RESULT);
+
+ Future<String> result = mBluetoothOperationExecutor.schedule(mMockStringOperation);
+
+ verify(mMockStringOperation, never()).run();
+
+ when(mMockSemaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)).thenReturn(true);
+
+ assertThat(result.get(TIMEOUT, TimeUnit.MILLISECONDS)).isEqualTo(OPERATION_RESULT);
+ verify(mMockStringOperation).execute(mBluetoothOperationExecutor);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testNotifySuccessWithResult() throws Exception {
+ when(mMockBlockingQueueProvider.get()).thenReturn(new LinkedBlockingDeque<Object>());
+ Future<String> future = mBluetoothOperationExecutor.schedule(mMockStringOperation);
+
+ mBluetoothOperationExecutor.notifySuccess(mMockStringOperation, OPERATION_RESULT);
+
+ assertThat(future.get(1, TimeUnit.MILLISECONDS)).isEqualTo(OPERATION_RESULT);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testNotifySuccessTwice() throws Exception {
+ BlockingQueue<Object> resultQueue = new LinkedBlockingDeque<Object>();
+ when(mMockBlockingQueueProvider.get()).thenReturn(resultQueue);
+ Future<String> future = mBluetoothOperationExecutor.schedule(mMockStringOperation);
+
+ mBluetoothOperationExecutor.notifySuccess(mMockStringOperation, OPERATION_RESULT);
+
+ assertThat(future.get(1, TimeUnit.MILLISECONDS)).isEqualTo(OPERATION_RESULT);
+
+ // the second notification should be ignored
+ mBluetoothOperationExecutor.notifySuccess(mMockStringOperation, OPERATION_RESULT);
+ assertThat(resultQueue).isEmpty();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testNotifySuccessWithNullResult() throws Exception {
+ when(mMockBlockingQueueProvider.get()).thenReturn(new LinkedBlockingDeque<Object>());
+ Future<String> future = mBluetoothOperationExecutor.schedule(mMockStringOperation);
+
+ mBluetoothOperationExecutor.notifySuccess(mMockStringOperation, null);
+
+ assertThat(future.get(1, TimeUnit.MILLISECONDS)).isNull();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testNotifySuccess() throws Exception {
+ when(mMockBlockingQueueProvider.get()).thenReturn(new LinkedBlockingDeque<Object>());
+ Future<Void> future = mBluetoothOperationExecutor.schedule(mMockVoidOperation);
+
+ mBluetoothOperationExecutor.notifySuccess(mMockVoidOperation);
+
+ future.get(1, TimeUnit.MILLISECONDS);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testNotifyCompletionSuccess() throws Exception {
+ when(mMockBlockingQueueProvider.get()).thenReturn(new LinkedBlockingDeque<Object>());
+ Future<Void> future = mBluetoothOperationExecutor.schedule(mMockVoidOperation);
+
+ mBluetoothOperationExecutor
+ .notifyCompletion(mMockVoidOperation, BluetoothGatt.GATT_SUCCESS);
+
+ future.get(1, TimeUnit.MILLISECONDS);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testNotifyCompletionFailure() throws Exception {
+ when(mMockBlockingQueueProvider.get()).thenReturn(new LinkedBlockingDeque<Object>());
+ Future<Void> future = mBluetoothOperationExecutor.schedule(mMockVoidOperation);
+
+ mBluetoothOperationExecutor
+ .notifyCompletion(mMockVoidOperation, BluetoothGatt.GATT_FAILURE);
+
+ try {
+ BluetoothOperationExecutor.getResult(future, 1);
+ fail("Expected BluetoothException");
+ } catch (BluetoothException e) {
+ //expected
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testNotifyFailure() throws Exception {
+ when(mMockBlockingQueueProvider.get()).thenReturn(new LinkedBlockingDeque<Object>());
+ Future<Void> future = mBluetoothOperationExecutor.schedule(mMockVoidOperation);
+
+ mBluetoothOperationExecutor
+ .notifyFailure(mMockVoidOperation, new BluetoothException("test"));
+
+ try {
+ BluetoothOperationExecutor.getResult(future, 1);
+ fail("Expected BluetoothException");
+ } catch (BluetoothException e) {
+ //expected
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWaitFor() throws Exception {
+ mBluetoothOperationExecutor.waitFor(Arrays.asList(mMockFuture, mMockFuture2));
+
+ verify(mMockFuture).get();
+ verify(mMockFuture2).get();
+ }
+
+ @SuppressWarnings("unchecked")
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWaitForWithTimeout() throws Exception {
+ mBluetoothOperationExecutor.waitFor(
+ Arrays.asList(mMockFuture, mMockFuture2),
+ TIMEOUT);
+
+ verify(mMockFuture).get(TIMEOUT, TimeUnit.MILLISECONDS);
+ verify(mMockFuture2).get(TIMEOUT, TimeUnit.MILLISECONDS);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetResult() throws Exception {
+ when(mMockFuture.get()).thenReturn(OPERATION_RESULT);
+
+ Object result = BluetoothOperationExecutor.getResult(mMockFuture);
+
+ assertThat(result).isEqualTo(OPERATION_RESULT);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetResultWithTimeout() throws Exception {
+ when(mMockFuture.get(TIMEOUT, TimeUnit.MILLISECONDS)).thenThrow(new TimeoutException());
+
+ try {
+ BluetoothOperationExecutor.getResult(mMockFuture, TIMEOUT);
+ fail("Expected BluetoothOperationTimeoutException");
+ } catch (BluetoothOperationTimeoutException e) {
+ //expected
+ }
+ verify(mMockFuture).cancel(true);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_SynchronousOperation_execute() throws Exception {
+ when(mMockBlockingQueueProvider.get()).thenReturn(mMockBlockingQueue);
+ SynchronousOperation<String> synchronousOperation = new SynchronousOperation<String>() {
+ @Override
+ public String call() throws BluetoothException {
+ return OPERATION_RESULT;
+ }
+ };
+
+ @SuppressWarnings("unused") // future return.
+ Future<?> possiblyIgnoredError = mBluetoothOperationExecutor.schedule(synchronousOperation);
+
+ verify(mMockBlockingQueue).add(OPERATION_RESULT);
+ verify(mMockSemaphore).release();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_SynchronousOperation_exception() throws Exception {
+ final BluetoothException exception = new BluetoothException(EXCEPTION_REASON);
+ when(mMockBlockingQueueProvider.get()).thenReturn(mMockBlockingQueue);
+ SynchronousOperation<String> synchronousOperation = new SynchronousOperation<String>() {
+ @Override
+ public String call() throws BluetoothException {
+ throw exception;
+ }
+ };
+
+ @SuppressWarnings("unused") // future return.
+ Future<?> possiblyIgnoredError = mBluetoothOperationExecutor.schedule(synchronousOperation);
+
+ verify(mMockBlockingQueue).add(exception);
+ verify(mMockSemaphore).release();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_AsynchronousOperation_exception() throws Exception {
+ final BluetoothException exception = new BluetoothException(EXCEPTION_REASON);
+ when(mMockBlockingQueueProvider.get()).thenReturn(mMockBlockingQueue);
+ Operation<String> operation = new Operation<String>() {
+ @Override
+ public void run() throws BluetoothException {
+ throw exception;
+ }
+ };
+
+ @SuppressWarnings("unused") // future return.
+ Future<?> possiblyIgnoredError = mBluetoothOperationExecutor.schedule(operation);
+
+ verify(mMockBlockingQueue).add(exception);
+ verify(mMockSemaphore).release();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/EventLoopTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/EventLoopTest.java
new file mode 100644
index 0000000..70dcec8
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/EventLoopTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.eventloop;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class EventLoopTest {
+ private static final String TAG = "EventLoopTest";
+
+ private final EventLoop mEventLoop = EventLoop.newInstance(TAG);
+ private final List<Integer> mExecutedRunnables = new ArrayList<>();
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ /*
+ @Test
+ public void remove() {
+ mEventLoop.postRunnable(new NumberedRunnable(0));
+ NumberedRunnable runnableToAddAndRemove = new NumberedRunnable(1);
+ mEventLoop.postRunnable(runnableToAddAndRemove);
+ mEventLoop.removeRunnable(runnableToAddAndRemove);
+ mEventLoop.postRunnable(new NumberedRunnable(2));
+
+ assertThat(mExecutedRunnables).containsExactly(0, 2);
+ }
+ */
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void isPosted() {
+ NumberedRunnable runnable = new NumberedRunnable(0);
+ mEventLoop.postRunnableDelayed(runnable, 10 * 1000L);
+ assertThat(mEventLoop.isPosted(runnable)).isTrue();
+ mEventLoop.removeRunnable(runnable);
+ assertThat(mEventLoop.isPosted(runnable)).isFalse();
+
+ // Let a runnable execute, then verify that it's not posted.
+ mEventLoop.postRunnable(runnable);
+ assertThat(mEventLoop.isPosted(runnable)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void postAndWaitAfterDestroy() throws InterruptedException {
+ mEventLoop.destroy();
+ mEventLoop.postAndWait(new NumberedRunnable(0));
+
+ assertThat(mExecutedRunnables).isEmpty();
+ }
+
+
+ private class NumberedRunnable extends NamedRunnable {
+ private final int mId;
+
+ private NumberedRunnable(int id) {
+ super("NumberedRunnable:" + id);
+ this.mId = id;
+ }
+
+ @Override
+ public void run() {
+ // Note: when running in robolectric, this is not actually executed on a different
+ // thread, it's executed in the same thread the test runs in, so this is safe.
+ mExecutedRunnables.add(mId);
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
new file mode 100644
index 0000000..346a961
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.nearby.FastPairDevice;
+
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
+import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
+import com.android.server.nearby.provider.FastPairDataProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import service.proto.Rpcs;
+
+public class FastPairAdvHandlerTest {
+ @Mock
+ private Context mContext;
+ @Mock
+ private FastPairDataProvider mFastPairDataProvider;
+ @Mock
+ private FastPairHalfSheetManager mFastPairHalfSheetManager;
+ @Mock
+ private FastPairNotificationManager mFastPairNotificationManager;
+ private static final String BLUETOOTH_ADDRESS = "AA:BB:CC:DD";
+ private static final int CLOSE_RSSI = -80;
+ private static final int FAR_AWAY_RSSI = -120;
+ private static final int TX_POWER = -70;
+ private static final byte[] INITIAL_BYTE_ARRAY = new byte[]{0x01, 0x02, 0x03};
+
+ LocatorContextWrapper mLocatorContextWrapper;
+ FastPairAdvHandler mFastPairAdvHandler;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mLocatorContextWrapper = new LocatorContextWrapper(mContext);
+ mLocatorContextWrapper.getLocator().overrideBindingForTest(
+ FastPairHalfSheetManager.class, mFastPairHalfSheetManager
+ );
+ mLocatorContextWrapper.getLocator().overrideBindingForTest(
+ FastPairNotificationManager.class, mFastPairNotificationManager
+ );
+ when(mFastPairDataProvider.loadFastPairAntispoofKeyDeviceMetadata(any()))
+ .thenReturn(Rpcs.GetObservedDeviceResponse.getDefaultInstance());
+ mFastPairAdvHandler = new FastPairAdvHandler(mLocatorContextWrapper, mFastPairDataProvider);
+ }
+
+ @Test
+ public void testInitialBroadcast() {
+ FastPairDevice fastPairDevice = new FastPairDevice.Builder()
+ .setData(INITIAL_BYTE_ARRAY)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setRssi(CLOSE_RSSI)
+ .setTxPower(TX_POWER)
+ .build();
+
+ mFastPairAdvHandler.handleBroadcast(fastPairDevice);
+
+ verify(mFastPairHalfSheetManager).showHalfSheet(any());
+ }
+
+ @Test
+ public void testInitialBroadcast_farAway_notShowHalfSheet() {
+ FastPairDevice fastPairDevice = new FastPairDevice.Builder()
+ .setData(INITIAL_BYTE_ARRAY)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setRssi(FAR_AWAY_RSSI)
+ .setTxPower(TX_POWER)
+ .build();
+
+ mFastPairAdvHandler.handleBroadcast(fastPairDevice);
+
+ verify(mFastPairHalfSheetManager, never()).showHalfSheet(any());
+ }
+
+ @Test
+ public void testSubsequentBroadcast() {
+ byte[] fastPairRecordWithBloomFilter =
+ new byte[]{
+ (byte) 0x02,
+ (byte) 0x01,
+ (byte) 0x02, // Flags
+ (byte) 0x02,
+ (byte) 0x0A,
+ (byte) 0xEB, // Tx Power (-20)
+ (byte) 0x0B,
+ (byte) 0x16,
+ (byte) 0x2C,
+ (byte) 0xFE, // FastPair Service Data
+ (byte) 0x00, // Flags (model ID length = 3)
+ (byte) 0x40, // Account key hash flags (length = 4, type = 0)
+ (byte) 0x11,
+ (byte) 0x22,
+ (byte) 0x33,
+ (byte) 0x44, // Account key hash (0x11223344)
+ (byte) 0x11, // Account key salt flags (length = 1, type = 1)
+ (byte) 0x55, // Account key salt
+ };
+ FastPairDevice fastPairDevice = new FastPairDevice.Builder()
+ .setData(fastPairRecordWithBloomFilter)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setRssi(CLOSE_RSSI)
+ .setTxPower(TX_POWER)
+ .build();
+
+ mFastPairAdvHandler.handleBroadcast(fastPairDevice);
+
+ verify(mFastPairHalfSheetManager, never()).showHalfSheet(any());
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairManagerTest.java
new file mode 100644
index 0000000..26d1847
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairManagerTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+
+public class FastPairManagerTest {
+ private FastPairManager mFastPairManager;
+ @Mock private Context mContext;
+ private LocatorContextWrapper mLocatorContextWrapper;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mLocatorContextWrapper = new LocatorContextWrapper(mContext);
+ mFastPairManager = new FastPairManager(mLocatorContextWrapper);
+ when(mContext.getContentResolver()).thenReturn(
+ InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testFastPairInit() {
+ mFastPairManager.initiate();
+
+ verify(mContext, times(1)).registerReceiver(any(), any());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testFastPairCleanUp() {
+ mFastPairManager.cleanUp();
+
+ verify(mContext, times(1)).unregisterReceiver(any());
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/ModuleTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/ModuleTest.java
new file mode 100644
index 0000000..bb4e3d0
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/ModuleTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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 src.com.android.server.nearby.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
+
+import com.android.server.nearby.common.eventloop.EventLoop;
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.fastpair.FastPairAdvHandler;
+import com.android.server.nearby.fastpair.FastPairModule;
+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 org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+
+import java.time.Clock;
+
+import src.com.android.server.nearby.fastpair.testing.MockingLocator;
+
+public class ModuleTest {
+ private Locator mLocator;
+
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mLocator = MockingLocator.withMocksOnly(ApplicationProvider.getApplicationContext());
+ mLocator.bind(new FastPairModule());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void genericConstructor() {
+ assertThat(mLocator.get(FastPairCacheManager.class)).isNotNull();
+ assertThat(mLocator.get(FootprintsDeviceManager.class)).isNotNull();
+ assertThat(mLocator.get(EventLoop.class)).isNotNull();
+ assertThat(mLocator.get(FastPairHalfSheetManager.class)).isNotNull();
+ assertThat(mLocator.get(FastPairAdvHandler.class)).isNotNull();
+ assertThat(mLocator.get(Clock.class)).isNotNull();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void genericDestroy() {
+ mLocator.destroy();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java
new file mode 100644
index 0000000..adae97d
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.cache;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
+
+import com.google.protobuf.ByteString;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import service.proto.Cache;
+
+public class FastPairCacheManagerTest {
+
+ private static final String MODEL_ID = "001";
+ private static final String MODEL_ID2 = "002";
+ private static final String APP_NAME = "APP_NAME";
+ private static final String MAC_ADDRESS = "00:11:22:33";
+ private static final ByteString ACCOUNT_KEY = ByteString.copyFromUtf8("axgs");
+ private static final String MAC_ADDRESS_B = "00:11:22:44";
+ private static final ByteString ACCOUNT_KEY_B = ByteString.copyFromUtf8("axgb");
+
+ @Mock
+ DiscoveryItem mDiscoveryItem;
+ @Mock
+ DiscoveryItem mDiscoveryItem2;
+ @Mock
+ Cache.StoredFastPairItem mStoredFastPairItem;
+ Cache.StoredDiscoveryItem mStoredDiscoveryItem = Cache.StoredDiscoveryItem.newBuilder()
+ .setTriggerId(MODEL_ID)
+ .setAppName(APP_NAME).build();
+ Cache.StoredDiscoveryItem mStoredDiscoveryItem2 = Cache.StoredDiscoveryItem.newBuilder()
+ .setTriggerId(MODEL_ID2)
+ .setAppName(APP_NAME).build();
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void notSaveRetrieveInfo() {
+ Context mContext = ApplicationProvider.getApplicationContext();
+ when(mDiscoveryItem.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem);
+ when(mDiscoveryItem.getTriggerId()).thenReturn(MODEL_ID);
+
+ FastPairCacheManager fastPairCacheManager = new FastPairCacheManager(mContext);
+
+ assertThat(fastPairCacheManager.getStoredDiscoveryItem(MODEL_ID).getAppName())
+ .isNotEqualTo(APP_NAME);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void saveRetrieveInfo() {
+ Context mContext = ApplicationProvider.getApplicationContext();
+ when(mDiscoveryItem.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem);
+ when(mDiscoveryItem.getTriggerId()).thenReturn(MODEL_ID);
+
+ FastPairCacheManager fastPairCacheManager = new FastPairCacheManager(mContext);
+ fastPairCacheManager.saveDiscoveryItem(mDiscoveryItem);
+ assertThat(fastPairCacheManager.getStoredDiscoveryItem(MODEL_ID).getAppName())
+ .isEqualTo(APP_NAME);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void getAllInfo() {
+ Context mContext = ApplicationProvider.getApplicationContext();
+ when(mDiscoveryItem.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem);
+ when(mDiscoveryItem.getTriggerId()).thenReturn(MODEL_ID);
+ when(mDiscoveryItem2.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem2);
+ when(mDiscoveryItem2.getTriggerId()).thenReturn(MODEL_ID2);
+
+ FastPairCacheManager fastPairCacheManager = new FastPairCacheManager(mContext);
+ fastPairCacheManager.saveDiscoveryItem(mDiscoveryItem);
+
+ assertThat(fastPairCacheManager.getAllSavedStoreDiscoveryItem()).hasSize(2);
+
+ fastPairCacheManager.saveDiscoveryItem(mDiscoveryItem2);
+
+ assertThat(fastPairCacheManager.getAllSavedStoreDiscoveryItem()).hasSize(3);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void saveRetrieveInfoStoredFastPairItem() {
+ Context mContext = ApplicationProvider.getApplicationContext();
+ Cache.StoredFastPairItem storedFastPairItem = Cache.StoredFastPairItem.newBuilder()
+ .setMacAddress(MAC_ADDRESS)
+ .setAccountKey(ACCOUNT_KEY)
+ .build();
+
+
+ FastPairCacheManager fastPairCacheManager = new FastPairCacheManager(mContext);
+ fastPairCacheManager.putStoredFastPairItem(storedFastPairItem);
+
+ assertThat(fastPairCacheManager.getStoredFastPairItemFromMacAddress(
+ MAC_ADDRESS).getAccountKey())
+ .isEqualTo(ACCOUNT_KEY);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void checkGetAllFastPairItems() {
+ Context mContext = ApplicationProvider.getApplicationContext();
+ Cache.StoredFastPairItem storedFastPairItem = Cache.StoredFastPairItem.newBuilder()
+ .setMacAddress(MAC_ADDRESS)
+ .setAccountKey(ACCOUNT_KEY)
+ .build();
+ Cache.StoredFastPairItem storedFastPairItemB = Cache.StoredFastPairItem.newBuilder()
+ .setMacAddress(MAC_ADDRESS_B)
+ .setAccountKey(ACCOUNT_KEY_B)
+ .build();
+
+ FastPairCacheManager fastPairCacheManager = new FastPairCacheManager(mContext);
+ fastPairCacheManager.putStoredFastPairItem(storedFastPairItem);
+ fastPairCacheManager.putStoredFastPairItem(storedFastPairItemB);
+
+ assertThat(fastPairCacheManager.getAllSavedStoredFastPairItem().size())
+ .isEqualTo(2);
+
+ fastPairCacheManager.removeStoredFastPairItem(MAC_ADDRESS_B);
+
+ assertThat(fastPairCacheManager.getAllSavedStoredFastPairItem().size())
+ .isEqualTo(1);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
new file mode 100644
index 0000000..58e4c47
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.halfsheet;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.FastPairController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import service.proto.Cache;
+
+public class FastPairHalfSheetManagerTest {
+ private static final String BLEADDRESS = "11:22:44:66";
+ private static final String NAME = "device_name";
+ private FastPairHalfSheetManager mFastPairHalfSheetManager;
+ private Cache.ScanFastPairStoreItem mScanFastPairStoreItem;
+ @Mock
+ LocatorContextWrapper mContextWrapper;
+ @Mock
+ ResolveInfo mResolveInfo;
+ @Mock
+ PackageManager mPackageManager;
+ @Mock
+ Locator mLocator;
+ @Mock
+ FastPairController mFastPairController;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mScanFastPairStoreItem = Cache.ScanFastPairStoreItem.newBuilder()
+ .setAddress(BLEADDRESS)
+ .setDeviceName(NAME)
+ .build();
+ }
+
+ @Test
+ public void verifyFastPairHalfSheetManagerBehavior() {
+ mLocator.overrideBindingForTest(FastPairController.class, mFastPairController);
+ ResolveInfo resolveInfo = new ResolveInfo();
+ List<ResolveInfo> resolveInfoList = new ArrayList<>();
+
+ mPackageManager = mock(PackageManager.class);
+ when(mContextWrapper.getPackageManager()).thenReturn(mPackageManager);
+ resolveInfo.activityInfo = new ActivityInfo();
+ ApplicationInfo applicationInfo = new ApplicationInfo();
+ applicationInfo.sourceDir = "/apex/com.android.tethering";
+ applicationInfo.packageName = "test.package";
+ resolveInfo.activityInfo.applicationInfo = applicationInfo;
+ resolveInfoList.add(resolveInfo);
+ when(mPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(resolveInfoList);
+ when(mPackageManager.canRequestPackageInstalls()).thenReturn(false);
+
+ mFastPairHalfSheetManager =
+ new FastPairHalfSheetManager(mContextWrapper);
+
+ when(mContextWrapper.getLocator()).thenReturn(mLocator);
+
+ ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+
+ verify(mContextWrapper, atLeastOnce())
+ .startActivityAsUser(intentArgumentCaptor.capture(), eq(UserHandle.CURRENT));
+ }
+
+ @Test
+ public void verifyFastPairHalfSheetManagerHalfSheetApkNotValidBehavior() {
+ mLocator.overrideBindingForTest(FastPairController.class, mFastPairController);
+ ResolveInfo resolveInfo = new ResolveInfo();
+ List<ResolveInfo> resolveInfoList = new ArrayList<>();
+
+ mPackageManager = mock(PackageManager.class);
+ when(mContextWrapper.getPackageManager()).thenReturn(mPackageManager);
+ resolveInfo.activityInfo = new ActivityInfo();
+ ApplicationInfo applicationInfo = new ApplicationInfo();
+ // application directory is wrong
+ applicationInfo.sourceDir = "/apex/com.android.nearby";
+ applicationInfo.packageName = "test.package";
+ resolveInfo.activityInfo.applicationInfo = applicationInfo;
+ resolveInfoList.add(resolveInfo);
+ when(mPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(resolveInfoList);
+ when(mPackageManager.canRequestPackageInstalls()).thenReturn(false);
+
+ mFastPairHalfSheetManager =
+ new FastPairHalfSheetManager(mContextWrapper);
+
+ when(mContextWrapper.getLocator()).thenReturn(mLocator);
+
+ ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+
+ verify(mContextWrapper, never())
+ .startActivityAsUser(intentArgumentCaptor.capture(), eq(UserHandle.CURRENT));
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/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..c406e47
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java
@@ -0,0 +1,54 @@
+/*
+ * 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.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();
+ }
+
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/MockingLocator.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/MockingLocator.java
new file mode 100644
index 0000000..b261b26
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/MockingLocator.java
@@ -0,0 +1,53 @@
+/*
+ * 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 src.com.android.server.nearby.fastpair.testing;
+
+import android.content.Context;
+
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+
+/** A locator for tests that, by default, installs mocks for everything that's requested of it. */
+public class MockingLocator extends Locator {
+ private final LocatorContextWrapper mLocatorContextWrapper;
+
+ /**
+ * Creates a MockingLocator with the explicit bindings already configured on the given locator.
+ */
+ public static MockingLocator withBindings(Context context, Locator locator) {
+ Locator mockingLocator = new Locator(context);
+ mockingLocator.bind(new MockingModule());
+ locator.attachParent(mockingLocator);
+ return new MockingLocator(context, locator);
+ }
+
+ /** Creates a MockingLocator with no explicit bindings. */
+ public static MockingLocator withMocksOnly(Context context) {
+ return withBindings(context, new Locator(context));
+ }
+
+ @SuppressWarnings("nullness") // due to passing in this before initialized.
+ private MockingLocator(Context context, Locator locator) {
+ super(context, locator);
+ this.mLocatorContextWrapper = new LocatorContextWrapper(context, this);
+ }
+
+ /** Returns a LocatorContextWrapper with this Locator attached. */
+ public LocatorContextWrapper getContextForTest() {
+ return mLocatorContextWrapper;
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/MockingModule.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/MockingModule.java
new file mode 100644
index 0000000..7938c55
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/MockingModule.java
@@ -0,0 +1,39 @@
+/*
+ * 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 src.com.android.server.nearby.fastpair.testing;
+
+import android.content.Context;
+
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.common.locator.Module;
+
+
+import org.mockito.Mockito;
+
+/** Module for tests that just provides mocks for anything that's requested of it. */
+public class MockingModule extends Module {
+
+ @Override
+ public void configure(Context context, Class<?> type, Locator locator) {
+ configureMock(type, locator);
+ }
+
+ private <T> void configureMock(Class<T> type, Locator locator) {
+ T mock = Mockito.mock(type);
+ locator.bind(type, mock);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/metrics/NearbyMetricsTest.java b/nearby/tests/unit/src/com/android/server/nearby/metrics/NearbyMetricsTest.java
new file mode 100644
index 0000000..91962ce
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/metrics/NearbyMetricsTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.metrics;
+
+import static android.nearby.ScanRequest.SCAN_MODE_BALANCED;
+import static android.nearby.ScanRequest.SCAN_TYPE_FAST_PAIR;
+
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PublicCredential;
+import android.nearby.ScanRequest;
+import android.os.WorkSource;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.server.nearby.proto.NearbyStatsLog;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+public class NearbyMetricsTest {
+ private static final int SESSION_ID = 11111;
+ private static final int WORK_SOURCE_UID = 2222;
+
+ private static final String DEVICE_NAME = "testDevice";
+ private static final int SCAN_MEDIUM = 1;
+ private static final int RSSI = -60;
+ private static final String FAST_PAIR_MODEL_ID = "1234";
+ private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
+ private static final byte[] SCAN_DATA = new byte[] {1, 2, 3, 4};
+ private static final PublicCredential PUBLIC_CREDENTIAL =
+ new PublicCredential.Builder(
+ new byte[] {1},
+ new byte[] {2},
+ new byte[] {3},
+ new byte[] {4},
+ new byte[] {5})
+ .build();
+
+ private final WorkSource mWorkSource = new WorkSource(WORK_SOURCE_UID);
+ private final WorkSource mEmptyWorkSource = new WorkSource();
+
+ private final ScanRequest.Builder mScanRequestBuilder =
+ new ScanRequest.Builder()
+ .setScanMode(SCAN_MODE_BALANCED)
+ .setScanType(SCAN_TYPE_FAST_PAIR);
+ private final ScanRequest mScanRequest = mScanRequestBuilder.setWorkSource(mWorkSource).build();
+ private final ScanRequest mScanRequestWithEmptyWorkSource =
+ mScanRequestBuilder.setWorkSource(mEmptyWorkSource).build();
+
+ private final NearbyDeviceParcelable mNearbyDevice =
+ new NearbyDeviceParcelable.Builder()
+ .setName(DEVICE_NAME)
+ .setMedium(SCAN_MEDIUM)
+ .setTxPower(1)
+ .setRssi(RSSI)
+ .setAction(1)
+ .setPublicCredential(PUBLIC_CREDENTIAL)
+ .setFastPairModelId(FAST_PAIR_MODEL_ID)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setData(SCAN_DATA)
+ .build();
+
+ private MockitoSession mSession;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mSession =
+ ExtendedMockito.mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .mockStatic(NearbyStatsLog.class)
+ .startMocking();
+ }
+
+ @After
+ public void tearDown() {
+ mSession.finishMocking();
+ }
+
+ @Test
+ public void testLogScanStart() {
+ NearbyMetrics.logScanStarted(SESSION_ID, mScanRequest);
+ ExtendedMockito.verify(() -> NearbyStatsLog.write(
+ NearbyStatsLog.NEARBY_DEVICE_SCAN_STATE_CHANGED,
+ WORK_SOURCE_UID,
+ SESSION_ID,
+ NearbyStatsLog
+ .NEARBY_DEVICE_SCAN_STATE_CHANGED__SCAN_STATE__NEARBY_SCAN_STATE_STARTED,
+ SCAN_TYPE_FAST_PAIR,
+ 0,
+ 0,
+ "",
+ ""));
+ }
+
+ @Test
+ public void testLogScanStart_emptyWorkSource() {
+ NearbyMetrics.logScanStarted(SESSION_ID, mScanRequestWithEmptyWorkSource);
+ ExtendedMockito.verify(() -> NearbyStatsLog.write(
+ NearbyStatsLog.NEARBY_DEVICE_SCAN_STATE_CHANGED,
+ -1,
+ SESSION_ID,
+ NearbyStatsLog
+ .NEARBY_DEVICE_SCAN_STATE_CHANGED__SCAN_STATE__NEARBY_SCAN_STATE_STARTED,
+ SCAN_TYPE_FAST_PAIR,
+ 0,
+ 0,
+ "",
+ ""));
+ }
+
+ @Test
+ public void testLogScanStopped() {
+ NearbyMetrics.logScanStopped(SESSION_ID, mScanRequest);
+ ExtendedMockito.verify(() -> NearbyStatsLog.write(
+ NearbyStatsLog.NEARBY_DEVICE_SCAN_STATE_CHANGED,
+ WORK_SOURCE_UID,
+ SESSION_ID,
+ NearbyStatsLog
+ .NEARBY_DEVICE_SCAN_STATE_CHANGED__SCAN_STATE__NEARBY_SCAN_STATE_STOPPED,
+ SCAN_TYPE_FAST_PAIR,
+ 0,
+ 0,
+ "",
+ ""));
+ }
+
+ @Test
+ public void testLogScanStopped_emptyWorkSource() {
+ NearbyMetrics.logScanStopped(SESSION_ID, mScanRequestWithEmptyWorkSource);
+ ExtendedMockito.verify(() -> NearbyStatsLog.write(
+ NearbyStatsLog.NEARBY_DEVICE_SCAN_STATE_CHANGED,
+ -1,
+ SESSION_ID,
+ NearbyStatsLog
+ .NEARBY_DEVICE_SCAN_STATE_CHANGED__SCAN_STATE__NEARBY_SCAN_STATE_STOPPED,
+ SCAN_TYPE_FAST_PAIR,
+ 0,
+ 0,
+ "",
+ ""));
+ }
+
+ @Test
+ public void testLogScanDeviceDiscovered() {
+ NearbyMetrics.logScanDeviceDiscovered(SESSION_ID, mScanRequest, mNearbyDevice);
+ ExtendedMockito.verify(() -> NearbyStatsLog.write(
+ NearbyStatsLog.NEARBY_DEVICE_SCAN_STATE_CHANGED,
+ WORK_SOURCE_UID,
+ SESSION_ID,
+ NearbyStatsLog
+ .NEARBY_DEVICE_SCAN_STATE_CHANGED__SCAN_STATE__NEARBY_SCAN_STATE_DISCOVERED,
+ SCAN_TYPE_FAST_PAIR,
+ SCAN_MEDIUM,
+ RSSI,
+ FAST_PAIR_MODEL_ID,
+ ""));
+ }
+
+ @Test
+ public void testLogScanDeviceDiscovered_emptyWorkSource() {
+ NearbyMetrics.logScanDeviceDiscovered(
+ SESSION_ID, mScanRequestWithEmptyWorkSource, mNearbyDevice);
+ ExtendedMockito.verify(() -> NearbyStatsLog.write(
+ NearbyStatsLog.NEARBY_DEVICE_SCAN_STATE_CHANGED,
+ -1,
+ SESSION_ID,
+ NearbyStatsLog
+ .NEARBY_DEVICE_SCAN_STATE_CHANGED__SCAN_STATE__NEARBY_SCAN_STATE_DISCOVERED,
+ SCAN_TYPE_FAST_PAIR,
+ SCAN_MEDIUM,
+ RSSI,
+ FAST_PAIR_MODEL_ID,
+ ""));
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
new file mode 100644
index 0000000..5e0ccbe
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.BroadcastRequest;
+import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PresenceCredential;
+import android.nearby.PrivateCredential;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collections;
+
+/**
+ * Unit test for {@link FastAdvertisement}.
+ */
+public class FastAdvertisementTest {
+
+ private static final int IDENTITY_TYPE = PresenceCredential.IDENTITY_TYPE_PRIVATE;
+ private static final byte[] IDENTITY = new byte[]{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
+ private static final int MEDIUM_TYPE_BLE = 0;
+ private static final byte[] SALT = {2, 3};
+ private static final byte TX_POWER = 4;
+ private static final int PRESENCE_ACTION = 123;
+ private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] AUTHENTICITY_KEY = new byte[]{12, 13, 14};
+ private static final byte[] EXPECTED_ADV_BYTES =
+ new byte[]{2, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 123};
+ private static final String DEVICE_NAME = "test_device";
+
+ private PresenceBroadcastRequest.Builder mBuilder;
+ private PrivateCredential mCredential;
+
+ @Before
+ public void setUp() {
+ mCredential =
+ new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, IDENTITY, DEVICE_NAME)
+ .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+ .build();
+ mBuilder =
+ new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+ SALT, mCredential)
+ .setTxPower(TX_POWER)
+ .setVersion(BroadcastRequest.PRESENCE_VERSION_V0)
+ .addAction(PRESENCE_ACTION);
+ }
+
+ @Test
+ public void testFastAdvertisementCreateFromRequest() {
+ FastAdvertisement originalAdvertisement = FastAdvertisement.createFromRequest(
+ mBuilder.build());
+
+ assertThat(originalAdvertisement.getActions()).containsExactly(PRESENCE_ACTION);
+ assertThat(originalAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+ assertThat(originalAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+ assertThat(originalAdvertisement.getLtvFieldCount()).isEqualTo(4);
+ assertThat(originalAdvertisement.getLength()).isEqualTo(19);
+ assertThat(originalAdvertisement.getVersion()).isEqualTo(
+ BroadcastRequest.PRESENCE_VERSION_V0);
+ assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT);
+ }
+
+ @Test
+ public void testFastAdvertisementSerialization() {
+ FastAdvertisement originalAdvertisement = FastAdvertisement.createFromRequest(
+ mBuilder.build());
+ byte[] bytes = originalAdvertisement.toBytes();
+
+ assertThat(bytes).hasLength(originalAdvertisement.getLength());
+ assertThat(bytes).isEqualTo(EXPECTED_ADV_BYTES);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java
new file mode 100644
index 0000000..39cab94
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.PresenceCredential;
+import android.nearby.PresenceDevice;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+/**
+ * Unit tests for {@link PresenceDiscoveryResult}.
+ */
+public class PresenceDiscoveryResultTest {
+ private static final int PRESENCE_ACTION = 123;
+ private static final int TX_POWER = -1;
+ private static final int RSSI = -41;
+ private static final byte[] SALT = new byte[]{12, 34};
+ private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] AUTHENTICITY_KEY = new byte[]{12, 13, 14};
+ private static final byte[] PUBLIC_KEY = new byte[]{1, 1, 2, 2};
+ private static final byte[] ENCRYPTED_METADATA = new byte[]{1, 2, 3, 4, 5};
+ private static final byte[] METADATA_ENCRYPTION_KEY_TAG = new byte[]{1, 1, 3, 4, 5};
+
+ private PresenceDiscoveryResult.Builder mBuilder;
+ private PublicCredential mCredential;
+
+ @Before
+ public void setUp() {
+ mCredential =
+ new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
+ ENCRYPTED_METADATA, METADATA_ENCRYPTION_KEY_TAG)
+ .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+ .build();
+ mBuilder = new PresenceDiscoveryResult.Builder()
+ .setPublicCredential(mCredential)
+ .setSalt(SALT)
+ .setTxPower(TX_POWER)
+ .setRssi(RSSI)
+ .addPresenceAction(PRESENCE_ACTION);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testToDevice() {
+ PresenceDiscoveryResult discoveryResult = mBuilder.build();
+ PresenceDevice presenceDevice = discoveryResult.toPresenceDevice();
+
+ assertThat(presenceDevice.getRssi()).isEqualTo(RSSI);
+ assertThat(Arrays.equals(presenceDevice.getSalt(), SALT)).isTrue();
+ assertThat(Arrays.equals(presenceDevice.getSecretId(), SECRET_ID)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testMatches() {
+ PresenceScanFilter scanFilter = new PresenceScanFilter.Builder()
+ .setMaxPathLoss(80)
+ .addPresenceAction(PRESENCE_ACTION)
+ .addCredential(mCredential)
+ .build();
+
+ PresenceDiscoveryResult discoveryResult = mBuilder.build();
+ assertThat(discoveryResult.matches(scanFilter)).isTrue();
+ }
+
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
new file mode 100644
index 0000000..d32e325
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+
+
+
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+
+public class PresenceManagerTest {
+ private PresenceManager mPresenceManager;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testInit() {}
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
new file mode 100644
index 0000000..f485e18
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.le.AdvertiseSettings;
+import android.content.Context;
+import android.nearby.BroadcastCallback;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.nearby.injector.ContextHubManagerAdapter;
+import com.android.server.nearby.injector.Injector;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/**
+ * Unit test for {@link BleBroadcastProvider}.
+ */
+public class BleBroadcastProviderTest {
+ @Rule
+ public final MockitoRule mocks = MockitoJUnit.rule();
+
+ @Mock
+ private BleBroadcastProvider.BroadcastListener mBroadcastListener;
+ private BleBroadcastProvider mBleBroadcastProvider;
+
+ @Before
+ public void setUp() {
+ mBleBroadcastProvider = new BleBroadcastProvider(new TestInjector(),
+ MoreExecutors.directExecutor());
+ }
+
+ @Test
+ public void testOnStatus_success() {
+ byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
+ mBleBroadcastProvider.start(advertiseBytes, mBroadcastListener);
+ verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
+
+ AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
+ mBleBroadcastProvider.onStartSuccess(settings);
+ verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+ }
+
+ @Test
+ public void testOnStatus_failure() {
+ byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
+ mBleBroadcastProvider.start(advertiseBytes, mBroadcastListener);
+
+ mBleBroadcastProvider.onStartFailure(BroadcastCallback.STATUS_FAILURE);
+ verify(mBroadcastListener, times(2)).onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
+ }
+
+ private static class TestInjector implements Injector {
+
+ @Override
+ public BluetoothAdapter getBluetoothAdapter() {
+ Context context = ApplicationProvider.getApplicationContext();
+ BluetoothManager bluetoothManager = context.getSystemService(BluetoothManager.class);
+ return bluetoothManager.getAdapter();
+ }
+
+ @Override
+ public ContextHubManagerAdapter getContextHubManagerAdapter() {
+ return null;
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
new file mode 100644
index 0000000..8e97443
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.nearby.injector.ContextHubManagerAdapter;
+import com.android.server.nearby.injector.Injector;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public final class BleDiscoveryProviderTest {
+
+ private BluetoothAdapter mBluetoothAdapter;
+ private BleDiscoveryProvider mBleDiscoveryProvider;
+ @Mock
+ private AbstractDiscoveryProvider.Listener mListener;
+
+ @Before
+ public void setup() {
+ initMocks(this);
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ Injector injector = new TestInjector();
+
+ mBluetoothAdapter = context.getSystemService(BluetoothManager.class).getAdapter();
+ mBleDiscoveryProvider = new BleDiscoveryProvider(context, injector);
+ }
+
+ @Test
+ public void test_callback() throws InterruptedException {
+ mBleDiscoveryProvider.getController().setListener(mListener);
+ mBleDiscoveryProvider.onStart();
+ mBleDiscoveryProvider.getScanCallback()
+ .onScanResult(CALLBACK_TYPE_ALL_MATCHES, createScanResult());
+
+ // Wait for callback to be invoked
+ Thread.sleep(500);
+ verify(mListener, times(1)).onNearbyDeviceDiscovered(any());
+ }
+
+ @Test
+ public void test_stopScan() {
+ mBleDiscoveryProvider.onStart();
+ mBleDiscoveryProvider.onStop();
+ }
+
+ private class TestInjector implements Injector {
+ @Override
+ public BluetoothAdapter getBluetoothAdapter() {
+ return mBluetoothAdapter;
+ }
+
+ @Override
+ public ContextHubManagerAdapter getContextHubManagerAdapter() {
+ return null;
+ }
+ }
+
+ private ScanResult createScanResult() {
+ BluetoothDevice bluetoothDevice = mBluetoothAdapter
+ .getRemoteDevice("11:22:33:44:55:66");
+ byte[] scanRecord = new byte[] {2, 1, 6, 6, 22, 44, -2, 113, -116, 23, 2, 10, -11, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+ return new ScanResult(
+ bluetoothDevice,
+ /* eventType= */ 0,
+ /* primaryPhy= */ 0,
+ /* secondaryPhy= */ 0,
+ /* advertisingSid= */ 0,
+ -31,
+ -50,
+ /* periodicAdvertisingInterval= */ 0,
+ parseScanRecord(scanRecord),
+ 1645579363003L);
+ }
+
+ private static ScanRecord parseScanRecord(byte[] bytes) {
+ Class<?> scanRecordClass = ScanRecord.class;
+ try {
+ Method method = scanRecordClass
+ .getDeclaredMethod("parseFromBytes", byte[].class);
+ return (ScanRecord) method.invoke(null, bytes);
+ } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException
+ | InvocationTargetException e) {
+ return null;
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
new file mode 100644
index 0000000..d45d570
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.nearby.BroadcastCallback;
+import android.nearby.BroadcastRequest;
+import android.nearby.IBroadcastListener;
+import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PresenceCredential;
+import android.nearby.PrivateCredential;
+import android.provider.DeviceConfig;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.Collections;
+
+/**
+ * Unit test for {@link BroadcastProviderManager}.
+ */
+public class BroadcastProviderManagerTest {
+ private static final byte[] IDENTITY = new byte[]{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
+ private static final int MEDIUM_TYPE_BLE = 0;
+ private static final byte[] SALT = {2, 3};
+ private static final byte TX_POWER = 4;
+ private static final int PRESENCE_ACTION = 123;
+ private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] AUTHENTICITY_KEY = new byte[]{12, 13, 14};
+ private static final String DEVICE_NAME = "test_device";
+
+ @Rule
+ public final MockitoRule mocks = MockitoJUnit.rule();
+
+ @Mock
+ IBroadcastListener mBroadcastListener;
+ @Mock
+ BleBroadcastProvider mBleBroadcastProvider;
+ private Context mContext;
+ private BroadcastProviderManager mBroadcastProviderManager;
+ private BroadcastRequest mBroadcastRequest;
+ private UiAutomation mUiAutomation =
+ InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+ @Before
+ public void setUp() {
+ mUiAutomation.adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
+ DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+ "true", false);
+
+ mContext = ApplicationProvider.getApplicationContext();
+ mBroadcastProviderManager = new BroadcastProviderManager(MoreExecutors.directExecutor(),
+ mBleBroadcastProvider);
+
+ PrivateCredential privateCredential =
+ new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, IDENTITY, DEVICE_NAME)
+ .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+ .build();
+ mBroadcastRequest =
+ new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+ SALT, privateCredential)
+ .setTxPower(TX_POWER)
+ .setVersion(BroadcastRequest.PRESENCE_VERSION_V0)
+ .addAction(PRESENCE_ACTION).build();
+ }
+
+ @Test
+ public void testStartAdvertising() {
+ mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
+ verify(mBleBroadcastProvider).start(any(byte[].class), any(
+ BleBroadcastProvider.BroadcastListener.class));
+ }
+
+ @Test
+ public void testStartAdvertising_featureDisabled() throws Exception {
+ DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+ "false", false);
+ mBroadcastProviderManager = new BroadcastProviderManager(MoreExecutors.directExecutor(),
+ mBleBroadcastProvider);
+ mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
+ verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
+ }
+
+ @Test
+ public void testOnStatusChanged() throws Exception {
+ mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
+ mBroadcastProviderManager.onStatusChanged(BroadcastCallback.STATUS_OK);
+ verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
new file mode 100644
index 0000000..1b29b52
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.location.ContextHubClient;
+import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubTransaction;
+import android.hardware.location.NanoAppMessage;
+import android.hardware.location.NanoAppState;
+
+import com.android.server.nearby.injector.ContextHubManagerAdapter;
+import com.android.server.nearby.injector.Injector;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+public class ChreCommunicationTest {
+ @Mock Injector mInjector;
+ @Mock ContextHubManagerAdapter mManager;
+ @Mock ContextHubTransaction<List<NanoAppState>> mTransaction;
+ @Mock ContextHubTransaction.Response<List<NanoAppState>> mTransactionResponse;
+ @Mock ContextHubClient mClient;
+ @Mock ChreCommunication.ContextHubCommsCallback mChreCallback;
+
+ @Captor
+ ArgumentCaptor<ChreCommunication.OnQueryCompleteListener> mOnQueryCompleteListenerCaptor;
+
+ private ChreCommunication mChreCommunication;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mInjector.getContextHubManagerAdapter()).thenReturn(mManager);
+ when(mManager.getContextHubs()).thenReturn(Collections.singletonList(new ContextHubInfo()));
+ when(mManager.queryNanoApps(any())).thenReturn(mTransaction);
+ when(mManager.createClient(any(), any(), any())).thenReturn(mClient);
+ when(mTransactionResponse.getResult()).thenReturn(ContextHubTransaction.RESULT_SUCCESS);
+ when(mTransactionResponse.getContents())
+ .thenReturn(
+ Collections.singletonList(
+ new NanoAppState(ChreDiscoveryProvider.NANOAPP_ID, 1, true)));
+
+ mChreCommunication = new ChreCommunication(mInjector, new InlineExecutor());
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
+
+ verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+ mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
+ }
+
+ @Test
+ public void testStart() {
+ verify(mChreCallback).started(true);
+ }
+
+ @Test
+ public void testStop() {
+ mChreCommunication.stop();
+ verify(mClient).close();
+ }
+
+ @Test
+ public void testSendMessageToNanApp() {
+ NanoAppMessage message =
+ NanoAppMessage.createMessageToNanoApp(
+ ChreDiscoveryProvider.NANOAPP_ID,
+ ChreDiscoveryProvider.NANOAPP_MESSAGE_TYPE_FILTER,
+ new byte[] {1, 2, 3});
+ mChreCommunication.sendMessageToNanoApp(message);
+ verify(mClient).sendMessageToNanoApp(eq(message));
+ }
+
+ @Test
+ public void testOnMessageFromNanoApp() {
+ NanoAppMessage message =
+ NanoAppMessage.createMessageToNanoApp(
+ ChreDiscoveryProvider.NANOAPP_ID,
+ ChreDiscoveryProvider.NANOAPP_MESSAGE_TYPE_FILTER_RESULT,
+ new byte[] {1, 2, 3});
+ mChreCommunication.onMessageFromNanoApp(mClient, message);
+ verify(mChreCallback).onMessageFromNanoApp(eq(message));
+ }
+
+ @Test
+ public void testOnHubReset() {
+ mChreCommunication.onHubReset(mClient);
+ verify(mChreCallback).onHubReset();
+ }
+
+ @Test
+ public void testOnNanoAppLoaded() {
+ mChreCommunication.onNanoAppLoaded(mClient, ChreDiscoveryProvider.NANOAPP_ID);
+ verify(mChreCallback).onNanoAppRestart(eq(ChreDiscoveryProvider.NANOAPP_ID));
+ }
+
+ private static class InlineExecutor implements Executor {
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
new file mode 100644
index 0000000..7c0dd92
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.hardware.location.NanoAppMessage;
+import android.nearby.ScanFilter;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.google.protobuf.ByteString;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import service.proto.Blefilter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+public class ChreDiscoveryProviderTest {
+ @Mock AbstractDiscoveryProvider.Listener mListener;
+ @Mock ChreCommunication mChreCommunication;
+
+ @Captor ArgumentCaptor<ChreCommunication.ContextHubCommsCallback> mChreCallbackCaptor;
+
+ private ChreDiscoveryProvider mChreDiscoveryProvider;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ mChreDiscoveryProvider =
+ new ChreDiscoveryProvider(context, mChreCommunication, new InLineExecutor());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testOnStart() {
+ List<ScanFilter> scanFilters = new ArrayList<>();
+ mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
+ mChreDiscoveryProvider.onStart();
+ verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
+ mChreCallbackCaptor.getValue().started(true);
+ verify(mChreCommunication).sendMessageToNanoApp(any());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testOnNearbyDeviceDiscovered() {
+ Blefilter.PublicCredential credential =
+ Blefilter.PublicCredential.newBuilder()
+ .setSecretId(ByteString.copyFrom(new byte[] {1}))
+ .setAuthenticityKey(ByteString.copyFrom(new byte[2]))
+ .setPublicKey(ByteString.copyFrom(new byte[3]))
+ .setEncryptedMetadata(ByteString.copyFrom(new byte[4]))
+ .setEncryptedMetadataTag(ByteString.copyFrom(new byte[5]))
+ .build();
+ Blefilter.BleFilterResult result =
+ Blefilter.BleFilterResult.newBuilder()
+ .setTxPower(2)
+ .setRssi(1)
+ .setPublicCredential(credential)
+ .build();
+ Blefilter.BleFilterResults results =
+ Blefilter.BleFilterResults.newBuilder().addResult(result).build();
+ NanoAppMessage chre_message =
+ NanoAppMessage.createMessageToNanoApp(
+ ChreDiscoveryProvider.NANOAPP_ID,
+ ChreDiscoveryProvider.NANOAPP_MESSAGE_TYPE_FILTER_RESULT,
+ results.toByteArray());
+ mChreDiscoveryProvider.getController().setListener(mListener);
+ mChreDiscoveryProvider.onStart();
+ verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
+ mChreCallbackCaptor.getValue().onMessageFromNanoApp(chre_message);
+ verify(mListener).onNearbyDeviceDiscovered(any());
+ }
+
+ private static class InLineExecutor implements Executor {
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/UtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/UtilsTest.java
new file mode 100644
index 0000000..eeea319
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/UtilsTest.java
@@ -0,0 +1,651 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.accounts.Account;
+import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
+import android.nearby.aidl.FastPairDeviceMetadataParcel;
+import android.nearby.aidl.FastPairDiscoveryItemParcel;
+import android.nearby.aidl.FastPairEligibleAccountParcel;
+
+import androidx.test.filters.SdkSuppress;
+
+import com.android.server.nearby.fastpair.footprint.FastPairUploadInfo;
+
+import com.google.protobuf.ByteString;
+
+import org.junit.Test;
+
+import java.util.List;
+
+import service.proto.Cache;
+import service.proto.Data;
+import service.proto.FastPairString.FastPairStrings;
+import service.proto.Rpcs;
+
+public class UtilsTest {
+
+ private static final String ASSISTANT_SETUP_HALFSHEET = "ASSISTANT_SETUP_HALFSHEET";
+ private static final String ASSISTANT_SETUP_NOTIFICATION = "ASSISTANT_SETUP_NOTIFICATION";
+ private static final int BLE_TX_POWER = 5;
+ private static final String CONFIRM_PIN_DESCRIPTION = "CONFIRM_PIN_DESCRIPTION";
+ private static final String CONFIRM_PIN_TITLE = "CONFIRM_PIN_TITLE";
+ private static final String CONNECT_SUCCESS_COMPANION_APP_INSTALLED =
+ "CONNECT_SUCCESS_COMPANION_APP_INSTALLED";
+ private static final String CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED =
+ "CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED";
+ private static final int DEVICE_TYPE = 1;
+ private static final String DOWNLOAD_COMPANION_APP_DESCRIPTION =
+ "DOWNLOAD_COMPANION_APP_DESCRIPTION";
+ private static final Account ELIGIBLE_ACCOUNT_1 = new Account("abc@google.com", "type1");
+ private static final String FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION =
+ "FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION";
+ private static final String FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION =
+ "FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION";
+ private static final byte[] IMAGE = new byte[]{7, 9};
+ private static final String IMAGE_URL = "IMAGE_URL";
+ private static final String INITIAL_NOTIFICATION_DESCRIPTION =
+ "INITIAL_NOTIFICATION_DESCRIPTION";
+ private static final String INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT =
+ "INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT";
+ private static final String INITIAL_PAIRING_DESCRIPTION = "INITIAL_PAIRING_DESCRIPTION";
+ private static final String INTENT_URI = "INTENT_URI";
+ private static final String LOCALE = "LOCALE";
+ private static final String OPEN_COMPANION_APP_DESCRIPTION = "OPEN_COMPANION_APP_DESCRIPTION";
+ private static final String RETRO_ACTIVE_PAIRING_DESCRIPTION =
+ "RETRO_ACTIVE_PAIRING_DESCRIPTION";
+ private static final String SUBSEQUENT_PAIRING_DESCRIPTION = "SUBSEQUENT_PAIRING_DESCRIPTION";
+ private static final String SYNC_CONTACT_DESCRPTION = "SYNC_CONTACT_DESCRPTION";
+ private static final String SYNC_CONTACTS_TITLE = "SYNC_CONTACTS_TITLE";
+ private static final String SYNC_SMS_DESCRIPTION = "SYNC_SMS_DESCRIPTION";
+ private static final String SYNC_SMS_TITLE = "SYNC_SMS_TITLE";
+ private static final float TRIGGER_DISTANCE = 111;
+ private static final String TRUE_WIRELESS_IMAGE_URL_CASE = "TRUE_WIRELESS_IMAGE_URL_CASE";
+ private static final String TRUE_WIRELESS_IMAGE_URL_LEFT_BUD =
+ "TRUE_WIRELESS_IMAGE_URL_LEFT_BUD";
+ private static final String TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD =
+ "TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD";
+ private static final String UNABLE_TO_CONNECT_DESCRIPTION = "UNABLE_TO_CONNECT_DESCRIPTION";
+ private static final String UNABLE_TO_CONNECT_TITLE = "UNABLE_TO_CONNECT_TITLE";
+ private static final String UPDATE_COMPANION_APP_DESCRIPTION =
+ "UPDATE_COMPANION_APP_DESCRIPTION";
+ private static final String WAIT_LAUNCH_COMPANION_APP_DESCRIPTION =
+ "WAIT_LAUNCH_COMPANION_APP_DESCRIPTION";
+ private static final byte[] ACCOUNT_KEY = new byte[]{3};
+ private static final byte[] SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS = new byte[]{2, 8};
+ private static final byte[] ANTI_SPOOFING_KEY = new byte[]{4, 5, 6};
+ private static final String ACTION_URL = "ACTION_URL";
+ private static final int ACTION_URL_TYPE = 1;
+ private static final String APP_NAME = "APP_NAME";
+ private static final int ATTACHMENT_TYPE = 1;
+ private static final byte[] AUTHENTICATION_PUBLIC_KEY_SEC_P256R1 = new byte[]{5, 7};
+ private static final byte[] BLE_RECORD_BYTES = new byte[]{2, 4};
+ private static final int DEBUG_CATEGORY = 1;
+ private static final String DEBUG_MESSAGE = "DEBUG_MESSAGE";
+ private static final String DESCRIPTION = "DESCRIPTION";
+ private static final String DEVICE_NAME = "DEVICE_NAME";
+ private static final String DISPLAY_URL = "DISPLAY_URL";
+ private static final String ENTITY_ID = "ENTITY_ID";
+ private static final String FEATURE_GRAPHIC_URL = "FEATURE_GRAPHIC_URL";
+ private static final long FIRST_OBSERVATION_TIMESTAMP_MILLIS = 8393L;
+ private static final String GROUP_ID = "GROUP_ID";
+ private static final String ICON_FIFE_URL = "ICON_FIFE_URL";
+ private static final byte[] ICON_PNG = new byte[]{2, 5};
+ private static final String ID = "ID";
+ private static final long LAST_OBSERVATION_TIMESTAMP_MILLIS = 934234L;
+ private static final int LAST_USER_EXPERIENCE = 1;
+ private static final long LOST_MILLIS = 393284L;
+ private static final String MAC_ADDRESS = "MAC_ADDRESS";
+ private static final String NAME = "NAME";
+ private static final String PACKAGE_NAME = "PACKAGE_NAME";
+ private static final long PENDING_APP_INSTALL_TIMESTAMP_MILLIS = 832393L;
+ private static final int RSSI = 9;
+ private static final int STATE = 1;
+ private static final String TITLE = "TITLE";
+ private static final String TRIGGER_ID = "TRIGGER_ID";
+ private static final int TX_POWER = 63;
+ private static final int TYPE = 1;
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testHappyPathConvertToFastPairDevicesWithAccountKey() {
+ FastPairAccountKeyDeviceMetadataParcel[] array = {
+ genHappyPathFastPairAccountkeyDeviceMetadataParcel()};
+
+ List<Data.FastPairDeviceWithAccountKey> deviceWithAccountKey =
+ Utils.convertToFastPairDevicesWithAccountKey(array);
+ assertThat(deviceWithAccountKey.size()).isEqualTo(1);
+ assertThat(deviceWithAccountKey.get(0)).isEqualTo(
+ genHappyPathFastPairDeviceWithAccountKey());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConvertToFastPairDevicesWithAccountKeyWithNullArray() {
+ FastPairAccountKeyDeviceMetadataParcel[] array = null;
+ assertThat(Utils.convertToFastPairDevicesWithAccountKey(array).size()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConvertToFastPairDevicesWithAccountKeyWithNullElement() {
+ FastPairAccountKeyDeviceMetadataParcel[] array = {null};
+ assertThat(Utils.convertToFastPairDevicesWithAccountKey(array).size()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConvertToFastPairDevicesWithAccountKeyWithEmptyElementNoCrash() {
+ FastPairAccountKeyDeviceMetadataParcel[] array = {
+ genEmptyFastPairAccountkeyDeviceMetadataParcel()};
+
+ List<Data.FastPairDeviceWithAccountKey> deviceWithAccountKey =
+ Utils.convertToFastPairDevicesWithAccountKey(array);
+ assertThat(deviceWithAccountKey.size()).isEqualTo(1);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConvertToFastPairDevicesWithAccountKeyWithEmptyMetadataDiscoveryNoCrash() {
+ FastPairAccountKeyDeviceMetadataParcel[] array = {
+ genFastPairAccountkeyDeviceMetadataParcelWithEmptyMetadataDiscoveryItem()};
+
+ List<Data.FastPairDeviceWithAccountKey> deviceWithAccountKey =
+ Utils.convertToFastPairDevicesWithAccountKey(array);
+ assertThat(deviceWithAccountKey.size()).isEqualTo(1);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConvertToFastPairDevicesWithAccountKeyWithMixedArrayElements() {
+ FastPairAccountKeyDeviceMetadataParcel[] array = {
+ null,
+ genHappyPathFastPairAccountkeyDeviceMetadataParcel(),
+ genEmptyFastPairAccountkeyDeviceMetadataParcel(),
+ genFastPairAccountkeyDeviceMetadataParcelWithEmptyMetadataDiscoveryItem()};
+
+ assertThat(Utils.convertToFastPairDevicesWithAccountKey(array).size()).isEqualTo(3);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testHappyPathConvertToAccountList() {
+ FastPairEligibleAccountParcel[] array = {genHappyPathFastPairEligibleAccountParcel()};
+
+ List<Account> accountList = Utils.convertToAccountList(array);
+ assertThat(accountList.size()).isEqualTo(1);
+ assertThat(accountList.get(0)).isEqualTo(ELIGIBLE_ACCOUNT_1);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConvertToAccountListNullArray() {
+ FastPairEligibleAccountParcel[] array = null;
+
+ List<Account> accountList = Utils.convertToAccountList(array);
+ assertThat(accountList.size()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConvertToAccountListWithNullElement() {
+ FastPairEligibleAccountParcel[] array = {null};
+
+ List<Account> accountList = Utils.convertToAccountList(array);
+ assertThat(accountList.size()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConvertToAccountListWithEmptyElementNotCrash() {
+ FastPairEligibleAccountParcel[] array =
+ {genEmptyFastPairEligibleAccountParcel()};
+
+ List<Account> accountList = Utils.convertToAccountList(array);
+ assertThat(accountList.size()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConvertToAccountListWithMixedArrayElements() {
+ FastPairEligibleAccountParcel[] array = {
+ genHappyPathFastPairEligibleAccountParcel(),
+ genEmptyFastPairEligibleAccountParcel(),
+ null,
+ genHappyPathFastPairEligibleAccountParcel()};
+
+ List<Account> accountList = Utils.convertToAccountList(array);
+ assertThat(accountList.size()).isEqualTo(2);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testHappyPathConvertToGetObservedDeviceResponse() {
+ Rpcs.GetObservedDeviceResponse response =
+ Utils.convertToGetObservedDeviceResponse(
+ genHappyPathFastPairAntispoofKeyDeviceMetadataParcel());
+ assertThat(response).isEqualTo(genHappyPathObservedDeviceResponse());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConvertToGetObservedDeviceResponseWithNullInput() {
+ assertThat(Utils.convertToGetObservedDeviceResponse(null))
+ .isEqualTo(null);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConvertToGetObservedDeviceResponseWithEmptyInputNotCrash() {
+ Utils.convertToGetObservedDeviceResponse(
+ genEmptyFastPairAntispoofKeyDeviceMetadataParcel());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConvertToGetObservedDeviceResponseWithEmptyDeviceMetadataNotCrash() {
+ Utils.convertToGetObservedDeviceResponse(
+ genFastPairAntispoofKeyDeviceMetadataParcelWithEmptyDeviceMetadata());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testHappyPathConvertToFastPairAccountKeyDeviceMetadata() {
+ FastPairAccountKeyDeviceMetadataParcel metadataParcel =
+ Utils.convertToFastPairAccountKeyDeviceMetadata(genHappyPathFastPairUploadInfo());
+ ensureHappyPathAsExpected(metadataParcel);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConvertToFastPairAccountKeyDeviceMetadataWithNullInput() {
+ assertThat(Utils.convertToFastPairAccountKeyDeviceMetadata(null)).isEqualTo(null);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConvertToFastPairAccountKeyDeviceMetadataWithEmptyFieldsNotCrash() {
+ Utils.convertToFastPairAccountKeyDeviceMetadata(
+ new FastPairUploadInfo(
+ null /* discoveryItem */,
+ null /* accountKey */,
+ null /* sha256AccountKeyPublicAddress */));
+ }
+
+ private static FastPairUploadInfo genHappyPathFastPairUploadInfo() {
+ return new FastPairUploadInfo(
+ genHappyPathStoredDiscoveryItem(),
+ ByteString.copyFrom(ACCOUNT_KEY),
+ ByteString.copyFrom(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS));
+
+ }
+
+ private static void ensureHappyPathAsExpected(
+ FastPairAccountKeyDeviceMetadataParcel metadataParcel) {
+ assertThat(metadataParcel).isNotNull();
+ assertThat(metadataParcel.deviceAccountKey).isEqualTo(ACCOUNT_KEY);
+ assertThat(metadataParcel.sha256DeviceAccountKeyPublicAddress)
+ .isEqualTo(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS);
+ ensureHappyPathAsExpected(metadataParcel.metadata);
+ ensureHappyPathAsExpected(metadataParcel.discoveryItem);
+ }
+
+ private static void ensureHappyPathAsExpected(FastPairDeviceMetadataParcel metadataParcel) {
+ assertThat(metadataParcel).isNotNull();
+
+ assertThat(metadataParcel.connectSuccessCompanionAppInstalled).isEqualTo(
+ CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
+ assertThat(metadataParcel.connectSuccessCompanionAppNotInstalled).isEqualTo(
+ CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
+
+ assertThat(metadataParcel.deviceType).isEqualTo(DEVICE_TYPE);
+
+ assertThat(metadataParcel.failConnectGoToSettingsDescription).isEqualTo(
+ FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
+
+ assertThat(metadataParcel.initialNotificationDescription).isEqualTo(
+ INITIAL_NOTIFICATION_DESCRIPTION);
+ assertThat(metadataParcel.initialNotificationDescriptionNoAccount).isEqualTo(
+ INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
+ assertThat(metadataParcel.initialPairingDescription).isEqualTo(INITIAL_PAIRING_DESCRIPTION);
+
+
+ assertThat(metadataParcel.retroactivePairingDescription).isEqualTo(
+ RETRO_ACTIVE_PAIRING_DESCRIPTION);
+
+ assertThat(metadataParcel.subsequentPairingDescription).isEqualTo(
+ SUBSEQUENT_PAIRING_DESCRIPTION);
+
+ assertThat(metadataParcel.trueWirelessImageUrlCase).isEqualTo(TRUE_WIRELESS_IMAGE_URL_CASE);
+ assertThat(metadataParcel.trueWirelessImageUrlLeftBud).isEqualTo(
+ TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
+ assertThat(metadataParcel.trueWirelessImageUrlRightBud).isEqualTo(
+ TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
+ assertThat(metadataParcel.waitLaunchCompanionAppDescription).isEqualTo(
+ WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
+
+ /* do we need upload this? */
+ // assertThat(metadataParcel.locale).isEqualTo(LOCALE);
+ // assertThat(metadataParcel.name).isEqualTo(NAME);
+ // assertThat(metadataParcel.downloadCompanionAppDescription).isEqualTo(
+ // DOWNLOAD_COMPANION_APP_DESCRIPTION);
+ // assertThat(metadataParcel.openCompanionAppDescription).isEqualTo(
+ // OPEN_COMPANION_APP_DESCRIPTION);
+ // assertThat(metadataParcel.triggerDistance).isWithin(DELTA).of(TRIGGER_DISTANCE);
+ // assertThat(metadataParcel.unableToConnectDescription).isEqualTo(
+ // UNABLE_TO_CONNECT_DESCRIPTION);
+ // assertThat(metadataParcel.unableToConnectTitle).isEqualTo(UNABLE_TO_CONNECT_TITLE);
+ // assertThat(metadataParcel.updateCompanionAppDescription).isEqualTo(
+ // UPDATE_COMPANION_APP_DESCRIPTION);
+
+ // assertThat(metadataParcel.bleTxPower).isEqualTo(BLE_TX_POWER);
+ // assertThat(metadataParcel.image).isEqualTo(IMAGE);
+ // assertThat(metadataParcel.imageUrl).isEqualTo(IMAGE_URL);
+ // assertThat(metadataParcel.intentUri).isEqualTo(INTENT_URI);
+ }
+
+ private static void ensureHappyPathAsExpected(FastPairDiscoveryItemParcel itemParcel) {
+ assertThat(itemParcel.actionUrl).isEqualTo(ACTION_URL);
+ assertThat(itemParcel.actionUrlType).isEqualTo(ACTION_URL_TYPE);
+ assertThat(itemParcel.appName).isEqualTo(APP_NAME);
+ assertThat(itemParcel.authenticationPublicKeySecp256r1)
+ .isEqualTo(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1);
+ assertThat(itemParcel.description).isEqualTo(DESCRIPTION);
+ assertThat(itemParcel.deviceName).isEqualTo(DEVICE_NAME);
+ assertThat(itemParcel.displayUrl).isEqualTo(DISPLAY_URL);
+ assertThat(itemParcel.firstObservationTimestampMillis)
+ .isEqualTo(FIRST_OBSERVATION_TIMESTAMP_MILLIS);
+ assertThat(itemParcel.iconFifeUrl).isEqualTo(ICON_FIFE_URL);
+ assertThat(itemParcel.iconPng).isEqualTo(ICON_PNG);
+ assertThat(itemParcel.id).isEqualTo(ID);
+ assertThat(itemParcel.lastObservationTimestampMillis)
+ .isEqualTo(LAST_OBSERVATION_TIMESTAMP_MILLIS);
+ assertThat(itemParcel.macAddress).isEqualTo(MAC_ADDRESS);
+ assertThat(itemParcel.packageName).isEqualTo(PACKAGE_NAME);
+ assertThat(itemParcel.pendingAppInstallTimestampMillis)
+ .isEqualTo(PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
+ assertThat(itemParcel.rssi).isEqualTo(RSSI);
+ assertThat(itemParcel.state).isEqualTo(STATE);
+ assertThat(itemParcel.title).isEqualTo(TITLE);
+ assertThat(itemParcel.triggerId).isEqualTo(TRIGGER_ID);
+ assertThat(itemParcel.txPower).isEqualTo(TX_POWER);
+ }
+
+ private static FastPairEligibleAccountParcel genHappyPathFastPairEligibleAccountParcel() {
+ FastPairEligibleAccountParcel parcel = new FastPairEligibleAccountParcel();
+ parcel.account = ELIGIBLE_ACCOUNT_1;
+ parcel.optIn = true;
+
+ return parcel;
+ }
+
+ private static FastPairEligibleAccountParcel genEmptyFastPairEligibleAccountParcel() {
+ return new FastPairEligibleAccountParcel();
+ }
+
+ private static FastPairDeviceMetadataParcel genEmptyFastPairDeviceMetadataParcel() {
+ return new FastPairDeviceMetadataParcel();
+ }
+
+ private static FastPairDiscoveryItemParcel genEmptyFastPairDiscoveryItemParcel() {
+ return new FastPairDiscoveryItemParcel();
+ }
+
+ private static FastPairAccountKeyDeviceMetadataParcel
+ genEmptyFastPairAccountkeyDeviceMetadataParcel() {
+ return new FastPairAccountKeyDeviceMetadataParcel();
+ }
+
+ private static FastPairAccountKeyDeviceMetadataParcel
+ genFastPairAccountkeyDeviceMetadataParcelWithEmptyMetadataDiscoveryItem() {
+ FastPairAccountKeyDeviceMetadataParcel parcel =
+ new FastPairAccountKeyDeviceMetadataParcel();
+ parcel.metadata = genEmptyFastPairDeviceMetadataParcel();
+ parcel.discoveryItem = genEmptyFastPairDiscoveryItemParcel();
+
+ return parcel;
+ }
+
+ private static FastPairAccountKeyDeviceMetadataParcel
+ genHappyPathFastPairAccountkeyDeviceMetadataParcel() {
+ FastPairAccountKeyDeviceMetadataParcel parcel =
+ new FastPairAccountKeyDeviceMetadataParcel();
+ parcel.deviceAccountKey = ACCOUNT_KEY;
+ parcel.metadata = genHappyPathFastPairDeviceMetadataParcel();
+ parcel.sha256DeviceAccountKeyPublicAddress = SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS;
+ parcel.discoveryItem = genHappyPathFastPairDiscoveryItemParcel();
+
+ return parcel;
+ }
+
+ private static FastPairDeviceMetadataParcel genHappyPathFastPairDeviceMetadataParcel() {
+ FastPairDeviceMetadataParcel parcel = new FastPairDeviceMetadataParcel();
+
+ parcel.bleTxPower = BLE_TX_POWER;
+ parcel.connectSuccessCompanionAppInstalled = CONNECT_SUCCESS_COMPANION_APP_INSTALLED;
+ parcel.connectSuccessCompanionAppNotInstalled =
+ CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED;
+ parcel.deviceType = DEVICE_TYPE;
+ parcel.downloadCompanionAppDescription = DOWNLOAD_COMPANION_APP_DESCRIPTION;
+ parcel.failConnectGoToSettingsDescription = FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION;
+ parcel.image = IMAGE;
+ parcel.imageUrl = IMAGE_URL;
+ parcel.initialNotificationDescription = INITIAL_NOTIFICATION_DESCRIPTION;
+ parcel.initialNotificationDescriptionNoAccount =
+ INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT;
+ parcel.initialPairingDescription = INITIAL_PAIRING_DESCRIPTION;
+ parcel.intentUri = INTENT_URI;
+ parcel.name = NAME;
+ parcel.openCompanionAppDescription = OPEN_COMPANION_APP_DESCRIPTION;
+ parcel.retroactivePairingDescription = RETRO_ACTIVE_PAIRING_DESCRIPTION;
+ parcel.subsequentPairingDescription = SUBSEQUENT_PAIRING_DESCRIPTION;
+ parcel.triggerDistance = TRIGGER_DISTANCE;
+ parcel.trueWirelessImageUrlCase = TRUE_WIRELESS_IMAGE_URL_CASE;
+ parcel.trueWirelessImageUrlLeftBud = TRUE_WIRELESS_IMAGE_URL_LEFT_BUD;
+ parcel.trueWirelessImageUrlRightBud = TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD;
+ parcel.unableToConnectDescription = UNABLE_TO_CONNECT_DESCRIPTION;
+ parcel.unableToConnectTitle = UNABLE_TO_CONNECT_TITLE;
+ parcel.updateCompanionAppDescription = UPDATE_COMPANION_APP_DESCRIPTION;
+ parcel.waitLaunchCompanionAppDescription = WAIT_LAUNCH_COMPANION_APP_DESCRIPTION;
+
+ return parcel;
+ }
+
+ private static Cache.StoredDiscoveryItem genHappyPathStoredDiscoveryItem() {
+ Cache.StoredDiscoveryItem.Builder storedDiscoveryItemBuilder =
+ Cache.StoredDiscoveryItem.newBuilder();
+ storedDiscoveryItemBuilder.setActionUrl(ACTION_URL);
+ storedDiscoveryItemBuilder.setActionUrlType(Cache.ResolvedUrlType.WEBPAGE);
+ storedDiscoveryItemBuilder.setAppName(APP_NAME);
+ storedDiscoveryItemBuilder.setAuthenticationPublicKeySecp256R1(
+ ByteString.copyFrom(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1));
+ storedDiscoveryItemBuilder.setDescription(DESCRIPTION);
+ storedDiscoveryItemBuilder.setDeviceName(DEVICE_NAME);
+ storedDiscoveryItemBuilder.setDisplayUrl(DISPLAY_URL);
+ storedDiscoveryItemBuilder.setFirstObservationTimestampMillis(
+ FIRST_OBSERVATION_TIMESTAMP_MILLIS);
+ storedDiscoveryItemBuilder.setIconFifeUrl(ICON_FIFE_URL);
+ storedDiscoveryItemBuilder.setIconPng(ByteString.copyFrom(ICON_PNG));
+ storedDiscoveryItemBuilder.setId(ID);
+ storedDiscoveryItemBuilder.setLastObservationTimestampMillis(
+ LAST_OBSERVATION_TIMESTAMP_MILLIS);
+ storedDiscoveryItemBuilder.setMacAddress(MAC_ADDRESS);
+ storedDiscoveryItemBuilder.setPackageName(PACKAGE_NAME);
+ storedDiscoveryItemBuilder.setPendingAppInstallTimestampMillis(
+ PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
+ storedDiscoveryItemBuilder.setRssi(RSSI);
+ storedDiscoveryItemBuilder.setState(Cache.StoredDiscoveryItem.State.STATE_ENABLED);
+ storedDiscoveryItemBuilder.setTitle(TITLE);
+ storedDiscoveryItemBuilder.setTriggerId(TRIGGER_ID);
+ storedDiscoveryItemBuilder.setTxPower(TX_POWER);
+
+ FastPairStrings.Builder stringsBuilder = FastPairStrings.newBuilder();
+ stringsBuilder.setPairingFinishedCompanionAppInstalled(
+ CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
+ stringsBuilder.setPairingFinishedCompanionAppNotInstalled(
+ CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
+ stringsBuilder.setPairingFailDescription(
+ FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
+ stringsBuilder.setTapToPairWithAccount(
+ INITIAL_NOTIFICATION_DESCRIPTION);
+ stringsBuilder.setTapToPairWithoutAccount(
+ INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
+ stringsBuilder.setInitialPairingDescription(INITIAL_PAIRING_DESCRIPTION);
+ stringsBuilder.setRetroactivePairingDescription(RETRO_ACTIVE_PAIRING_DESCRIPTION);
+ stringsBuilder.setSubsequentPairingDescription(SUBSEQUENT_PAIRING_DESCRIPTION);
+ stringsBuilder.setWaitAppLaunchDescription(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
+ storedDiscoveryItemBuilder.setFastPairStrings(stringsBuilder.build());
+
+ Cache.FastPairInformation.Builder fpInformationBuilder =
+ Cache.FastPairInformation.newBuilder();
+ Rpcs.TrueWirelessHeadsetImages.Builder imagesBuilder =
+ Rpcs.TrueWirelessHeadsetImages.newBuilder();
+ imagesBuilder.setCaseUrl(TRUE_WIRELESS_IMAGE_URL_CASE);
+ imagesBuilder.setLeftBudUrl(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
+ imagesBuilder.setRightBudUrl(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
+ fpInformationBuilder.setTrueWirelessImages(imagesBuilder.build());
+ fpInformationBuilder.setDeviceType(Rpcs.DeviceType.HEADPHONES);
+
+ storedDiscoveryItemBuilder.setFastPairInformation(fpInformationBuilder.build());
+ storedDiscoveryItemBuilder.setTxPower(TX_POWER);
+
+ storedDiscoveryItemBuilder.setIconPng(ByteString.copyFrom(ICON_PNG));
+ storedDiscoveryItemBuilder.setIconFifeUrl(ICON_FIFE_URL);
+ storedDiscoveryItemBuilder.setActionUrl(ACTION_URL);
+
+ return storedDiscoveryItemBuilder.build();
+ }
+
+ private static Data.FastPairDeviceWithAccountKey genHappyPathFastPairDeviceWithAccountKey() {
+ Data.FastPairDeviceWithAccountKey.Builder fpDeviceBuilder =
+ Data.FastPairDeviceWithAccountKey.newBuilder();
+ fpDeviceBuilder.setAccountKey(ByteString.copyFrom(ACCOUNT_KEY));
+ fpDeviceBuilder.setSha256AccountKeyPublicAddress(
+ ByteString.copyFrom(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS));
+ fpDeviceBuilder.setDiscoveryItem(genHappyPathStoredDiscoveryItem());
+
+ return fpDeviceBuilder.build();
+ }
+
+ private static FastPairDiscoveryItemParcel genHappyPathFastPairDiscoveryItemParcel() {
+ FastPairDiscoveryItemParcel parcel = new FastPairDiscoveryItemParcel();
+ parcel.actionUrl = ACTION_URL;
+ parcel.actionUrlType = ACTION_URL_TYPE;
+ parcel.appName = APP_NAME;
+ parcel.authenticationPublicKeySecp256r1 = AUTHENTICATION_PUBLIC_KEY_SEC_P256R1;
+ parcel.description = DESCRIPTION;
+ parcel.deviceName = DEVICE_NAME;
+ parcel.displayUrl = DISPLAY_URL;
+ parcel.firstObservationTimestampMillis = FIRST_OBSERVATION_TIMESTAMP_MILLIS;
+ parcel.iconFifeUrl = ICON_FIFE_URL;
+ parcel.iconPng = ICON_PNG;
+ parcel.id = ID;
+ parcel.lastObservationTimestampMillis = LAST_OBSERVATION_TIMESTAMP_MILLIS;
+ parcel.macAddress = MAC_ADDRESS;
+ parcel.packageName = PACKAGE_NAME;
+ parcel.pendingAppInstallTimestampMillis = PENDING_APP_INSTALL_TIMESTAMP_MILLIS;
+ parcel.rssi = RSSI;
+ parcel.state = STATE;
+ parcel.title = TITLE;
+ parcel.triggerId = TRIGGER_ID;
+ parcel.txPower = TX_POWER;
+
+ return parcel;
+ }
+
+ private static Rpcs.GetObservedDeviceResponse genHappyPathObservedDeviceResponse() {
+ Rpcs.Device.Builder deviceBuilder = Rpcs.Device.newBuilder();
+ deviceBuilder.setAntiSpoofingKeyPair(Rpcs.AntiSpoofingKeyPair.newBuilder()
+ .setPublicKey(ByteString.copyFrom(ANTI_SPOOFING_KEY))
+ .build());
+ Rpcs.TrueWirelessHeadsetImages.Builder imagesBuilder =
+ Rpcs.TrueWirelessHeadsetImages.newBuilder();
+ imagesBuilder.setLeftBudUrl(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
+ imagesBuilder.setRightBudUrl(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
+ imagesBuilder.setCaseUrl(TRUE_WIRELESS_IMAGE_URL_CASE);
+ deviceBuilder.setTrueWirelessImages(imagesBuilder.build());
+ deviceBuilder.setImageUrl(IMAGE_URL);
+ deviceBuilder.setIntentUri(INTENT_URI);
+ deviceBuilder.setName(NAME);
+ deviceBuilder.setBleTxPower(BLE_TX_POWER);
+ deviceBuilder.setTriggerDistance(TRIGGER_DISTANCE);
+ deviceBuilder.setDeviceType(Rpcs.DeviceType.HEADPHONES);
+
+ return Rpcs.GetObservedDeviceResponse.newBuilder()
+ .setDevice(deviceBuilder.build())
+ .setImage(ByteString.copyFrom(IMAGE))
+ .setStrings(Rpcs.ObservedDeviceStrings.newBuilder()
+ .setConnectSuccessCompanionAppInstalled(
+ CONNECT_SUCCESS_COMPANION_APP_INSTALLED)
+ .setConnectSuccessCompanionAppNotInstalled(
+ CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED)
+ .setDownloadCompanionAppDescription(
+ DOWNLOAD_COMPANION_APP_DESCRIPTION)
+ .setFailConnectGoToSettingsDescription(
+ FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION)
+ .setInitialNotificationDescription(
+ INITIAL_NOTIFICATION_DESCRIPTION)
+ .setInitialNotificationDescriptionNoAccount(
+ INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT)
+ .setInitialPairingDescription(
+ INITIAL_PAIRING_DESCRIPTION)
+ .setOpenCompanionAppDescription(
+ OPEN_COMPANION_APP_DESCRIPTION)
+ .setRetroactivePairingDescription(
+ RETRO_ACTIVE_PAIRING_DESCRIPTION)
+ .setSubsequentPairingDescription(
+ SUBSEQUENT_PAIRING_DESCRIPTION)
+ .setUnableToConnectDescription(
+ UNABLE_TO_CONNECT_DESCRIPTION)
+ .setUnableToConnectTitle(
+ UNABLE_TO_CONNECT_TITLE)
+ .setUpdateCompanionAppDescription(
+ UPDATE_COMPANION_APP_DESCRIPTION)
+ .setWaitLaunchCompanionAppDescription(
+ WAIT_LAUNCH_COMPANION_APP_DESCRIPTION)
+ .build())
+ .build();
+ }
+
+ private static FastPairAntispoofKeyDeviceMetadataParcel
+ genHappyPathFastPairAntispoofKeyDeviceMetadataParcel() {
+ FastPairAntispoofKeyDeviceMetadataParcel parcel =
+ new FastPairAntispoofKeyDeviceMetadataParcel();
+ parcel.antispoofPublicKey = ANTI_SPOOFING_KEY;
+ parcel.deviceMetadata = genHappyPathFastPairDeviceMetadataParcel();
+
+ return parcel;
+ }
+
+ private static FastPairAntispoofKeyDeviceMetadataParcel
+ genFastPairAntispoofKeyDeviceMetadataParcelWithEmptyDeviceMetadata() {
+ FastPairAntispoofKeyDeviceMetadataParcel parcel =
+ new FastPairAntispoofKeyDeviceMetadataParcel();
+ parcel.antispoofPublicKey = ANTI_SPOOFING_KEY;
+ parcel.deviceMetadata = genEmptyFastPairDeviceMetadataParcel();
+
+ return parcel;
+ }
+
+ private static FastPairAntispoofKeyDeviceMetadataParcel
+ genEmptyFastPairAntispoofKeyDeviceMetadataParcel() {
+ return new FastPairAntispoofKeyDeviceMetadataParcel();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java
new file mode 100644
index 0000000..f098600
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.protobuf.ByteString;
+
+import org.junit.Test;
+
+import service.proto.Cache;
+import service.proto.FastPairString.FastPairStrings;
+import service.proto.Rpcs;
+import service.proto.Rpcs.GetObservedDeviceResponse;
+
+public final class DataUtilsTest {
+ private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
+ private static final String APP_PACKAGE = "test_package";
+ private static final String APP_ACTION_URL =
+ "intent:#Intent;action=cto_be_set%3AACTION_MAGIC_PAIR;"
+ + "package=to_be_set;"
+ + "component=to_be_set;"
+ + "to_be_set%3AEXTRA_COMPANION_APP="
+ + APP_PACKAGE
+ + ";end";
+ private static final long DEVICE_ID = 12;
+ private static final String DEVICE_NAME = "My device";
+ private static final byte[] DEVICE_PUBLIC_KEY = base16().decode("0123456789ABCDEF");
+ private static final String DEVICE_COMPANY = "Company name";
+ private static final byte[] DEVICE_IMAGE = new byte[] {0x00, 0x01, 0x10, 0x11};
+ private static final String DEVICE_IMAGE_URL = "device_image_url";
+ private static final String AUTHORITY = "com.android.test";
+ private static final String SIGNATURE_HASH = "as8dfbyu2duas7ikanvklpaclo2";
+ private static final String ACCOUNT = "test@gmail.com";
+
+ private static final String MESSAGE_INIT_NOTIFY_DESCRIPTION = "message 1";
+ private static final String MESSAGE_INIT_NOTIFY_DESCRIPTION_NO_ACCOUNT = "message 2";
+ private static final String MESSAGE_INIT_PAIR_DESCRIPTION = "message 3 %s";
+ private static final String MESSAGE_COMPANION_INSTALLED = "message 4";
+ private static final String MESSAGE_COMPANION_NOT_INSTALLED = "message 5";
+ private static final String MESSAGE_SUBSEQUENT_PAIR_DESCRIPTION = "message 6";
+ private static final String MESSAGE_RETROACTIVE_PAIR_DESCRIPTION = "message 7";
+ private static final String MESSAGE_WAIT_LAUNCH_COMPANION_APP_DESCRIPTION = "message 8";
+ private static final String MESSAGE_FAIL_CONNECT_DESCRIPTION = "message 9";
+ private static final String MESSAGE_FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION =
+ "message 10";
+ private static final String MESSAGE_ASSISTANT_HALF_SHEET_DESCRIPTION = "message 11";
+ private static final String MESSAGE_ASSISTANT_NOTIFICATION_DESCRIPTION = "message 12";
+
+ @Test
+ public void test_toScanFastPairStoreItem_withAccount() {
+ Cache.ScanFastPairStoreItem item = DataUtils.toScanFastPairStoreItem(
+ createObservedDeviceResponse(), BLUETOOTH_ADDRESS, ACCOUNT);
+ assertThat(item.getAddress()).isEqualTo(BLUETOOTH_ADDRESS);
+ assertThat(item.getActionUrl()).isEqualTo(APP_ACTION_URL);
+ assertThat(item.getDeviceName()).isEqualTo(DEVICE_NAME);
+ assertThat(item.getIconPng()).isEqualTo(ByteString.copyFrom(DEVICE_IMAGE));
+ assertThat(item.getIconFifeUrl()).isEqualTo(DEVICE_IMAGE_URL);
+ assertThat(item.getAntiSpoofingPublicKey())
+ .isEqualTo(ByteString.copyFrom(DEVICE_PUBLIC_KEY));
+
+ FastPairStrings strings = item.getFastPairStrings();
+ assertThat(strings.getTapToPairWithAccount()).isEqualTo(MESSAGE_INIT_NOTIFY_DESCRIPTION);
+ assertThat(strings.getTapToPairWithoutAccount())
+ .isEqualTo(MESSAGE_INIT_NOTIFY_DESCRIPTION_NO_ACCOUNT);
+ assertThat(strings.getInitialPairingDescription())
+ .isEqualTo(String.format(MESSAGE_INIT_PAIR_DESCRIPTION, DEVICE_NAME));
+ assertThat(strings.getPairingFinishedCompanionAppInstalled())
+ .isEqualTo(MESSAGE_COMPANION_INSTALLED);
+ assertThat(strings.getPairingFinishedCompanionAppNotInstalled())
+ .isEqualTo(MESSAGE_COMPANION_NOT_INSTALLED);
+ assertThat(strings.getSubsequentPairingDescription())
+ .isEqualTo(MESSAGE_SUBSEQUENT_PAIR_DESCRIPTION);
+ assertThat(strings.getRetroactivePairingDescription())
+ .isEqualTo(MESSAGE_RETROACTIVE_PAIR_DESCRIPTION);
+ assertThat(strings.getWaitAppLaunchDescription())
+ .isEqualTo(MESSAGE_WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
+ assertThat(strings.getPairingFailDescription())
+ .isEqualTo(MESSAGE_FAIL_CONNECT_DESCRIPTION);
+ }
+
+ @Test
+ public void test_toScanFastPairStoreItem_withoutAccount() {
+ Cache.ScanFastPairStoreItem item = DataUtils.toScanFastPairStoreItem(
+ createObservedDeviceResponse(), BLUETOOTH_ADDRESS, /* account= */ null);
+ FastPairStrings strings = item.getFastPairStrings();
+ assertThat(strings.getInitialPairingDescription())
+ .isEqualTo(MESSAGE_INIT_NOTIFY_DESCRIPTION_NO_ACCOUNT);
+ }
+
+ @Test
+ public void test_toString() {
+ Cache.ScanFastPairStoreItem item = DataUtils.toScanFastPairStoreItem(
+ createObservedDeviceResponse(), BLUETOOTH_ADDRESS, ACCOUNT);
+ FastPairStrings strings = item.getFastPairStrings();
+
+ assertThat(DataUtils.toString(strings))
+ .isEqualTo("FastPairStrings[tapToPairWithAccount=message 1, "
+ + "tapToPairWithoutAccount=message 2, "
+ + "initialPairingDescription=message 3 " + DEVICE_NAME + ", "
+ + "pairingFinishedCompanionAppInstalled=message 4, "
+ + "pairingFinishedCompanionAppNotInstalled=message 5, "
+ + "subsequentPairingDescription=message 6, "
+ + "retroactivePairingDescription=message 7, "
+ + "waitAppLaunchDescription=message 8, "
+ + "pairingFailDescription=message 9]");
+ }
+
+ private static GetObservedDeviceResponse createObservedDeviceResponse() {
+ return GetObservedDeviceResponse.newBuilder()
+ .setDevice(
+ Rpcs.Device.newBuilder()
+ .setId(DEVICE_ID)
+ .setName(DEVICE_NAME)
+ .setAntiSpoofingKeyPair(
+ Rpcs.AntiSpoofingKeyPair
+ .newBuilder()
+ .setPublicKey(
+ ByteString.copyFrom(DEVICE_PUBLIC_KEY)))
+ .setIntentUri(APP_ACTION_URL)
+ .setDataOnlyConnection(true)
+ .setAssistantSupported(false)
+ .setCompanionDetail(
+ Rpcs.CompanionAppDetails.newBuilder()
+ .setAuthority(AUTHORITY)
+ .setCertificateHash(SIGNATURE_HASH)
+ .build())
+ .setCompanyName(DEVICE_COMPANY)
+ .setImageUrl(DEVICE_IMAGE_URL))
+ .setImage(ByteString.copyFrom(DEVICE_IMAGE))
+ .setStrings(
+ Rpcs.ObservedDeviceStrings.newBuilder()
+ .setInitialNotificationDescription(MESSAGE_INIT_NOTIFY_DESCRIPTION)
+ .setInitialNotificationDescriptionNoAccount(
+ MESSAGE_INIT_NOTIFY_DESCRIPTION_NO_ACCOUNT)
+ .setInitialPairingDescription(MESSAGE_INIT_PAIR_DESCRIPTION)
+ .setConnectSuccessCompanionAppInstalled(MESSAGE_COMPANION_INSTALLED)
+ .setConnectSuccessCompanionAppNotInstalled(
+ MESSAGE_COMPANION_NOT_INSTALLED)
+ .setSubsequentPairingDescription(
+ MESSAGE_SUBSEQUENT_PAIR_DESCRIPTION)
+ .setRetroactivePairingDescription(
+ MESSAGE_RETROACTIVE_PAIR_DESCRIPTION)
+ .setWaitLaunchCompanionAppDescription(
+ MESSAGE_WAIT_LAUNCH_COMPANION_APP_DESCRIPTION)
+ .setFailConnectGoToSettingsDescription(
+ MESSAGE_FAIL_CONNECT_DESCRIPTION))
+ .build();
+ }
+}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 52bc2c0..1b9f2ec 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -20,17 +20,12 @@
}
// Include build rules from Sources.bp
-// sc-mainline-prod only: do not include Sources.bp
-// build = ["Sources.bp"]
+build = ["Sources.bp"]
filegroup {
name: "service-connectivity-tiramisu-sources",
srcs: [
- // sc-mainline-prod only: Building T sources is disabled on this branch.
- // "src/**/*.java",
- "src/com/android/server/ConnectivityServiceInitializer.java",
- // filegroup contains empty stubs on sc-mainline-prod.
- ":services.connectivity-tiramisu-updatable-sources",
+ "src/**/*.java",
],
visibility: ["//visibility:private"],
}
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index e9053dd..4ac6174 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -91,6 +91,7 @@
* if setIncludeTestInterfaces is true, any test interfaces.
*/
private volatile String mIfaceMatch;
+
/**
* Track test interfaces if true, don't track otherwise.
*/
diff --git a/service/Android.bp b/service/Android.bp
index 031fe31..7dbdc92 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -169,7 +169,7 @@
"networkstack-client",
"PlatformProperties",
"service-connectivity-protos",
- "NetworkStackApiStableShims",
+ "NetworkStackApiCurrentShims",
],
apex_available: [
"com.android.tethering",
@@ -216,6 +216,11 @@
apex_available: [
"com.android.tethering",
],
+ optimize: {
+ enabled: true,
+ shrink: true,
+ proguard_flags_files: ["proguard.flags"],
+ },
lint: { strict_updatability_linting: true },
}
diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt
index 4b21569..c1c2e9d 100644
--- a/service/jarjar-rules.txt
+++ b/service/jarjar-rules.txt
@@ -105,6 +105,9 @@
# From the API shims
rule com.android.networkstack.apishim.** com.android.connectivity.@0
+# From fast-pair-lite-protos
+rule service.proto.** com.android.server.nearby.@0
+
# From filegroup framework-connectivity-protos
rule android.service.*Proto com.android.connectivity.@0
diff --git a/service/proguard.flags b/service/proguard.flags
new file mode 100644
index 0000000..2b20ddd
--- /dev/null
+++ b/service/proguard.flags
@@ -0,0 +1,21 @@
+# Make sure proguard keeps all connectivity classes
+# TODO: instead of keeping everything, consider listing only "entry points"
+# (service loader, JNI registered methods, etc) and letting the optimizer do its job
+-keep class android.net.** { *; }
+-keep class com.android.connectivity.** { *; }
+-keep class com.android.net.** { *; }
+-keep class com.android.server.** { *; }
+
+# Prevent proguard from stripping out any nearby-service and fast-pair-lite-protos fields.
+# TODO: This could be optimized in the future to only keep the critical
+# entry points and then let proguard strip out any unused code within
+# the service. "com.android.server.nearby.service.proto" must be kept to prevent proguard
+# from stripping out any fast-pair-lite-protos fields.
+-keep class com.android.server.nearby.** { *; }
+
+# The lite proto runtime uses reflection to access fields based on the names in
+# the schema, keep all the fields.
+# This replicates the base proguard rule used by the build by default
+# (proguard_basic_keeps.flags), but needs to be specified here because the
+# com.google.protobuf package is jarjared to the below package.
+-keepclassmembers class * extends com.android.connectivity.com.google.protobuf.MessageLite { <fields>; }
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index f330dbf..40b2bdb 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -5490,6 +5490,7 @@
@Override
@Deprecated
public int getLastTetherError(String iface) {
+ enforceAccessPermission();
final TetheringManager tm = (TetheringManager) mContext.getSystemService(
Context.TETHERING_SERVICE);
return tm.getLastTetherError(iface);
@@ -5498,6 +5499,7 @@
@Override
@Deprecated
public String[] getTetherableIfaces() {
+ enforceAccessPermission();
final TetheringManager tm = (TetheringManager) mContext.getSystemService(
Context.TETHERING_SERVICE);
return tm.getTetherableIfaces();
@@ -5506,6 +5508,7 @@
@Override
@Deprecated
public String[] getTetheredIfaces() {
+ enforceAccessPermission();
final TetheringManager tm = (TetheringManager) mContext.getSystemService(
Context.TETHERING_SERVICE);
return tm.getTetheredIfaces();
@@ -5515,6 +5518,7 @@
@Override
@Deprecated
public String[] getTetheringErroredIfaces() {
+ enforceAccessPermission();
final TetheringManager tm = (TetheringManager) mContext.getSystemService(
Context.TETHERING_SERVICE);
@@ -5524,6 +5528,7 @@
@Override
@Deprecated
public String[] getTetherableUsbRegexs() {
+ enforceAccessPermission();
final TetheringManager tm = (TetheringManager) mContext.getSystemService(
Context.TETHERING_SERVICE);
@@ -5533,6 +5538,7 @@
@Override
@Deprecated
public String[] getTetherableWifiRegexs() {
+ enforceAccessPermission();
final TetheringManager tm = (TetheringManager) mContext.getSystemService(
Context.TETHERING_SERVICE);
return tm.getTetherableWifiRegexs();
diff --git a/tests/common/java/android/net/netstats/NetworkStatsCollectionTest.kt b/tests/common/java/android/net/netstats/NetworkStatsCollectionTest.kt
new file mode 100644
index 0000000..ca0e5ed
--- /dev/null
+++ b/tests/common/java/android/net/netstats/NetworkStatsCollectionTest.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.net.netstats
+
+import android.net.NetworkIdentitySet
+import android.net.NetworkStatsCollection
+import android.net.NetworkStatsHistory
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.SC_V2
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+import kotlin.test.fail
+
+@ConnectivityModuleTest
+@RunWith(JUnit4::class)
+@SmallTest
+class NetworkStatsCollectionTest {
+ @Rule
+ @JvmField
+ val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
+ @Test
+ fun testBuilder() {
+ val ident = NetworkIdentitySet()
+ val key1 = NetworkStatsCollection.Key(ident, /* uid */ 0, /* set */ 0, /* tag */ 0)
+ val key2 = NetworkStatsCollection.Key(ident, /* uid */ 1, /* set */ 0, /* tag */ 0)
+ val bucketDuration = 10L
+ val entry1 = NetworkStatsHistory.Entry(10, 10, 40, 4, 50, 5, 60)
+ val entry2 = NetworkStatsHistory.Entry(30, 10, 3, 41, 7, 1, 0)
+ val history1 = NetworkStatsHistory.Builder(10, 5)
+ .addEntry(entry1)
+ .addEntry(entry2)
+ .build()
+ val history2 = NetworkStatsHistory(10, 5)
+ val actualCollection = NetworkStatsCollection.Builder(bucketDuration)
+ .addEntry(key1, history1)
+ .addEntry(key2, history2)
+ .build()
+
+ // The builder will omit any entry with empty history. Thus, only history1
+ // is expected in the result collection.
+ val actualEntries = actualCollection.entries
+ assertEquals(1, actualEntries.size)
+ val actualHistory = actualEntries[key1] ?: fail("There should be an entry for $key1")
+ assertEquals(history1.entries, actualHistory.entries)
+ }
+}
\ No newline at end of file
diff --git a/tests/common/java/android/net/netstats/NetworkStatsHistoryTest.kt b/tests/common/java/android/net/netstats/NetworkStatsHistoryTest.kt
new file mode 100644
index 0000000..c2654c5
--- /dev/null
+++ b/tests/common/java/android/net/netstats/NetworkStatsHistoryTest.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.net.netstats
+
+import android.net.NetworkStatsHistory
+import android.text.format.DateUtils
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.SC_V2
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+
+@ConnectivityModuleTest
+@RunWith(JUnit4::class)
+@SmallTest
+class NetworkStatsHistoryTest {
+ @Rule
+ @JvmField
+ val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
+ @Test
+ fun testBuilder() {
+ val entry1 = NetworkStatsHistory.Entry(10, 30, 40, 4, 50, 5, 60)
+ val entry2 = NetworkStatsHistory.Entry(30, 15, 3, 41, 7, 1, 0)
+ val entry3 = NetworkStatsHistory.Entry(7, 301, 11, 14, 31, 2, 80)
+ val statsEmpty = NetworkStatsHistory
+ .Builder(DateUtils.HOUR_IN_MILLIS, /* initialCapacity */ 10).build()
+ assertEquals(0, statsEmpty.entries.size)
+ assertEquals(DateUtils.HOUR_IN_MILLIS, statsEmpty.bucketDuration)
+ val statsSingle = NetworkStatsHistory
+ .Builder(DateUtils.HOUR_IN_MILLIS, /* initialCapacity */ 8)
+ .addEntry(entry1)
+ .build()
+ statsSingle.assertEntriesEqual(entry1)
+ assertEquals(DateUtils.HOUR_IN_MILLIS, statsSingle.bucketDuration)
+ val statsMultiple = NetworkStatsHistory
+ .Builder(DateUtils.SECOND_IN_MILLIS, /* initialCapacity */ 0)
+ .addEntry(entry1).addEntry(entry2).addEntry(entry3)
+ .build()
+ assertEquals(DateUtils.SECOND_IN_MILLIS, statsMultiple.bucketDuration)
+ statsMultiple.assertEntriesEqual(entry1, entry2, entry3)
+ }
+
+ fun NetworkStatsHistory.assertEntriesEqual(vararg entries: NetworkStatsHistory.Entry) {
+ assertEquals(entries.size, this.entries.size)
+ entries.forEachIndexed { i, element ->
+ assertEquals(element, this.entries[i])
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/common/java/android/net/netstats/NetworkTemplateTest.kt b/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
new file mode 100644
index 0000000..192694b
--- /dev/null
+++ b/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
@@ -0,0 +1,205 @@
+/*
+ * 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.net.netstats
+
+import android.net.NetworkStats.DEFAULT_NETWORK_ALL
+import android.net.NetworkStats.METERED_ALL
+import android.net.NetworkStats.METERED_YES
+import android.net.NetworkStats.ROAMING_YES
+import android.net.NetworkStats.ROAMING_ALL
+import android.net.NetworkTemplate
+import android.net.NetworkTemplate.MATCH_BLUETOOTH
+import android.net.NetworkTemplate.MATCH_CARRIER
+import android.net.NetworkTemplate.MATCH_ETHERNET
+import android.net.NetworkTemplate.MATCH_MOBILE
+import android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD
+import android.net.NetworkTemplate.MATCH_PROXY
+import android.net.NetworkTemplate.MATCH_WIFI
+import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD
+import android.net.NetworkTemplate.NETWORK_TYPE_ALL
+import android.net.NetworkTemplate.OEM_MANAGED_ALL
+import android.telephony.TelephonyManager
+import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL
+import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.SC_V2
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+private const val TEST_IMSI1 = "imsi"
+private const val TEST_WIFI_KEY1 = "wifiKey1"
+private const val TEST_WIFI_KEY2 = "wifiKey2"
+
+@RunWith(JUnit4::class)
+@ConnectivityModuleTest
+class NetworkTemplateTest {
+ @Rule
+ @JvmField
+ val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
+ @Test
+ fun testBuilderMatchRules() {
+ // Verify unknown match rules cannot construct templates.
+ listOf(Integer.MIN_VALUE, -1, Integer.MAX_VALUE).forEach {
+ assertFailsWith<IllegalArgumentException> {
+ NetworkTemplate.Builder(it).build()
+ }
+ }
+
+ // Verify hidden match rules cannot construct templates.
+ listOf(MATCH_WIFI_WILDCARD, MATCH_MOBILE_WILDCARD, MATCH_PROXY).forEach {
+ assertFailsWith<IllegalArgumentException> {
+ NetworkTemplate.Builder(it).build()
+ }
+ }
+
+ // Verify template which matches metered cellular and carrier networks with
+ // the given IMSI. See buildTemplateMobileAll and buildTemplateCarrierMetered.
+ listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
+ NetworkTemplate.Builder(matchRule).setSubscriberIds(setOf(TEST_IMSI1))
+ .setMeteredness(METERED_YES).build().let {
+ val expectedTemplate = NetworkTemplate(matchRule, TEST_IMSI1,
+ arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
+ ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+ assertEquals(expectedTemplate, it)
+ }
+ }
+
+ // Verify template which matches roaming cellular and carrier networks with
+ // the given IMSI.
+ listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
+ NetworkTemplate.Builder(matchRule).setSubscriberIds(setOf(TEST_IMSI1))
+ .setRoaming(ROAMING_YES).setMeteredness(METERED_YES).build().let {
+ val expectedTemplate = NetworkTemplate(matchRule, TEST_IMSI1,
+ arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
+ ROAMING_YES, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+ assertEquals(expectedTemplate, it)
+ }
+ }
+
+ // Verify carrier template cannot be created without IMSI.
+ assertFailsWith<IllegalArgumentException> {
+ NetworkTemplate.Builder(MATCH_CARRIER).build()
+ }
+
+ // Verify template which matches metered cellular networks,
+ // regardless of IMSI. See buildTemplateMobileWildcard.
+ NetworkTemplate.Builder(MATCH_MOBILE).setMeteredness(METERED_YES).build().let {
+ val expectedTemplate = NetworkTemplate(MATCH_MOBILE_WILDCARD, null /*subscriberId*/,
+ null /*subscriberIds*/, arrayOf<String>(),
+ METERED_YES, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+ assertEquals(expectedTemplate, it)
+ }
+
+ // Verify template which matches metered cellular networks and ratType.
+ // See NetworkTemplate#buildTemplateMobileWithRatType.
+ NetworkTemplate.Builder(MATCH_MOBILE).setSubscriberIds(setOf(TEST_IMSI1))
+ .setMeteredness(METERED_YES).setRatType(TelephonyManager.NETWORK_TYPE_UMTS)
+ .build().let {
+ val expectedTemplate = NetworkTemplate(MATCH_MOBILE, TEST_IMSI1,
+ arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
+ ROAMING_ALL, DEFAULT_NETWORK_ALL, TelephonyManager.NETWORK_TYPE_UMTS,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+ assertEquals(expectedTemplate, it)
+ }
+
+ // Verify template which matches all wifi networks,
+ // regardless of Wifi Network Key. See buildTemplateWifiWildcard and buildTemplateWifi.
+ NetworkTemplate.Builder(MATCH_WIFI).build().let {
+ val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
+ null /*subscriberIds*/, arrayOf<String>(),
+ METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+ assertEquals(expectedTemplate, it)
+ }
+
+ // Verify template which matches wifi networks with the given Wifi Network Key.
+ // See buildTemplateWifi(wifiNetworkKey).
+ NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
+ val expectedTemplate = NetworkTemplate(MATCH_WIFI, null /*subscriberId*/,
+ null /*subscriberIds*/, arrayOf(TEST_WIFI_KEY1),
+ METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+ assertEquals(expectedTemplate, it)
+ }
+
+ // Verify template which matches all wifi networks with the
+ // given Wifi Network Key, and IMSI. See buildTemplateWifi(wifiNetworkKey, subscriberId).
+ NetworkTemplate.Builder(MATCH_WIFI).setSubscriberIds(setOf(TEST_IMSI1))
+ .setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
+ val expectedTemplate = NetworkTemplate(MATCH_WIFI, TEST_IMSI1,
+ arrayOf(TEST_IMSI1), arrayOf(TEST_WIFI_KEY1),
+ METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+ assertEquals(expectedTemplate, it)
+ }
+
+ // Verify template which matches ethernet and bluetooth networks.
+ // See buildTemplateEthernet and buildTemplateBluetooth.
+ listOf(MATCH_ETHERNET, MATCH_BLUETOOTH).forEach { matchRule ->
+ NetworkTemplate.Builder(matchRule).build().let {
+ val expectedTemplate = NetworkTemplate(matchRule, null /*subscriberId*/,
+ null /*subscriberIds*/, arrayOf<String>(),
+ METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+ assertEquals(expectedTemplate, it)
+ }
+ }
+ }
+
+ @Test
+ fun testBuilderWifiNetworkKeys() {
+ // Verify template builder which generates same template with the given different
+ // sequence keys.
+ NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
+ setOf(TEST_WIFI_KEY1, TEST_WIFI_KEY2)).build().let {
+ val expectedTemplate = NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
+ setOf(TEST_WIFI_KEY2, TEST_WIFI_KEY1)).build()
+ assertEquals(expectedTemplate, it)
+ }
+
+ // Verify template which matches non-wifi networks with the given key is invalid.
+ listOf(MATCH_MOBILE, MATCH_CARRIER, MATCH_ETHERNET, MATCH_BLUETOOTH, -1,
+ Integer.MAX_VALUE).forEach { matchRule ->
+ assertFailsWith<IllegalArgumentException> {
+ NetworkTemplate.Builder(matchRule).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build()
+ }
+ }
+
+ // Verify template which matches wifi networks with the given null key is invalid.
+ assertFailsWith<IllegalArgumentException> {
+ NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(null)).build()
+ }
+
+ // Verify template which matches wifi wildcard with the given empty key set.
+ NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf<String>()).build().let {
+ val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
+ arrayOf<String>() /*subscriberIds*/, arrayOf<String>(),
+ METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+ assertEquals(expectedTemplate, it)
+ }
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index 96ce65f..eb7d1ea 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -54,6 +54,7 @@
import android.os.BatteryManager;
import android.os.Binder;
import android.os.Bundle;
+import android.os.RemoteCallback;
import android.os.SystemClock;
import android.provider.DeviceConfig;
import android.service.notification.NotificationListenerService;
@@ -141,6 +142,7 @@
private static final int ACTIVITY_NETWORK_STATE_TIMEOUT_MS = 6_000;
private static final int JOB_NETWORK_STATE_TIMEOUT_MS = 10_000;
+ private static final int LAUNCH_ACTIVITY_TIMEOUT_MS = 10_000;
// Must be higher than NETWORK_TIMEOUT_MS
private static final int ORDERED_BROADCAST_TIMEOUT_MS = NETWORK_TIMEOUT_MS * 4;
@@ -802,6 +804,22 @@
mDeviceIdleDeviceConfigStateHelper.restoreOriginalValues();
}
+ protected void launchActivity() throws Exception {
+ turnScreenOn();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_ACTIVTIY);
+ final RemoteCallback callback = new RemoteCallback(result -> latch.countDown());
+ launchIntent.putExtra(Intent.EXTRA_REMOTE_CALLBACK, callback);
+ mContext.startActivity(launchIntent);
+ // There might be a race when app2 is launched but ACTION_FINISH_ACTIVITY has not registered
+ // before test calls finishActivity(). When the issue is happened, there is no way to fix
+ // it, so have a callback design to make sure that the app is launched completely and
+ // ACTION_FINISH_ACTIVITY will be registered before leaving this method.
+ if (!latch.await(LAUNCH_ACTIVITY_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ fail("Timed out waiting for launching activity");
+ }
+ }
+
protected void launchComponentAndAssertNetworkAccess(int type) throws Exception {
launchComponentAndAssertNetworkAccess(type, true);
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java
index ad7ec9e..a0d88c9 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java
@@ -18,37 +18,28 @@
import static android.os.Process.SYSTEM_UID;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.assertIsUidRestrictedOnMeteredNetworks;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.assertNetworkingBlockedStatusForUid;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isUidNetworkingBlocked;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isUidRestrictedOnMeteredNetworks;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
import org.junit.After;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
public class NetworkPolicyManagerTest extends AbstractRestrictBackgroundNetworkTestCase {
private static final boolean METERED = true;
private static final boolean NON_METERED = false;
- @Rule
- public final MeterednessConfigurationRule mMeterednessConfiguration =
- new MeterednessConfigurationRule();
-
@Before
public void setUp() throws Exception {
super.setUp();
- assumeTrue(canChangeActiveNetworkMeteredness());
-
registerBroadcastReceiver();
removeRestrictBackgroundWhitelist(mUid);
@@ -145,13 +136,14 @@
removeRestrictBackgroundWhitelist(mUid);
// Make TEST_APP2_PKG go to foreground and mUid will be allowed temporarily.
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+ launchActivity();
assertForegroundState();
assertNetworkingBlockedStatusForUid(mUid, METERED,
false /* expectedResult */); // Match NTWK_ALLOWED_TMP_ALLOWLIST
// Back to background.
finishActivity();
+ assertBackgroundState();
assertNetworkingBlockedStatusForUid(mUid, METERED,
true /* expectedResult */); // Match NTWK_BLOCKED_BG_RESTRICT
} finally {
@@ -222,26 +214,27 @@
// enabled and mUid is not in the restrict background whitelist and TEST_APP2_PKG is not
// in the foreground. For other cases, it will return false.
setRestrictBackground(true);
- assertTrue(isUidRestrictedOnMeteredNetworks(mUid));
+ assertIsUidRestrictedOnMeteredNetworks(mUid, true /* expectedResult */);
// Make TEST_APP2_PKG go to foreground and isUidRestrictedOnMeteredNetworks() will
// return false.
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+ launchActivity();
assertForegroundState();
- assertFalse(isUidRestrictedOnMeteredNetworks(mUid));
+ assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
// Back to background.
finishActivity();
+ assertBackgroundState();
// Add mUid into restrict background whitelist and isUidRestrictedOnMeteredNetworks()
// will return false.
addRestrictBackgroundWhitelist(mUid);
- assertFalse(isUidRestrictedOnMeteredNetworks(mUid));
+ assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
removeRestrictBackgroundWhitelist(mUid);
} finally {
// Restrict background is disabled and isUidRestrictedOnMeteredNetworks() will return
// false.
setRestrictBackground(false);
- assertFalse(isUidRestrictedOnMeteredNetworks(mUid));
+ assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
}
}
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
index 56be3e3..0a0f24b 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
@@ -447,6 +447,11 @@
PollingCheck.waitFor(() -> (expectedResult == isUidNetworkingBlocked(uid, metered)));
}
+ public static void assertIsUidRestrictedOnMeteredNetworks(int uid, boolean expectedResult)
+ throws Exception {
+ PollingCheck.waitFor(() -> (expectedResult == isUidRestrictedOnMeteredNetworks(uid)));
+ }
+
public static boolean isUidNetworkingBlocked(int uid, boolean meteredNetwork) {
final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
try {
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
index 5f0f6d6..4266aad 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
@@ -24,6 +24,7 @@
@Before
public void setUp() throws Exception {
super.setUp();
+ setRestrictedNetworkingMode(false);
}
@After
@@ -34,8 +35,6 @@
@Test
public void testNetworkAccess() throws Exception {
- setRestrictedNetworkingMode(false);
-
// go to foreground state and enable restricted mode
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
setRestrictedNetworkingMode(true);
@@ -54,4 +53,18 @@
finishActivity();
assertBackgroundNetworkAccess(true);
}
+
+ @Test
+ public void testNetworkAccess_withBatterySaver() throws Exception {
+ setBatterySaverMode(true);
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertBackgroundNetworkAccess(true);
+
+ setRestrictedNetworkingMode(true);
+ // App would be denied network access since Restricted mode is on.
+ assertBackgroundNetworkAccess(false);
+ setRestrictedNetworkingMode(false);
+ // Given that Restricted mode is turned off, app should be able to access network again.
+ assertBackgroundNetworkAccess(true);
+ }
}
diff --git a/tests/cts/hostside/app2/AndroidManifest.xml b/tests/cts/hostside/app2/AndroidManifest.xml
index 6c9b469..ff7240d 100644
--- a/tests/cts/hostside/app2/AndroidManifest.xml
+++ b/tests/cts/hostside/app2/AndroidManifest.xml
@@ -22,6 +22,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!--
This application is used to listen to RESTRICT_BACKGROUND_CHANGED intents and store
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
index 9fdb9c9..eb7dca7 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
@@ -17,7 +17,6 @@
import static com.android.cts.net.hostside.app2.Common.ACTION_FINISH_ACTIVITY;
import static com.android.cts.net.hostside.app2.Common.TAG;
-import static com.android.cts.net.hostside.app2.Common.TEST_PKG;
import static com.android.cts.net.hostside.app2.Common.TYPE_COMPONENT_ACTIVTY;
import android.app.Activity;
@@ -25,13 +24,10 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.os.AsyncTask;
import android.os.Bundle;
-import android.os.RemoteException;
+import android.os.RemoteCallback;
import android.util.Log;
-import com.android.cts.net.hostside.INetworkStateObserver;
-
/**
* Activity used to bring process to foreground.
*/
@@ -51,7 +47,13 @@
MyActivity.this.finish();
}
};
- registerReceiver(finishCommandReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY));
+ registerReceiver(finishCommandReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY),
+ Context.RECEIVER_EXPORTED);
+ final RemoteCallback callback = getIntent().getParcelableExtra(
+ Intent.EXTRA_REMOTE_CALLBACK);
+ if (callback != null) {
+ callback.sendResult(null);
+ }
}
@Override
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyJobService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyJobService.java
index 51c3157..8c112b6 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyJobService.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyJobService.java
@@ -56,7 +56,8 @@
}
}
};
- registerReceiver(mFinishCommandReceiver, new IntentFilter(ACTION_FINISH_JOB));
+ registerReceiver(mFinishCommandReceiver, new IntentFilter(ACTION_FINISH_JOB),
+ Context.RECEIVER_EXPORTED);
return true;
}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
index a95fc64..f633df4 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
@@ -330,6 +330,11 @@
"testNetworkAccess");
}
+ public void testNetworkAccess_restrictedMode_withBatterySaver() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
+ "testNetworkAccess_withBatterySaver");
+ }
+
/************************
* Expedited job tests. *
************************/
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index fb720a7..de4f41b 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -29,6 +29,11 @@
import static android.app.usage.NetworkStats.Bucket.STATE_FOREGROUND;
import static android.app.usage.NetworkStats.Bucket.TAG_NONE;
import static android.app.usage.NetworkStats.Bucket.UID_ALL;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import android.app.AppOpsManager;
import android.app.usage.NetworkStats;
@@ -40,7 +45,10 @@
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
+import android.net.NetworkStatsCollection;
+import android.net.NetworkStatsHistory;
import android.net.TrafficStats;
+import android.net.netstats.NetworkStatsDataMigrationUtils;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
@@ -49,10 +57,13 @@
import android.platform.test.annotations.AppModeFull;
import android.telephony.TelephonyManager;
import android.test.InstrumentationTestCase;
+import android.text.TextUtils;
import android.util.Log;
import com.android.compatibility.common.util.ShellIdentityUtils;
import com.android.compatibility.common.util.SystemUtil;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.testutils.DevSdkIgnoreRule;
import java.io.IOException;
import java.io.InputStream;
@@ -62,6 +73,10 @@
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
public class NetworkStatsManagerTest extends InstrumentationTestCase {
private static final String LOG_TAG = "NetworkStatsManagerTest";
@@ -825,6 +840,43 @@
// storing files of >2MB in CTS.
mNsm.unregisterUsageCallback(usageCallback);
+
+ // For T- devices, the registerUsageCallback invocation below will need a looper
+ // from the thread that calls into the API, which is not available in the test.
+ if (SdkLevel.isAtLeastT()) {
+ mNsm.registerUsageCallback(mNetworkInterfacesToTest[i].getNetworkType(),
+ getSubscriberId(i), THRESHOLD_BYTES, usageCallback);
+ mNsm.unregisterUsageCallback(usageCallback);
+ }
+ }
+ }
+
+ @AppModeFull
+ @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+ public void testDataMigrationUtils() throws Exception {
+ final List<String> prefixes = List.of(PREFIX_UID, PREFIX_XT, PREFIX_UID_TAG);
+ for (final String prefix : prefixes) {
+ final long duration = TextUtils.equals(PREFIX_XT, prefix) ? TimeUnit.HOURS.toMillis(1)
+ : TimeUnit.HOURS.toMillis(2);
+
+ final NetworkStatsCollection collection =
+ NetworkStatsDataMigrationUtils.readPlatformCollection(prefix, duration);
+
+ final long now = System.currentTimeMillis();
+ final Set<Map.Entry<NetworkStatsCollection.Key, NetworkStatsHistory>> entries =
+ collection.getEntries().entrySet();
+ for (final Map.Entry<NetworkStatsCollection.Key, NetworkStatsHistory> entry : entries) {
+ for (final NetworkStatsHistory.Entry historyEntry : entry.getValue().getEntries()) {
+ // Verify all value fields are reasonable.
+ assertTrue(historyEntry.getBucketStart() <= now);
+ assertTrue(historyEntry.getActiveTime() <= duration);
+ assertTrue(historyEntry.getRxBytes() >= 0);
+ assertTrue(historyEntry.getRxPackets() >= 0);
+ assertTrue(historyEntry.getTxBytes() >= 0);
+ assertTrue(historyEntry.getTxPackets() >= 0);
+ assertTrue(historyEntry.getOperations() >= 0);
+ }
+ }
}
}
diff --git a/tests/deflake/Android.bp b/tests/deflake/Android.bp
index b3d0363..8205f1c 100644
--- a/tests/deflake/Android.bp
+++ b/tests/deflake/Android.bp
@@ -21,7 +21,7 @@
// FrameworksNetDeflakeTest depends on FrameworksNetTests so it should be disabled
// if FrameworksNetTests is disabled.
-enable_frameworks_net_deflake_test = false
+enable_frameworks_net_deflake_test = true
// Placeholder
// This is a placeholder comment to minimize merge conflicts, as enable_frameworks_net_deflake_test
// may have different values depending on the branch
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 94e8916..c9a41ba 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -3,13 +3,17 @@
//########################################################################
package {
// See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "Android-Apache-2.0"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["Android-Apache-2.0"],
}
// Whether to enable the FrameworksNetTests. Set to false in the branches that might have older
// frameworks/base since FrameworksNetTests includes the test for classes that are not in
// connectivity module.
-enable_frameworks_net_tests = false
+enable_frameworks_net_tests = true
// Placeholder
// This is a placeholder comment to minimize merge conflicts, as enable_frameworks_net_tests
// may have different values depending on the branch
diff --git a/tests/unit/java/android/net/NetworkStatsCollectionTest.java b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
index 32c106d..0f02850 100644
--- a/tests/unit/java/android/net/NetworkStatsCollectionTest.java
+++ b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
@@ -38,13 +38,11 @@
import static org.junit.Assert.fail;
import android.content.res.Resources;
-import android.net.NetworkStatsCollection.Key;
import android.os.Process;
import android.os.UserHandle;
import android.telephony.SubscriptionPlan;
import android.telephony.TelephonyManager;
import android.text.format.DateUtils;
-import android.util.ArrayMap;
import android.util.RecurrenceRule;
import androidx.test.InstrumentationRegistry;
@@ -75,7 +73,6 @@
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
-import java.util.Map;
/**
* Tests for {@link NetworkStatsCollection}.
@@ -534,52 +531,6 @@
assertThrows(ArithmeticException.class, () -> multiplySafeByRational(30, 3, 0));
}
- @Test
- public void testBuilder() {
- final Map<Key, NetworkStatsHistory> expectedEntries = new ArrayMap<>();
- final NetworkStats.Entry entry = new NetworkStats.Entry();
- final NetworkIdentitySet ident = new NetworkIdentitySet();
- final Key key1 = new Key(ident, 0, 0, 0);
- final Key key2 = new Key(ident, 1, 0, 0);
- final long bucketDuration = 10;
-
- final NetworkStatsHistory.Entry entry1 = new NetworkStatsHistory.Entry(10, 10, 40,
- 4, 50, 5, 60);
- final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(30, 10, 3,
- 41, 7, 1, 0);
-
- NetworkStatsHistory history1 = new NetworkStatsHistory.Builder(10, 5)
- .addEntry(entry1)
- .addEntry(entry2)
- .build();
-
- NetworkStatsHistory history2 = new NetworkStatsHistory(10, 5);
-
- NetworkStatsCollection actualCollection = new NetworkStatsCollection.Builder(bucketDuration)
- .addEntry(key1, history1)
- .addEntry(key2, history2)
- .build();
-
- // The builder will omit any entry with empty history. Thus, history2
- // is not expected in the result collection.
- expectedEntries.put(key1, history1);
-
- final Map<Key, NetworkStatsHistory> actualEntries = actualCollection.getEntries();
-
- assertEquals(expectedEntries.size(), actualEntries.size());
- for (Key expectedKey : expectedEntries.keySet()) {
- final NetworkStatsHistory expectedHistory = expectedEntries.get(expectedKey);
-
- final NetworkStatsHistory actualHistory = actualEntries.get(expectedKey);
- assertNotNull(actualHistory);
-
- assertEquals(expectedHistory.getEntries(), actualHistory.getEntries());
-
- actualEntries.remove(expectedKey);
- }
- assertEquals(0, actualEntries.size());
- }
-
/**
* Copy a {@link Resources#openRawResource(int)} into {@link File} for
* testing purposes.
diff --git a/tests/unit/java/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
index c170605..c5f8c00 100644
--- a/tests/unit/java/android/net/NetworkStatsHistoryTest.java
+++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
@@ -56,7 +56,6 @@
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
-import java.util.List;
import java.util.Random;
@RunWith(DevSdkIgnoreRunner.class)
@@ -533,40 +532,6 @@
assertEquals(512L + 4096L, stats.getTotalBytes());
}
- @Test
- public void testBuilder() {
- final NetworkStatsHistory.Entry entry1 = new NetworkStatsHistory.Entry(10, 30, 40,
- 4, 50, 5, 60);
- final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(30, 15, 3,
- 41, 7, 1, 0);
- final NetworkStatsHistory.Entry entry3 = new NetworkStatsHistory.Entry(7, 301, 11,
- 14, 31, 2, 80);
-
- final NetworkStatsHistory statsEmpty = new NetworkStatsHistory
- .Builder(HOUR_IN_MILLIS, 10).build();
- assertEquals(0, statsEmpty.getEntries().size());
- assertEquals(HOUR_IN_MILLIS, statsEmpty.getBucketDuration());
-
- NetworkStatsHistory statsSingle = new NetworkStatsHistory
- .Builder(HOUR_IN_MILLIS, 8)
- .addEntry(entry1)
- .build();
- assertEquals(1, statsSingle.getEntries().size());
- assertEquals(HOUR_IN_MILLIS, statsSingle.getBucketDuration());
- assertEquals(entry1, statsSingle.getEntries().get(0));
-
- NetworkStatsHistory statsMultiple = new NetworkStatsHistory
- .Builder(SECOND_IN_MILLIS, 0)
- .addEntry(entry1).addEntry(entry2).addEntry(entry3)
- .build();
- final List<NetworkStatsHistory.Entry> entries = statsMultiple.getEntries();
- assertEquals(3, entries.size());
- assertEquals(SECOND_IN_MILLIS, statsMultiple.getBucketDuration());
- assertEquals(entry1, entries.get(0));
- assertEquals(entry2, entries.get(1));
- assertEquals(entry3, entries.get(2));
- }
-
private static void assertIndexBeforeAfter(
NetworkStatsHistory stats, int before, int after, long time) {
assertEquals("unexpected before", before, stats.getIndexBefore(time));
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt
index 453612f..abd1825 100644
--- a/tests/unit/java/android/net/NetworkTemplateTest.kt
+++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -29,12 +29,8 @@
import android.net.NetworkStats.METERED_NO
import android.net.NetworkStats.METERED_YES
import android.net.NetworkStats.ROAMING_ALL
-import android.net.NetworkTemplate.MATCH_BLUETOOTH
-import android.net.NetworkTemplate.MATCH_CARRIER
-import android.net.NetworkTemplate.MATCH_ETHERNET
import android.net.NetworkTemplate.MATCH_MOBILE
import android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD
-import android.net.NetworkTemplate.MATCH_PROXY
import android.net.NetworkTemplate.MATCH_WIFI
import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD
import android.net.NetworkTemplate.NETWORK_TYPE_ALL
@@ -52,11 +48,9 @@
import android.net.wifi.WifiInfo
import android.os.Build
import android.telephony.TelephonyManager
-import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL
import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
-import com.android.testutils.SC_V2
import com.android.testutils.assertParcelSane
import org.junit.Before
import org.junit.Test
@@ -65,7 +59,6 @@
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
@@ -555,140 +548,4 @@
it.assertMatches(identMobileImsi3)
}
}
-
- @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
- @Test
- fun testBuilderMatchRules() {
- // Verify unknown match rules cannot construct templates.
- listOf(Integer.MIN_VALUE, -1, Integer.MAX_VALUE).forEach {
- assertFailsWith<IllegalArgumentException> {
- NetworkTemplate.Builder(it).build()
- }
- }
-
- // Verify hidden match rules cannot construct templates.
- listOf(MATCH_WIFI_WILDCARD, MATCH_MOBILE_WILDCARD, MATCH_PROXY).forEach {
- assertFailsWith<IllegalArgumentException> {
- NetworkTemplate.Builder(it).build()
- }
- }
-
- // Verify template which matches metered cellular and carrier networks with
- // the given IMSI. See buildTemplateMobileAll and buildTemplateCarrierMetered.
- listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
- NetworkTemplate.Builder(matchRule).setSubscriberIds(setOf(TEST_IMSI1))
- .setMeteredness(METERED_YES).build().let {
- val expectedTemplate = NetworkTemplate(matchRule, TEST_IMSI1,
- arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
- ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
- OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
- assertEquals(expectedTemplate, it)
- }
- }
-
- // Verify carrier template cannot be created without IMSI.
- assertFailsWith<IllegalArgumentException> {
- NetworkTemplate.Builder(MATCH_CARRIER).build()
- }
-
- // Verify template which matches metered cellular networks,
- // regardless of IMSI. See buildTemplateMobileWildcard.
- NetworkTemplate.Builder(MATCH_MOBILE).setMeteredness(METERED_YES).build().let {
- val expectedTemplate = NetworkTemplate(MATCH_MOBILE_WILDCARD, null /*subscriberId*/,
- null /*subscriberIds*/, arrayOf<String>(),
- METERED_YES, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
- OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
- assertEquals(expectedTemplate, it)
- }
-
- // Verify template which matches metered cellular networks and ratType.
- // See NetworkTemplate#buildTemplateMobileWithRatType.
- NetworkTemplate.Builder(MATCH_MOBILE).setSubscriberIds(setOf(TEST_IMSI1))
- .setMeteredness(METERED_YES).setRatType(TelephonyManager.NETWORK_TYPE_UMTS)
- .build().let {
- val expectedTemplate = NetworkTemplate(MATCH_MOBILE, TEST_IMSI1,
- arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
- ROAMING_ALL, DEFAULT_NETWORK_ALL, TelephonyManager.NETWORK_TYPE_UMTS,
- OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
- assertEquals(expectedTemplate, it)
- }
-
- // Verify template which matches all wifi networks,
- // regardless of Wifi Network Key. See buildTemplateWifiWildcard and buildTemplateWifi.
- NetworkTemplate.Builder(MATCH_WIFI).build().let {
- val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
- null /*subscriberIds*/, arrayOf<String>(),
- METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
- OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
- assertEquals(expectedTemplate, it)
- }
-
- // Verify template which matches wifi networks with the given Wifi Network Key.
- // See buildTemplateWifi(wifiNetworkKey).
- NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
- val expectedTemplate = NetworkTemplate(MATCH_WIFI, null /*subscriberId*/,
- null /*subscriberIds*/, arrayOf(TEST_WIFI_KEY1),
- METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
- OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
- assertEquals(expectedTemplate, it)
- }
-
- // Verify template which matches all wifi networks with the
- // given Wifi Network Key, and IMSI. See buildTemplateWifi(wifiNetworkKey, subscriberId).
- NetworkTemplate.Builder(MATCH_WIFI).setSubscriberIds(setOf(TEST_IMSI1))
- .setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
- val expectedTemplate = NetworkTemplate(MATCH_WIFI, TEST_IMSI1,
- arrayOf(TEST_IMSI1), arrayOf(TEST_WIFI_KEY1),
- METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
- OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
- assertEquals(expectedTemplate, it)
- }
-
- // Verify template which matches ethernet and bluetooth networks.
- // See buildTemplateEthernet and buildTemplateBluetooth.
- listOf(MATCH_ETHERNET, MATCH_BLUETOOTH).forEach { matchRule ->
- NetworkTemplate.Builder(matchRule).build().let {
- val expectedTemplate = NetworkTemplate(matchRule, null /*subscriberId*/,
- null /*subscriberIds*/, arrayOf<String>(),
- METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
- OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
- assertEquals(expectedTemplate, it)
- }
- }
- }
-
- @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
- @Test
- fun testBuilderWifiNetworkKeys() {
- // Verify template builder which generates same template with the given different
- // sequence keys.
- NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
- setOf(TEST_WIFI_KEY1, TEST_WIFI_KEY2)).build().let {
- val expectedTemplate = NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
- setOf(TEST_WIFI_KEY2, TEST_WIFI_KEY1)).build()
- assertEquals(expectedTemplate, it)
- }
-
- // Verify template which matches non-wifi networks with the given key is invalid.
- listOf(MATCH_MOBILE, MATCH_CARRIER, MATCH_ETHERNET, MATCH_BLUETOOTH, -1,
- Integer.MAX_VALUE).forEach { matchRule ->
- assertFailsWith<IllegalArgumentException> {
- NetworkTemplate.Builder(matchRule).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build()
- }
- }
-
- // Verify template which matches wifi networks with the given null key is invalid.
- assertFailsWith<IllegalArgumentException> {
- NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(null)).build()
- }
-
- // Verify template which matches wifi wildcard with the given empty key set.
- NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf<String>()).build().let {
- val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
- arrayOf<String>() /*subscriberIds*/, arrayOf<String>(),
- METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
- OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
- assertEquals(expectedTemplate, it)
- }
- }
}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 37df4eb..6316c72 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -16,6 +16,7 @@
package com.android.server;
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
import static android.Manifest.permission.CHANGE_NETWORK_STATE;
import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
import static android.Manifest.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE;
@@ -269,6 +270,7 @@
import android.net.RouteInfoParcel;
import android.net.SocketKeepalive;
import android.net.TelephonyNetworkSpecifier;
+import android.net.TetheringManager;
import android.net.TransportInfo;
import android.net.UidRange;
import android.net.UidRangeParcel;
@@ -545,6 +547,7 @@
@Mock PacProxyManager mPacProxyManager;
@Mock BpfNetMaps mBpfNetMaps;
@Mock CarrierPrivilegeAuthenticator mCarrierPrivilegeAuthenticator;
+ @Mock TetheringManager mTetheringManager;
// BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the
// underlying binder calls.
@@ -665,6 +668,7 @@
if (Context.NETWORK_STATS_SERVICE.equals(name)) return mStatsManager;
if (Context.BATTERY_STATS_SERVICE.equals(name)) return mBatteryStatsManager;
if (Context.PAC_PROXY_SERVICE.equals(name)) return mPacProxyManager;
+ if (Context.TETHERING_SERVICE.equals(name)) return mTetheringManager;
return super.getSystemService(name);
}
@@ -15771,4 +15775,36 @@
mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), false);
mDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
}
+
+ @Test
+ public void testLegacyTetheringApiGuardWithProperPermission() throws Exception {
+ final String testIface = "test0";
+ mServiceContext.setPermission(ACCESS_NETWORK_STATE, PERMISSION_DENIED);
+ assertThrows(SecurityException.class, () -> mService.getLastTetherError(testIface));
+ assertThrows(SecurityException.class, () -> mService.getTetherableIfaces());
+ assertThrows(SecurityException.class, () -> mService.getTetheredIfaces());
+ assertThrows(SecurityException.class, () -> mService.getTetheringErroredIfaces());
+ assertThrows(SecurityException.class, () -> mService.getTetherableUsbRegexs());
+ assertThrows(SecurityException.class, () -> mService.getTetherableWifiRegexs());
+
+ withPermission(ACCESS_NETWORK_STATE, () -> {
+ mService.getLastTetherError(testIface);
+ verify(mTetheringManager).getLastTetherError(testIface);
+
+ mService.getTetherableIfaces();
+ verify(mTetheringManager).getTetherableIfaces();
+
+ mService.getTetheredIfaces();
+ verify(mTetheringManager).getTetheredIfaces();
+
+ mService.getTetheringErroredIfaces();
+ verify(mTetheringManager).getTetheringErroredIfaces();
+
+ mService.getTetherableUsbRegexs();
+ verify(mTetheringManager).getTetherableUsbRegexs();
+
+ mService.getTetherableWifiRegexs();
+ verify(mTetheringManager).getTetherableWifiRegexs();
+ });
+ }
}