diff --git a/framework-t/Android.bp b/framework-t/Android.bp index f46d887f78..1ce96eed54 100644 --- a/framework-t/Android.bp +++ b/framework-t/Android.bp @@ -114,7 +114,7 @@ java_sdk_library { // 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 +131,10 @@ java_sdk_library { "//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/nearby/.gitignore b/nearby/.gitignore new file mode 100644 index 0000000000..4402b3d52a --- /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/PREUPLOAD.cfg b/nearby/PREUPLOAD.cfg new file mode 100644 index 0000000000..048ddb6a0b --- /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 0000000000..6925dc4be9 --- /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 +$ cd packages/modules/Nearby +$ aidegen . +# This will launch Intellij project for Nearby module. +``` + +## Build and Install + +```sh +$ source build/envsetup.sh && lunch +$ 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 0000000000..dbaca33b81 --- /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 0000000000..d7f063a662 --- /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 0000000000..826f54ffda --- /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 0000000000..b91d25947d --- /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 0000000000..e223b54fc9 --- /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 0000000000..cc94308d76 --- /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 0000000000..3273ea1b0d --- /dev/null +++ b/nearby/framework/java/android/nearby/BroadcastRequest.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.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 { + } + + /** + * The medium where the broadcast request should be sent. + * + * @hide + */ + @IntDef({Medium.BLE, Medium.MDNS}) + public @interface Medium { + int BLE = 1; + int MDNS = 2; + } + + /** + * 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 mMediums; + + BroadcastRequest(@BroadcastType int type, @BroadcastVersion int version, int txPower, + @Medium List 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. + */ + @NonNull + @Medium + public List 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 0000000000..818f8d5c4f --- /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 0000000000..4a2ff6dbcf --- /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 CREATOR = + new Creator() { + @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 0000000000..d2049d1509 --- /dev/null +++ b/nearby/framework/java/android/nearby/CredentialElement.java @@ -0,0 +1,90 @@ +/* + * 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 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 CREATOR = + new Parcelable.Creator() { + @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; + } +} diff --git a/nearby/framework/java/android/nearby/DataElement.java b/nearby/framework/java/android/nearby/DataElement.java new file mode 100644 index 0000000000..6fa5fb58f2 --- /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 CREATOR = new Creator() { + @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 0000000000..160ad7587a --- /dev/null +++ b/nearby/framework/java/android/nearby/FastPairAccountKeyDeviceMetadata.java @@ -0,0 +1,196 @@ +/* + * 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.annotation.SystemApi; +import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel; + +/** + * Class for metadata of a Fast Pair device associated with an account. + * + * @hide + */ +@SystemApi +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 + */ + @SystemApi + @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 + */ + @SystemApi + @Nullable + public byte[] getSha256DeviceAccountKeyPublicAddress() { + return mMetadataParcel.sha256DeviceAccountKeyPublicAddress; + } + + /** + * Get metadata of a Fast Pair device type. + * + * @hide + */ + @SystemApi + @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 + */ + @SystemApi + @Nullable + public FastPairDiscoveryItem getFastPairDiscoveryItem() { + if (mMetadataParcel.discoveryItem == null) { + return null; + } + return new FastPairDiscoveryItem(mMetadataParcel.discoveryItem); + } + + /** + * Builder used to create FastPairAccountKeyDeviceMetadata. + * + * @hide + */ + @SystemApi + public static final class Builder { + + private final FastPairAccountKeyDeviceMetadataParcel mBuilderParcel; + + /** + * Default constructor of Builder. + * + * @hide + */ + @SystemApi + 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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 0000000000..1837671206 --- /dev/null +++ b/nearby/framework/java/android/nearby/FastPairAntispoofKeyDeviceMetadata.java @@ -0,0 +1,128 @@ +/* + * 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.annotation.SystemApi; +import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel; + +/** + * Class for a type of registered Fast Pair device keyed by modelID, or antispoofKey. + * + * @hide + */ +@SystemApi +public class FastPairAntispoofKeyDeviceMetadata { + + FastPairAntispoofKeyDeviceMetadataParcel mMetadataParcel; + FastPairAntispoofKeyDeviceMetadata( + FastPairAntispoofKeyDeviceMetadataParcel metadataParcel) { + this.mMetadataParcel = metadataParcel; + } + + /** + * Get Antispoof public key. + * + * @hide + */ + @SystemApi + @Nullable + public byte[] getAntispoofPublicKey() { + return this.mMetadataParcel.antispoofPublicKey; + } + + /** + * Get metadata of a Fast Pair device type. + * + * @hide + */ + @SystemApi + @Nullable + public FastPairDeviceMetadata getFastPairDeviceMetadata() { + if (this.mMetadataParcel.deviceMetadata == null) { + return null; + } + return new FastPairDeviceMetadata(this.mMetadataParcel.deviceMetadata); + } + + /** + * Builder used to create FastPairAntispoofkeyDeviceMetadata. + * + * @hide + */ + @SystemApi + public static final class Builder { + + private final FastPairAntispoofKeyDeviceMetadataParcel mBuilderParcel; + + /** + * Default constructor of Builder. + * + * @hide + */ + @SystemApi + 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 Data Format. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @NonNull + public FastPairAntispoofKeyDeviceMetadata build() { + return new FastPairAntispoofKeyDeviceMetadata(mBuilderParcel); + } + } +} diff --git a/nearby/framework/java/android/nearby/FastPairClient.java b/nearby/framework/java/android/nearby/FastPairClient.java new file mode 100644 index 0000000000..dc70d71133 --- /dev/null +++ b/nearby/framework/java/android/nearby/FastPairClient.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nearby; + +import android.annotation.BinderThread; +import android.content.Context; +import android.nearby.aidl.IFastPairClient; +import android.nearby.aidl.IFastPairStatusCallback; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import java.lang.ref.WeakReference; + +/** + * 0p API for controlling Fast Pair. It communicates between main thread and service. + * + * @hide + */ +public class FastPairClient { + + private static final String TAG = "FastPairClient"; + private final IBinder mBinder; + private final WeakReference mWeakContext; + IFastPairClient mFastPairClient; + PairStatusCallbackIBinder mPairStatusCallbackIBinder; + + /** + * The Ibinder instance should be from + * {@link com.android.server.nearby.fastpair.halfsheet.FastPairService} so that the client can + * talk with the service. + */ + public FastPairClient(Context context, IBinder binder) { + mBinder = binder; + mFastPairClient = IFastPairClient.Stub.asInterface(mBinder); + mWeakContext = new WeakReference<>(context); + } + + /** + * Registers a callback at service to get UI updates. + */ + public void registerHalfSheet(FastPairStatusCallback fastPairStatusCallback) { + if (mPairStatusCallbackIBinder != null) { + return; + } + mPairStatusCallbackIBinder = new PairStatusCallbackIBinder(fastPairStatusCallback); + try { + mFastPairClient.registerHalfSheet(mPairStatusCallbackIBinder); + } catch (RemoteException e) { + Log.w(TAG, "Failed to register fastPairStatusCallback", e); + } + } + + /** + * Pairs the device at service. + */ + public void connect(FastPairDevice fastPairDevice) { + try { + mFastPairClient.connect(fastPairDevice); + } catch (RemoteException e) { + Log.w(TAG, "Failed to connect Fast Pair device" + fastPairDevice, e); + } + } + + private class PairStatusCallbackIBinder extends IFastPairStatusCallback.Stub { + private final FastPairStatusCallback mStatusCallback; + + private PairStatusCallbackIBinder(FastPairStatusCallback fastPairStatusCallback) { + mStatusCallback = fastPairStatusCallback; + } + + @BinderThread + @Override + public synchronized void onPairUpdate(FastPairDevice fastPairDevice, + PairStatusMetadata pairStatusMetadata) { + Context context = mWeakContext.get(); + if (context != null) { + Handler handler = new Handler(context.getMainLooper()); + handler.post(() -> + mStatusCallback.onPairUpdate(fastPairDevice, pairStatusMetadata)); + } + } + } +} diff --git a/nearby/framework/java/android/nearby/FastPairDataProviderService.java b/nearby/framework/java/android/nearby/FastPairDataProviderService.java new file mode 100644 index 0000000000..c6a1a650f7 --- /dev/null +++ b/nearby/framework/java/android/nearby/FastPairDataProviderService.java @@ -0,0 +1,761 @@ +/* + * 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.annotation.SystemApi; +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. + * + *

Fast Pair providers are identified by their UID / package name. + * + * @hide + */ +@SystemApi +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 + */ + @SystemApi + 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 + */ + @SystemApi + public static final int MANAGE_REQUEST_ADD = 0; + + /** + * Manage request type to remove, or opt-out. + * + * @hide + */ + @SystemApi + 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 + */ + @SystemApi + public static final int ERROR_CODE_BAD_REQUEST = 0; + + /** + * Error code for internal error. + * + * @hide + */ + @SystemApi + 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 + */ + @SystemApi + 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 + */ + @SystemApi + public interface FastPairAntispoofKeyDeviceMetadataCallback { + + /** + * Invoked once the meta data is loaded. + * + * @hide + */ + @SystemApi + void onFastPairAntispoofKeyDeviceMetadataReceived( + @NonNull FastPairAntispoofKeyDeviceMetadata metadata); + + /** Invoked in case of error. + * + * @hide + */ + @SystemApi + void onError(@ErrorCode int code, @Nullable String message); + } + + /** + * Callback to be invoked when Fast Pair devices of a given account is loaded. + * + * @hide + */ + @SystemApi + public interface FastPairAccountDevicesMetadataCallback { + + /** + * Should be invoked once the metadatas are loaded. + * + * @hide + */ + @SystemApi + void onFastPairAccountDevicesMetadataReceived( + @NonNull Collection metadatas); + /** + * Invoked in case of error. + * + * @hide + */ + @SystemApi + void onError(@ErrorCode int code, @Nullable String message); + } + + /** + * Callback to be invoked when FastPair eligible accounts are loaded. + * + * @hide + */ + @SystemApi + public interface FastPairEligibleAccountsCallback { + + /** + * Should be invoked once the eligible accounts are loaded. + * + * @hide + */ + @SystemApi + void onFastPairEligibleAccountsReceived( + @NonNull Collection accounts); + /** + * Invoked in case of error. + * + * @hide + */ + @SystemApi + void onError(@ErrorCode int code, @Nullable String message); + } + + /** + * Callback to be invoked when a management action is finished. + * + * @hide + */ + @SystemApi + public interface FastPairManageActionCallback { + + /** + * Should be invoked once the manage action is successful. + * + * @hide + */ + @SystemApi + void onSuccess(); + /** + * Invoked in case of error. + * + * @hide + */ + @SystemApi + 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 + */ + @SystemApi + 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 + */ + @SystemApi + 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 + */ + @SystemApi + 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 + */ + @SystemApi + 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 + */ + @SystemApi + 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 + */ + @SystemApi + 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 + * Model ID. + * @return raw bytes of modelId in the same format returned by Google at device registration + * time. + * @hide + */ + @SystemApi + 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 + */ + @SystemApi + 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 + */ + @SystemApi + 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 + */ + @SystemApi + public @NonNull Collection getDeviceAccountKeys() { + if (this.mMetadataRequestParcel.deviceAccountKeys == null) { + return new ArrayList(0); + } + List 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 + */ + @SystemApi + 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 + */ + @SystemApi + 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 + */ + @SystemApi + public @ManageRequestType int getRequestType() { + return this.mAccountRequestParcel.requestType; + } + /** + * Get account. + * + * @hide + */ + @SystemApi + 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 + */ + @SystemApi + 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 + */ + @SystemApi + public @ManageRequestType int getRequestType() { + return this.mRequestParcel.requestType; + } + /** + * Get account. + * + * @hide + */ + @SystemApi + public @NonNull Account getAccount() { + return this.mRequestParcel.account; + } + /** + * Get BleAddress. + * + * @hide + */ + @SystemApi + public @Nullable String getBleAddress() { + return this.mRequestParcel.bleAddress; + } + /** + * Get account key device metadata. + * + * @hide + */ + @SystemApi + 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 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 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 0000000000..5942966994 --- /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 0000000000..e12b4f8231 --- /dev/null +++ b/nearby/framework/java/android/nearby/FastPairDevice.java @@ -0,0 +1,299 @@ +/* + * 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 CREATOR = new Creator() { + @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()); + 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]; + } + }; + + // 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 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 mediums, + int rssi, + @Nullable String modelId, + @NonNull String bluetoothAddress, + @Nullable byte[] data) { + super(name, mediums, rssi); + this.mModelId = modelId; + this.mBluetoothAddress = bluetoothAddress; + this.mData = data; + } + + /** + * 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(" 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 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(), 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 mediums = getMediums(); + dest.writeInt(mediums.size()); + for (int medium : mediums) { + dest.writeInt(medium); + } + dest.writeInt(getRssi()); + 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 mMediums; + + @Nullable private String mName; + private int mRssi; + @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 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, 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 0000000000..ea75271b2e --- /dev/null +++ b/nearby/framework/java/android/nearby/FastPairDeviceMetadata.java @@ -0,0 +1,1009 @@ +/* + * 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.annotation.SystemApi; +import android.nearby.aidl.FastPairDeviceMetadataParcel; + +/** + * Class for the properties of a given type of Fast Pair device, including images and text. + * + * @hide + */ +@SystemApi +public class FastPairDeviceMetadata { + + FastPairDeviceMetadataParcel mMetadataParcel; + + FastPairDeviceMetadata( + FastPairDeviceMetadataParcel metadataParcel) { + this.mMetadataParcel = metadataParcel; + } + + /** + * Get ImageUrl, which will be displayed in notification. + * + * @hide + */ + @SystemApi + @Nullable + public String getImageUrl() { + return mMetadataParcel.imageUrl; + } + + /** + * Get IntentUri, which will be launched to install companion app. + * + * @hide + */ + @SystemApi + @Nullable + public String getIntentUri() { + return mMetadataParcel.intentUri; + } + + /** + * Get BLE transmit power, as described in Fast Pair spec, see + * Transmit Power + * + * @hide + */ + @SystemApi + public int getBleTxPower() { + return mMetadataParcel.bleTxPower; + } + + /** + * Get Fast Pair Half Sheet trigger distance in meters. + * + * @hide + */ + @SystemApi + 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 + */ + @SystemApi + @Nullable + public byte[] getImage() { + return mMetadataParcel.image; + } + + /** + * Get Fast Pair device type. + * DEVICE_TYPE_UNSPECIFIED = 0; + * HEADPHONES = 1; + * TRUE_WIRELESS_HEADPHONES = 7; + * @hide + */ + @SystemApi + public int getDeviceType() { + return mMetadataParcel.deviceType; + } + + /** + * Get Fast Pair device name. e.g., "Pixel Buds A-Series". + * + * @hide + */ + @SystemApi + @Nullable + public String getName() { + return mMetadataParcel.name; + } + + /** + * Get true wireless image url for left bud. + * + * @hide + */ + @SystemApi + @Nullable + public String getTrueWirelessImageUrlLeftBud() { + return mMetadataParcel.trueWirelessImageUrlLeftBud; + } + + /** + * Get true wireless image url for right bud. + * + * @hide + */ + @SystemApi + @Nullable + public String getTrueWirelessImageUrlRightBud() { + return mMetadataParcel.trueWirelessImageUrlRightBud; + } + + /** + * Get true wireless image url for case. + * + * @hide + */ + @SystemApi + @Nullable + public String getTrueWirelessImageUrlCase() { + return mMetadataParcel.trueWirelessImageUrlCase; + } + + /** + * Get Locale of the device. + * + * @hide + */ + @SystemApi + @Nullable + public String getLocale() { + return mMetadataParcel.locale; + } + + /** + * Get InitialNotificationDescription, which is a translated string of + * "Tap to pair. Earbuds will be tied to %s" based on locale. + * + * @hide + */ + @SystemApi + @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 + */ + @SystemApi + @Nullable + public String getInitialNotificationDescriptionNoAccount() { + return mMetadataParcel.initialNotificationDescriptionNoAccount; + } + + /** + * Get OpenCompanionAppDescription, which is a translated string of + * "Tap to finish setup" based on locale. + * + * @hide + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @Nullable + public String getConnectSuccessCompanionAppNotInstalled() { + return mMetadataParcel.connectSuccessCompanionAppNotInstalled; + } + + /** + * Get SubsequentPairingDescription, which is a translated string of + * "Connect %s to this phone" based on locale. + * + * @hide + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @Nullable + public String getFailConnectGoToSettingsDescription() { + return mMetadataParcel.failConnectGoToSettingsDescription; + } + + /** + * Get ConfirmPinTitle, which is a translated string of + * based on locale. + * + * @hide + */ + @SystemApi + @Nullable + public String getConfirmPinTitle() { + return mMetadataParcel.confirmPinTitle; + } + + /** + * Get ConfirmPinDescription, which is a translated string of "confirm pin" based on locale. + * + * @hide + */ + @SystemApi + @Nullable + public String getConfirmPinDescription() { + return mMetadataParcel.confirmPinDescription; + } + + /** + * Get SyncContactsTitle, which is a translated string of "sync contacts title" based on locale. + * + * @hide + */ + @SystemApi + @Nullable + public String getSyncContactsTitle() { + return mMetadataParcel.syncContactsTitle; + } + + /** + * Get SyncContactsDescription, which is a translated string of "sync contacts description" + * based on locale. + * + * @hide + */ + @SystemApi + @Nullable + public String getSyncContactsDescription() { + return mMetadataParcel.syncContactsDescription; + } + + /** + * Get SyncSmsTitle, which is a translated string of "sync sms title" based on locale. + * + * @hide + */ + @SystemApi + @Nullable + public String getSyncSmsTitle() { + return mMetadataParcel.syncSmsTitle; + } + + /** + * Get SyncSmsDescription, which is a translated string of "sync sms description" based on + * locale. + * + * @hide + */ + @SystemApi + @Nullable + public String getSyncSmsDescription() { + return mMetadataParcel.syncSmsDescription; + } + + /** + * Get AssistantSetupHalfSheet, which is a translated string of + * "Tap to set up your Google Assistant" based on locale. + * + * @hide + */ + @SystemApi + @Nullable + public String getAssistantSetupHalfSheet() { + return mMetadataParcel.assistantSetupHalfSheet; + } + + /** + * Get AssistantSetupNotification, which is a translated string of + * "Tap to set up your Google Assistant" based on locale. + * + * @hide + */ + @SystemApi + @Nullable + public String getAssistantSetupNotification() { + return mMetadataParcel.assistantSetupNotification; + } + + /** + * Get FastPairTvConnectDeviceNoAccountDescription, which is a translated string of + * "Select connect to pair your %s with this device" based on locale. + * + * @hide + */ + @SystemApi + @Nullable + public String getFastPairTvConnectDeviceNoAccountDescription() { + return mMetadataParcel.fastPairTvConnectDeviceNoAccountDescription; + } + + /** + * Builder used to create FastPairDeviceMetadata. + * + * @hide + */ + @SystemApi + public static final class Builder { + + private final FastPairDeviceMetadataParcel mBuilderParcel; + + /** + * Default constructor of Builder. + * + * @hide + */ + @SystemApi + 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.locale = 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; + mBuilderParcel.confirmPinTitle = null; + mBuilderParcel.confirmPinDescription = null; + mBuilderParcel.syncContactsTitle = null; + mBuilderParcel.syncContactsDescription = null; + mBuilderParcel.syncSmsTitle = null; + mBuilderParcel.syncSmsDescription = null; + mBuilderParcel.assistantSetupHalfSheet = null; + mBuilderParcel.assistantSetupNotification = null; + mBuilderParcel.fastPairTvConnectDeviceNoAccountDescription = null; + } + + /** + * Set ImageUlr. + * + * @param imageUrl Image Ulr. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @NonNull + public Builder setTrueWirelessImageUrlCase(@Nullable String trueWirelessImageUrlCase) { + mBuilderParcel.trueWirelessImageUrlCase = trueWirelessImageUrlCase; + return this; + } + + /** + * Set Locale. + * + * @param locale Device locale. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setLocale(@Nullable String locale) { + mBuilderParcel.locale = locale; + return this; + } + + /** + * Set InitialNotificationDescription. + * + * @param initialNotificationDescription Initial notification description. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @NonNull + public Builder setFailConnectGoToSettingsDescription( + @Nullable String failConnectGoToSettingsDescription) { + mBuilderParcel.failConnectGoToSettingsDescription = + failConnectGoToSettingsDescription; + return this; + } + + /** + * Set ConfirmPinTitle. + * + * @param confirmPinTitle Title of the UI to ask the user to confirm the pin code. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setConfirmPinTitle(@Nullable String confirmPinTitle) { + mBuilderParcel.confirmPinTitle = confirmPinTitle; + return this; + } + + /** + * Set ConfirmPinDescription. + * + * @param confirmPinDescription Description of the UI to ask the user to confirm the pin + * code. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setConfirmPinDescription(@Nullable String confirmPinDescription) { + mBuilderParcel.confirmPinDescription = confirmPinDescription; + return this; + } + + /** + * Set SyncContactsTitle. + * + * @param syncContactsTitle Title of the UI to ask the user to confirm to sync contacts. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setSyncContactsTitle(@Nullable String syncContactsTitle) { + mBuilderParcel.syncContactsTitle = syncContactsTitle; + return this; + } + + /** + * Set SyncContactsDescription. + * + * @param syncContactsDescription Description of the UI to ask the user to confirm to sync + * contacts. + * @hide + */ + @SystemApi + @NonNull + public Builder setSyncContactsDescription(@Nullable String syncContactsDescription) { + mBuilderParcel.syncContactsDescription = syncContactsDescription; + return this; + } + + /** + * Set SyncSmsTitle. + * + * @param syncSmsTitle Title of the UI to ask the user to confirm to sync SMS. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setSyncSmsTitle(@Nullable String syncSmsTitle) { + mBuilderParcel.syncSmsTitle = syncSmsTitle; + return this; + } + + /** + * Set SyncSmsDescription. + * + * @param syncSmsDescription Description of the UI to ask the user to confirm to sync SMS. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setSyncSmsDescription(@Nullable String syncSmsDescription) { + mBuilderParcel.syncSmsDescription = syncSmsDescription; + return this; + } + + /** + * Set AssistantSetupHalfSheet. + * + * @param assistantSetupHalfSheet Description in half sheet to ask user setup google + * assistant. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setAssistantSetupHalfSheet(@Nullable String assistantSetupHalfSheet) { + mBuilderParcel.assistantSetupHalfSheet = assistantSetupHalfSheet; + return this; + } + + /** + * Set AssistantSetupNotification. + * + * @param assistantSetupNotification Description in notification to ask user setup google + * assistant. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setAssistantSetupNotification( + @Nullable String assistantSetupNotification) { + mBuilderParcel.assistantSetupNotification = assistantSetupNotification; + return this; + } + + /** + * Set FastPairTvConnectDeviceNoAccountDescription. + * + * @param fastPairTvConnectDeviceNoAccountDescription Description of the connect device + * action on TV, when user is not logged + * in. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setFastPairTvConnectDeviceNoAccountDescription( + @Nullable String fastPairTvConnectDeviceNoAccountDescription) { + mBuilderParcel.fastPairTvConnectDeviceNoAccountDescription = + fastPairTvConnectDeviceNoAccountDescription; + return this; + } + + /** + * Build {@link FastPairDeviceMetadata} with the currently set configuration. + * + * @hide + */ + @SystemApi + @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 0000000000..bc6a6f81ae --- /dev/null +++ b/nearby/framework/java/android/nearby/FastPairDiscoveryItem.java @@ -0,0 +1,825 @@ +/* + * 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.annotation.SystemApi; +import android.nearby.aidl.FastPairDiscoveryItemParcel; + +/** + * Class for FastPairDiscoveryItem and its builder. + * + * @hide + */ +@SystemApi +public class FastPairDiscoveryItem { + + FastPairDiscoveryItemParcel mMetadataParcel; + + FastPairDiscoveryItem( + FastPairDiscoveryItemParcel metadataParcel) { + this.mMetadataParcel = metadataParcel; + } + + /** + * Get Id. + * + * @hide + */ + @SystemApi + @Nullable + public String getId() { + return mMetadataParcel.id; + } + + /** + * Get Type. + * + * @hide + */ + @SystemApi + public int getType() { + return mMetadataParcel.type; + } + + /** + * Get MacAddress. + * + * @hide + */ + @SystemApi + @Nullable + public String getMacAddress() { + return mMetadataParcel.macAddress; + } + + /** + * Get ActionUrl. + * + * @hide + */ + @SystemApi + @Nullable + public String getActionUrl() { + return mMetadataParcel.actionUrl; + } + + /** + * Get DeviceName. + * + * @hide + */ + @SystemApi + @Nullable + public String getDeviceName() { + return mMetadataParcel.deviceName; + } + + /** + * Get Title. + * + * @hide + */ + @SystemApi + @Nullable + public String getTitle() { + return mMetadataParcel.title; + } + + /** + * Get Description. + * + * @hide + */ + @SystemApi + @Nullable + public String getDescription() { + return mMetadataParcel.description; + } + + /** + * Get DisplayUrl. + * + * @hide + */ + @SystemApi + @Nullable + public String getDisplayUrl() { + return mMetadataParcel.displayUrl; + } + + /** + * Get LastObservationTimestampMillis. + * + * @hide + */ + @SystemApi + public long getLastObservationTimestampMillis() { + return mMetadataParcel.lastObservationTimestampMillis; + } + + /** + * Get FirstObservationTimestampMillis. + * + * @hide + */ + @SystemApi + public long getFirstObservationTimestampMillis() { + return mMetadataParcel.firstObservationTimestampMillis; + } + + /** + * Get State. + * + * @hide + */ + @SystemApi + public int getState() { + return mMetadataParcel.state; + } + + /** + * Get ActionUrlType. + * + * @hide + */ + @SystemApi + public int getActionUrlType() { + return mMetadataParcel.actionUrlType; + } + + /** + * Get Rssi. + * + * @hide + */ + @SystemApi + public int getRssi() { + return mMetadataParcel.rssi; + } + + /** + * Get PendingAppInstallTimestampMillis. + * + * @hide + */ + @SystemApi + public long getPendingAppInstallTimestampMillis() { + return mMetadataParcel.pendingAppInstallTimestampMillis; + } + + /** + * Get TxPower. + * + * @hide + */ + @SystemApi + public int getTxPower() { + return mMetadataParcel.txPower; + } + + /** + * Get AppName. + * + * @hide + */ + @SystemApi + @Nullable + public String getAppName() { + return mMetadataParcel.appName; + } + + /** + * Get GroupId. + * + * @hide + */ + @SystemApi + @Nullable + public String getGroupId() { + return mMetadataParcel.groupId; + } + + /** + * Get AttachmentType. + * + * @hide + */ + @SystemApi + public int getAttachmentType() { + return mMetadataParcel.attachmentType; + } + + /** + * Get PackageName. + * + * @hide + */ + @SystemApi + @Nullable + public String getPackageName() { + return mMetadataParcel.packageName; + } + + /** + * Get FeatureGraphicUrl. + * + * @hide + */ + @SystemApi + @Nullable + public String getFeatureGraphicUrl() { + return mMetadataParcel.featureGraphicUrl; + } + + /** + * Get TriggerId. + * + * @hide + */ + @SystemApi + @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 + */ + @SystemApi + @Nullable + public byte[] getIconPng() { + return mMetadataParcel.iconPng; + } + + /** + * Get IconFifeUrl. + * + * @hide + */ + @SystemApi + @Nullable + public String getIconFfeUrl() { + return mMetadataParcel.iconFifeUrl; + } + + /** + * Get DebugMessage. + * + * @hide + */ + @SystemApi + @Nullable + public String getDebugMessage() { + return mMetadataParcel.debugMessage; + } + + /** + * Get DebugCategory. + * + * @hide + */ + @SystemApi + public int getDebugCategory() { + return mMetadataParcel.debugCategory; + } + + /** + * Get LostMillis. + */ + public long getLostMillis() { + return mMetadataParcel.lostMillis; + } + + /** + * Get LastUserExperience. + * + * @hide + */ + @SystemApi + public int getLastUserExperience() { + return mMetadataParcel.lastUserExperience; + } + + /** + * Get BleRecordBytes. Raw bytes of {@link android.bluetooth.le.ScanRecord}. + * It is the most recent BLE advertisement related to this item. + * + * @return the most recent BLE advertisement in raw bytes of + * {@link android.bluetooth.le.ScanRecord}. + * @hide + */ + @SystemApi + @Nullable + public byte[] getBleRecordBytes() { + return mMetadataParcel.bleRecordBytes; + } + + /** + * Get EntityId. + * + * @hide + */ + @SystemApi + @Nullable + public String getEntityId() { + return mMetadataParcel.entityId; + } + + /** + * Get authenticationPublicKeySecp256r1, which is same as AntiSpoof public key, see + * Data Format. + * + * @return 64-byte authenticationPublicKeySecp256r1. + * @hide + */ + @SystemApi + @Nullable + public byte[] getAuthenticationPublicKeySecp256r1() { + return mMetadataParcel.authenticationPublicKeySecp256r1; + } + + /** + * Builder used to create FastPairDiscoveryItem. + * + * @hide + */ + @SystemApi + public static final class Builder { + + private final FastPairDiscoveryItemParcel mBuilderParcel; + + /** + * Default constructor of Builder. + * + * @hide + */ + @SystemApi + public Builder() { + mBuilderParcel = new FastPairDiscoveryItemParcel(); + } + + /** + * Set Id. + * + * @param id Unique id. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * + * @hide + */ + @SystemApi + @NonNull + public Builder setId(@Nullable String id) { + mBuilderParcel.id = id; + return this; + } + + /** + * Set Nearby Type. + * + * @param type Nearby type. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setType(int type) { + mBuilderParcel.type = type; + return this; + } + + /** + * Set MacAddress. + * + * @param macAddress Fast Pair device rotating mac address. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @NonNull + public Builder setAppName(@Nullable String appName) { + mBuilderParcel.appName = appName; + return this; + } + + /** + * Set GroupId. + * + * @param groupId ID used for associating several DiscoveryItems. These items may be + * visually displayed together. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setGroupId(@Nullable String groupId) { + mBuilderParcel.groupId = groupId; + return this; + } + + /** + * Set AttachmentType. + * + * @param attachmentType Whether the attachment is created in debug namespace. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setAttachmentType(int attachmentType) { + mBuilderParcel.attachmentType = attachmentType; + 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 + */ + @SystemApi + @NonNull + public Builder setPackageName(@Nullable String packageName) { + mBuilderParcel.packageName = packageName; + return this; + } + + /** + * Set FeatureGraphicUrl. + * + * @param featureGraphicUrl The "feature" graphic image url used for large sized list view + * entries. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setFeatureGraphicUrl(@Nullable String featureGraphicUrl) { + mBuilderParcel.featureGraphicUrl = featureGraphicUrl; + 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 + */ + @SystemApi + @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 + */ + @SystemApi + @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 + */ + @SystemApi + @NonNull + public Builder setIconFfeUrl(@Nullable String iconFifeUrl) { + mBuilderParcel.iconFifeUrl = iconFifeUrl; + return this; + } + + /** + * Set DebugMessage. + * + * @param debugMessage Message written to bugreport for 3P developers.(No sensitive info) + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setDebugMessage(@Nullable String debugMessage) { + mBuilderParcel.debugMessage = debugMessage; + return this; + } + + /** + * Set DebugCategory. + * + * @param debugCategory Weather the item is filtered out on server. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setDebugCategory(int debugCategory) { + mBuilderParcel.debugCategory = debugCategory; + return this; + } + + /** + * Set LostMillis. + * + * @param lostMillis Client timestamp when the trigger (e.g. beacon) was last lost + * (e.g. when Messages told us the beacon's no longer nearby). + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setLostMillis(long lostMillis) { + mBuilderParcel.lostMillis = lostMillis; + return this; + } + + /** + * Set LastUserExperience. + * + * @param lastUserExperience The kind of experience the user last had with this (e.g. if + * they dismissed the notification, that's bad; but if they tapped + * it, that's good). + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setLastUserExperience(int lastUserExperience) { + mBuilderParcel.lastUserExperience = lastUserExperience; + return this; + } + + /** + * Set BleRecordBytes. + * + * @param bleRecordBytes The most recent BLE advertisement related to this item. Raw bytes + * of {@link android.bluetooth.le.ScanRecord}. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setBleRecordBytes(@Nullable byte[] bleRecordBytes) { + mBuilderParcel.bleRecordBytes = bleRecordBytes; + return this; + } + + /** + * Set EntityId. + * + * @param entityId An ID generated on the server to uniquely identify content. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setEntityId(@Nullable String entityId) { + mBuilderParcel.entityId = entityId; + return this; + } + + /** + * Set authenticationPublicKeySecp256r1, which is same as AntiSpoof public key, see + * Data Format + * + * @param authenticationPublicKeySecp256r1 64-byte Fast Pair device public key. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * @hide + */ + @SystemApi + @NonNull + public Builder setAuthenticationPublicKeySecp256r1( + @Nullable byte[] authenticationPublicKeySecp256r1) { + mBuilderParcel.authenticationPublicKeySecp256r1 = authenticationPublicKeySecp256r1; + return this; + } + + /** + * Build {@link FastPairDiscoveryItem} with the currently set configuration. + * + * @hide + */ + @SystemApi + @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 0000000000..e6c3047b84 --- /dev/null +++ b/nearby/framework/java/android/nearby/FastPairEligibleAccount.java @@ -0,0 +1,121 @@ +/* + * 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.annotation.SystemApi; +import android.nearby.aidl.FastPairEligibleAccountParcel; + +/** + * Class for FastPairEligibleAccount and its builder. + * + * @hide + */ +@SystemApi +public class FastPairEligibleAccount { + + FastPairEligibleAccountParcel mAccountParcel; + + FastPairEligibleAccount(FastPairEligibleAccountParcel accountParcel) { + this.mAccountParcel = accountParcel; + } + + /** + * Get Account. + * + * @hide + */ + @SystemApi + @Nullable + public Account getAccount() { + return this.mAccountParcel.account; + } + + /** + * Get OptIn Status. + * + * @hide + */ + @SystemApi + public boolean isOptIn() { + return this.mAccountParcel.optIn; + } + + /** + * Builder used to create FastPairEligibleAccount. + * + * @hide + */ + @SystemApi + public static final class Builder { + + private final FastPairEligibleAccountParcel mBuilderParcel; + + /** + * Default constructor of Builder. + * + * @hide + */ + @SystemApi + 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 + */ + @SystemApi + @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 + */ + @SystemApi + @NonNull + public Builder setOptIn(boolean optIn) { + mBuilderParcel.optIn = optIn; + return this; + } + + /** + * Build {@link FastPairEligibleAccount} with the currently set configuration. + * + * @hide + */ + @SystemApi + @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 0000000000..1567828695 --- /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 0000000000..98c7e173ff --- /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 0000000000..2e6fc87926 --- /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 0000000000..62e109ead4 --- /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 0000000000..54033aa5ed --- /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 0000000000..538940cd3e --- /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 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 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 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 0000000000..1a881813bc --- /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 0000000000..f137de4794 --- /dev/null +++ b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java @@ -0,0 +1,343 @@ +/* + * 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.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. + * + *

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 CREATOR = + new Creator() { + @Override + public NearbyDeviceParcelable createFromParcel(Parcel in) { + Builder builder = new Builder(); + if (in.readInt() == 1) { + builder.setName(in.readString()); + } + builder.setMedium(in.readInt()); + builder.setRssi(in.readInt()); + 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 mRssi; + + @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 rssi, @Nullable String fastPairModelId, + @Nullable String bluetoothAddress, @Nullable byte[] data) { + mScanType = scanType; + mName = name; + mMedium = medium; + mRssi = rssi; + 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(mRssi); + 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) + + ", rssi=" + mRssi + + ", 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) + && (mRssi == otherNearbyDeviceParcelable.mRssi) + && (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, 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 received signal strength in dBm. + */ + @IntRange(from = -127, to = 126) + public int getRssi() { + return mRssi; + } + + /** + * 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 mRssi; + @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 RSSI between scanned device and the discovered device. + * + * @param rssi The received signal strength in dBm. + */ + @NonNull + public Builder setRssi(int rssi) { + mRssi = rssi; + 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, mRssi, 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 0000000000..3780fbbce4 --- /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 0000000000..2654046f9f --- /dev/null +++ b/nearby/framework/java/android/nearby/NearbyManager.java @@ -0,0 +1,355 @@ +/* + * 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. + * + *

To get a {@link NearbyManager} instance, call the + * Context.getSystemService(NearbyManager.class). + * + * @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> + sScanListeners = new WeakHashMap<>(); + @GuardedBy("sBroadcastListeners") + private static final WeakHashMap> + 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()) + .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 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 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 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 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 + * @param def the default value if no setting value + * @return whether the Fast Pair is enabled + */ + public static boolean getFastPairScanEnabled(@NonNull Context context, boolean def) { + final int enabled = Settings.Secure.getInt( + context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, (def ? 1 : 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 + */ + 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(() -> mScanCallback.onDiscovered( + toClientNearbyDevice(nearbyDeviceParcelable, mScanType))); + } + + @Override + public void onUpdated(NearbyDeviceParcelable nearbyDeviceParcelable) + throws RemoteException { + mExecutor.execute( + () -> mScanCallback.onUpdated( + toClientNearbyDevice(nearbyDeviceParcelable, mScanType))); + } + + @Override + public void onLost(NearbyDeviceParcelable nearbyDeviceParcelable) throws RemoteException { + mExecutor.execute( + () -> 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 0000000000..911a30089f --- /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 0000000000..438cd6b46f --- /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 CREATOR = new Creator() { + @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 0000000000..d01be065b8 --- /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 mActions; + private final PrivateCredential mCredential; + private final List mExtendedProperties; + + private PresenceBroadcastRequest(@BroadcastVersion int version, int txPower, + List mediums, byte[] salt, List actions, + PrivateCredential credential, List 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 CREATOR = + new Creator() { + @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 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 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 mMediums; + private final List mActions; + private final List mExtendedProperties; + private final byte[] mSalt; + private final PrivateCredential mCredential; + + private int mVersion; + private int mTxPower; + + public Builder(@NonNull List 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 0000000000..7ad6ae9a7f --- /dev/null +++ b/nearby/framework/java/android/nearby/PresenceCredential.java @@ -0,0 +1,160 @@ +/* + * 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.List; + +/** + * 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; + /** + * Public identity type. + */ + public static final int IDENTITY_TYPE_PUBLIC = 4; + + /** @hide **/ + @Retention(RetentionPolicy.SOURCE) + @IntDef({IDENTITY_TYPE_UNKNOWN, IDENTITY_TYPE_PRIVATE, IDENTITY_TYPE_PROVISIONED, + IDENTITY_TYPE_TRUSTED, IDENTITY_TYPE_PUBLIC}) + public @interface IdentityType { + } + + private final @CredentialType int mType; + private final @IdentityType int mIdentityType; + private final byte[] mSecretId; + private final byte[] mAuthenticityKey; + private final List mCredentialElements; + + PresenceCredential(@CredentialType int type, @IdentityType int identityType, + byte[] secretId, byte[] authenticityKey, List 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 getCredentialElements() { + return mCredentialElements; + } + + /** + * 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 0000000000..12fc2a3e03 --- /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 mExtendedProperties; + + /** + * The id of the device. + * + *

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 getExtendedProperties() { + return mExtendedProperties; + } + + private PresenceDevice(String deviceName, List mMediums, int rssi, String deviceId, + byte[] salt, byte[] secretId, byte[] encryptedIdentity, int deviceType, + String deviceImageUrl, long discoveryTimestampMillis, + List 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 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 CREATOR = new Creator() { + @Override + public PresenceDevice createFromParcel(Parcel in) { + String name = null; + if (in.readInt() == 1) { + name = in.readString(); + } + int size = in.readInt(); + List 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 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 mExtendedProperties; + private final List 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 0000000000..f0c3c0672f --- /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 mCredentials; + private final List mPresenceActions; + private final List mExtendedProperties; + + /** + * A list of credentials to filter on. + */ + @NonNull + public List getCredentials() { + return mCredentials; + } + + /** + * A list of presence actions for matching. + */ + @NonNull + public List getPresenceActions() { + return mPresenceActions; + } + + /** + * A bundle of extended properties for matching. + */ + @NonNull + public List getExtendedProperties() { + return mExtendedProperties; + } + + private PresenceScanFilter(int rssiThreshold, List credentials, + List presenceActions, List 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 CREATOR = new Creator() { + @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 mCredentials; + private final Set mPresenceIdentities; + private final Set mPresenceActions; + private final List 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 0000000000..d915cc6f3f --- /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 CREATOR = new Creator() { + @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 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 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 0000000000..8aac323bf0 --- /dev/null +++ b/nearby/framework/java/android/nearby/PublicCredential.java @@ -0,0 +1,184 @@ +/* + * 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; +import java.util.Objects; + +/** + * Represents a public credential. + * + * @hide + */ +@SystemApi +public final class PublicCredential extends PresenceCredential implements Parcelable { + @NonNull + public static final Creator CREATOR = new Creator() { + @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 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 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 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 0000000000..1b1b4bca2d --- /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 0000000000..1409426a1c --- /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 0000000000..438dfed9ca --- /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 0000000000..a90b72d899 --- /dev/null +++ b/nearby/framework/java/android/nearby/ScanRequest.java @@ -0,0 +1,366 @@ +/* + * 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 share protocol. */ + public static final int SCAN_TYPE_NEARBY_SHARE = 2; + /** Scan type for scanning devices using nearby presence protocol. */ + public static final int SCAN_TYPE_NEARBY_PRESENCE = 3; + /** Scan type for scanning devices using exposure notification protocol. */ + public static final int SCAN_TYPE_EXPOSURE_NOTIFICATION = 4; + + /** 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 CREATOR = new Creator() { + @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 mScanFilters; + + private ScanRequest(@ScanType int scanType, @ScanMode int scanMode, boolean bleEnabled, + @NonNull WorkSource workSource, List 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_SHARE + || scanType == SCAN_TYPE_NEARBY_PRESENCE + || scanType == SCAN_TYPE_EXPOSURE_NOTIFICATION; + } + + /** + * 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 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_SHARE, SCAN_TYPE_NEARBY_PRESENCE, + SCAN_TYPE_EXPOSURE_NOTIFICATION}) + 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 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. + * + *

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. + * + *

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 0000000000..53c73bdf6e --- /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 0000000000..fc3ba22e08 --- /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 0000000000..80143232af --- /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 0000000000..4fd4d4b83f --- /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 0000000000..afdcf154aa --- /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 0000000000..ef00321328 --- /dev/null +++ b/nearby/framework/java/android/nearby/aidl/FastPairDeviceMetadataParcel.aidl @@ -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 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; + + // Stings to be displayed in notification surfaced for a device. + // The locale of all of the Strings. + String locale; + + // 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; + + // The title of the UI to ask the user to confirm the pin code. + String confirmPinTitle; + + // The description of the UI to ask the user to confirm the pin code. + String confirmPinDescription; + + // The title of the UI to ask the user to confirm to sync contacts. + String syncContactsTitle; + + // The description of the UI to ask the user to confirm to sync contacts. + String syncContactsDescription; + + // The title of the UI to ask the user to confirm to sync SMS. + String syncSmsTitle; + + // The description of the UI to ask the user to confirm to sync SMS. + String syncSmsDescription; + + // The description in half sheet to ask user setup google assistant + String assistantSetupHalfSheet; + + // The description in notification to ask user setup google assistant + String assistantSetupNotification; + + // Description of the connect device action on TV, when user is not logged in. + String fastPairTvConnectDeviceNoAccountDescription; +} \ 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 0000000000..5ba18bb8db --- /dev/null +++ b/nearby/framework/java/android/nearby/aidl/FastPairDiscoveryItemParcel.aidl @@ -0,0 +1,126 @@ +/* + * 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; + + int type; + + // 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; + + // ID used for associating several DiscoveryItems. These items may be + // visually displayed together. + String groupId; + + // Whether the attachment is created in debug namespace + int attachmentType; + + // Package name of the App that owns this item. + String packageName; + + // The "feature" graphic image url used for large sized list view entries. + String featureGraphicUrl; + + // 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; + + // Message written to bugreport for 3P developers.(No sensitive info) + // null if the item is valid + String debugMessage; + + // Weather the item is filtered out on server. + int debugCategory; + + // Client timestamp when the trigger (e.g. beacon) was last lost (e.g. when + // Messages told us the beacon's no longer nearby). + long lostMillis; + + // The kind of experience the user last had with this (e.g. if they dismissed + // the notification, that's bad; but if they tapped it, that's good). + int lastUserExperience; + + // The most recent BLE advertisement related to this item. + byte[] bleRecordBytes; + + // An ID generated on the server to uniquely identify content. + String entityId; + + // 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 0000000000..747758d032 --- /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 0000000000..8db3356024 --- /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 0000000000..82cf550092 --- /dev/null +++ b/nearby/framework/java/android/nearby/aidl/FastPairManageAccountDeviceRequestParcel.aidl @@ -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 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; + // BLE address of the device at the device add time. + String bleAddress; +} \ 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 0000000000..3d9206464d --- /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 0000000000..7db18d0010 --- /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 0000000000..38abba4fb1 --- /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/IFastPairClient.aidl b/nearby/framework/java/android/nearby/aidl/IFastPairClient.aidl new file mode 100644 index 0000000000..4f666bc84e --- /dev/null +++ b/nearby/framework/java/android/nearby/aidl/IFastPairClient.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.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 IFastPairClient { + + void registerHalfSheet(in IFastPairStatusCallback fastPairStatusCallback); + + void unregisterHalfSheet(in IFastPairStatusCallback fastPairStatusCallback); + + void connect(in FastPairDevice fastPairDevice); +} \ 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 0000000000..295621188c --- /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 0000000000..9990014d40 --- /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 0000000000..6b4aaee0b6 --- /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 0000000000..bffc533e53 --- /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 0000000000..d844c06f40 --- /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/halfsheet/Android.bp b/nearby/halfsheet/Android.bp new file mode 100644 index 0000000000..486a3ff3af --- /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 0000000000..22987fb2a0 --- /dev/null +++ b/nearby/halfsheet/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + 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 0000000000..187d51e675 Binary files /dev/null and b/nearby/halfsheet/apk-certs/com.android.nearby.halfsheet.pk8 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 0000000000..440c524a7f --- /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 0000000000..e9f4288aa0 --- /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 0000000000..098dccbf50 --- /dev/null +++ b/nearby/halfsheet/res/anim/fast_pair_bottom_sheet_enter.xml @@ -0,0 +1,7 @@ + + + + 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 0000000000..1cf7401f76 --- /dev/null +++ b/nearby/halfsheet/res/anim/fast_pair_bottom_sheet_exit.xml @@ -0,0 +1,7 @@ + + + + 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 0000000000..9a51ddbe40 --- /dev/null +++ b/nearby/halfsheet/res/anim/fast_pair_half_sheet_slide_in.xml @@ -0,0 +1,14 @@ + + + + + + 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 0000000000..c589482c33 --- /dev/null +++ b/nearby/halfsheet/res/anim/fast_pair_half_sheet_slide_out.xml @@ -0,0 +1,15 @@ + + + + + + + + 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 0000000000..7d61d1c970 --- /dev/null +++ b/nearby/halfsheet/res/drawable/fast_pair_ic_info.xml @@ -0,0 +1,25 @@ + + + + \ 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 0000000000..6765e1194c --- /dev/null +++ b/nearby/halfsheet/res/drawable/fastpair_outline.xml @@ -0,0 +1,7 @@ + + + + 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 0000000000..7e7d8ddaeb --- /dev/null +++ b/nearby/halfsheet/res/drawable/half_sheet_bg.xml @@ -0,0 +1,10 @@ + + + + + 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 0000000000..7fbe229cfd --- /dev/null +++ b/nearby/halfsheet/res/layout/fast_pair_device_pairing_fragment.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000000..705aa1b158 --- /dev/null +++ b/nearby/halfsheet/res/layout/fast_pair_half_sheet.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + 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 0000000000..11b8343401 --- /dev/null +++ b/nearby/halfsheet/res/layout/fast_pair_heads_up_notification.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000000..dd289477ea --- /dev/null +++ b/nearby/halfsheet/res/layout/fast_pair_heads_up_notification_large_image.xml @@ -0,0 +1,7 @@ + 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 0000000000..ee1d89f59b --- /dev/null +++ b/nearby/halfsheet/res/layout/fast_pair_heads_up_notification_small_image.xml @@ -0,0 +1,11 @@ + diff --git a/nearby/halfsheet/res/values/colors.xml b/nearby/halfsheet/res/values/colors.xml new file mode 100644 index 0000000000..b066665504 --- /dev/null +++ b/nearby/halfsheet/res/values/colors.xml @@ -0,0 +1,28 @@ + + + + #00000000 + + @android:color/system_accent1_100 + @android:color/system_neutral1_900 + @android:color/system_neutral1_900 + @android:color/system_accent1_600 + @android:color/system_neutral2_700 + @android:color/system_neutral1_900 + + + #4285F4 + + + #DE000000 + #24000000 + #D93025 + #80868B + #FFFFFF + #1A73E8 + #F44336 + #24000000 + diff --git a/nearby/halfsheet/res/values/dimens.xml b/nearby/halfsheet/res/values/dimens.xml new file mode 100644 index 0000000000..f843042adb --- /dev/null +++ b/nearby/halfsheet/res/values/dimens.xml @@ -0,0 +1,42 @@ + + + + 160dp + 14sp + 11sp + 4dp + 8dp + 8dp + 40dp + 64dp + 32dp + 3dp + 350dp + 215dp + 136dp + 36dp + 48dp + 152dp + 100dp + 156 + 182 + + + 360dp + + 16dp + 70dp + 4dp + + 4dp + 32dp + 32dp + + 0dp + 0dp + 0dp + 0dp + 0dp + + 48dp + diff --git a/nearby/halfsheet/res/values/ints.xml b/nearby/halfsheet/res/values/ints.xml new file mode 100644 index 0000000000..07bf9d2347 --- /dev/null +++ b/nearby/halfsheet/res/values/ints.xml @@ -0,0 +1,5 @@ + + + 250 + 250 + diff --git a/nearby/halfsheet/res/values/overlayable.xml b/nearby/halfsheet/res/values/overlayable.xml new file mode 100644 index 0000000000..fffa2e357e --- /dev/null +++ b/nearby/halfsheet/res/values/overlayable.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ 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 0000000000..01a82e4c35 --- /dev/null +++ b/nearby/halfsheet/res/values/strings.xml @@ -0,0 +1,72 @@ + + + + + + + + Starting Setup… + + Set up device + + Device connected + + Couldn\'t connect + + + + + Done + + Save + + Connect + + Set up + + Settings + \ 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 0000000000..917bb63cfd --- /dev/null +++ b/nearby/halfsheet/res/values/styles.xml @@ -0,0 +1,38 @@ + + + + + + + + + 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 0000000000..9507b9b0e4 --- /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 = "HalfSheetActivity"; + + 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 0000000000..a62c8cc824 --- /dev/null +++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/DevicePairingFragment.java @@ -0,0 +1,486 @@ +/* + * 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.FastPairClient; +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.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. + * + *

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 FastPairClient mFastPairClient; + + private @PairStatusMetadata.Status int mPairStatus = PairStatusMetadata.Status.UNKNOWN; + // True when there is a companion app to open. + private boolean mIsLaunchable; + private boolean mIsConnecting; + // Indicates that the setup button is clicked before. + private boolean mSetupButtonClicked = false; + + // Holds the new text while we transition between the two. + private static final int TAG_PENDING_TEXT = R.id.toolbar_title; + public static final String APP_LAUNCH_FRAGMENT_TYPE = "APP_LAUNCH"; + + private static final String ARG_SETUP_BUTTON_CLICKED = "SETUP_BUTTON_CLICKED"; + private static final String ARG_PAIRING_RESULT = "PAIRING_RESULT"; + + /** + * Create certain fragment according to the intent. + */ + @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) { + mFastPairClient = new FastPairClient(getContext(), mBundle.getBinder(EXTRA_BINDER)); + mFastPairClient.registerHalfSheet(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(); + mFastPairClient.connect( + new FastPairDevice.Builder() + .addMedium(NearbyDevice.Medium.BLE) + .setBluetoothAddress(mScanFastPairStoreItem.getAddress()) + .setData(FastPairUtils.convertFrom(mScanFastPairStoreItem) + .toByteArray()) + .build()); + } + + // Receives callback from service. + @Override + public void onPairUpdate(FastPairDevice fastPairDevice, PairStatusMetadata pairStatusMetadata) { + @PairStatusMetadata.Status int status = pairStatusMetadata.getStatus(); + if (status == PairStatusMetadata.Status.DISMISS && getActivity() != null) { + getActivity().finish(); + } + mIsConnecting = false; + mPairStatus = status; + invalidateState(); + } + + @Override + public void invalidateState() { + HalfSheetFragmentState newState = NOT_STARTED; + if (mIsConnecting) { + newState = PAIRING; + } else { + switch (mPairStatus) { + case PairStatusMetadata.Status.SUCCESS: + newState = mIsLaunchable ? PAIRED_LAUNCHABLE : PAIRED_UNLAUNCHABLE; + break; + case PairStatusMetadata.Status.FAIL: + newState = FAILED; + break; + default: + if (mScanFastPairStoreItem != null) { + newState = FOUND_DEVICE; + } + } + } + if (newState == mFragmentState) { + return; + } + setState(newState); + } + + @Override + public void setState(HalfSheetFragmentState state) { + super.setState(state); + invalidateTitles(); + invalidateButtons(); + } + + private void setScanFastPairStoreItem(ScanFastPairStoreItem item) { + mScanFastPairStoreItem = item; + invalidateLaunchable(); + } + + private void invalidateLaunchable() { + String companionApp = + FastPairUtils.getCompanionAppFromActionUrl(mScanFastPairStoreItem.getActionUrl()); + if (isEmpty(companionApp)) { + mIsLaunchable = false; + return; + } + mIsLaunchable = + FastPairUtils.isLaunchable(Objects.requireNonNull(getContext()), companionApp); + } + + private void invalidateButtons() { + mConnectProgressBar.setVisibility(View.INVISIBLE); + mConnectButton.setVisibility(View.INVISIBLE); + mCancelButton.setVisibility(View.INVISIBLE); + mSetupButton.setVisibility(View.INVISIBLE); + mSettingsButton.setVisibility(View.INVISIBLE); + mInfoIconButton.setVisibility(View.INVISIBLE); + + switch (mFragmentState) { + case FOUND_DEVICE: + mInfoIconButton.setVisibility(View.VISIBLE); + mConnectButton.setVisibility(View.VISIBLE); + break; + case PAIRING: + mConnectProgressBar.setVisibility(View.VISIBLE); + mCancelButton.setVisibility(View.VISIBLE); + setBackgroundClickable(false); + break; + case PAIRED_LAUNCHABLE: + mCancelButton.setVisibility(View.VISIBLE); + mSetupButton.setVisibility(View.VISIBLE); + setBackgroundClickable(true); + break; + case FAILED: + mSettingsButton.setVisibility(View.VISIBLE); + setBackgroundClickable(true); + break; + case NOT_STARTED: + case PAIRED_UNLAUNCHABLE: + default: + mCancelButton.setVisibility(View.VISIBLE); + setBackgroundClickable(true); + } + } + + private void setBackgroundClickable(boolean isClickable) { + HalfSheetActivity activity = (HalfSheetActivity) getActivity(); + if (activity == null) { + Log.w(TAG, "setBackgroundClickable: failed to set clickable to " + isClickable + + " because cannot get HalfSheetActivity."); + return; + } + View background = activity.findViewById(R.id.background); + if (background == null) { + Log.w(TAG, "setBackgroundClickable: failed to set clickable to " + isClickable + + " cannot find background at HalfSheetActivity."); + return; + } + Log.d(TAG, "setBackgroundClickable to " + isClickable); + background.setClickable(isClickable); + } + + private void invalidateTitles() { + String newTitle = getTitle(); + invalidateTextView(mTitleView, newTitle); + String newSubTitle = getSubTitle(); + invalidateTextView(mSubTitleView, newSubTitle); + } + + private void invalidateTextView(TextView textView, String newText) { + CharSequence oldText = + textView.getTag(TAG_PENDING_TEXT) != null + ? (CharSequence) textView.getTag(TAG_PENDING_TEXT) + : textView.getText(); + if (TextUtils.equals(oldText, newText)) { + return; + } + if (TextUtils.isEmpty(oldText)) { + // First time run. Don't animate since there's nothing to animate from. + textView.setText(newText); + } else { + textView.setTag(TAG_PENDING_TEXT, newText); + textView + .animate() + .alpha(0f) + .setDuration(TEXT_ANIMATION_DURATION_MILLISECONDS) + .withEndAction( + () -> { + textView.setText(newText); + textView + .animate() + .alpha(1f) + .setDuration(TEXT_ANIMATION_DURATION_MILLISECONDS); + }); + } + } + + private String getTitle() { + switch (mFragmentState) { + case PAIRED_LAUNCHABLE: + return getString(R.string.fast_pair_title_setup); + case FAILED: + return getString(R.string.fast_pair_title_fail); + case FOUND_DEVICE: + case NOT_STARTED: + case PAIRED_UNLAUNCHABLE: + default: + return mScanFastPairStoreItem.getDeviceName(); + } + } + + private String getSubTitle() { + switch (mFragmentState) { + case PAIRED_LAUNCHABLE: + return String.format( + mScanFastPairStoreItem + .getFastPairStrings() + .getPairingFinishedCompanionAppInstalled(), + mScanFastPairStoreItem.getDeviceName()); + case FAILED: + return mScanFastPairStoreItem.getFastPairStrings().getPairingFailDescription(); + case PAIRED_UNLAUNCHABLE: + getString(R.string.fast_pair_device_ready); + // fall through + case FOUND_DEVICE: + case NOT_STARTED: + return mScanFastPairStoreItem.getFastPairStrings().getInitialPairingDescription(); + default: + return ""; + } + } +} diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/HalfSheetModuleFragment.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/HalfSheetModuleFragment.java new file mode 100644 index 0000000000..f1db4d0f5e --- /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. + * + *

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 0000000000..467997c065 --- /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 0000000000..903ea90d7f --- /dev/null +++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/FastPairUtils.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 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}. + * + *

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()) + .setType(Cache.NearbyType.NEARBY_DEVICE) + .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 0000000000..218c756070 --- /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/Android.bp b/nearby/service/Android.bp new file mode 100644 index 0000000000..802e2c8d2a --- /dev/null +++ b/nearby/service/Android.bp @@ -0,0 +1,98 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + 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", +} + + +// 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 0000000000..8fdac87f2e --- /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 0000000000..d721575d97 --- /dev/null +++ b/nearby/service/java/com/android/server/nearby/NearbyService.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; + +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.ChreCommunication; +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; + +import java.util.concurrent.Executors; + +import service.proto.Blefilter; + +/** 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( + mContext, + (results) -> { + // TODO(b/221082271): hooked with API codes. + for (Blefilter.BleFilterResult result : results.getResultList()) { + Log.i( + TAG, + String.format( + "received filter result with id: %d", + result.getId())); + } + }); + } + + @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. + * + *

{@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(); + mSystemInjector.initializeContextHubManagerAdapter(); + mPresenceManager.initiate( + new ChreCommunication( + mSystemInjector, Executors.newSingleThreadExecutor())); + 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 + 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 0000000000..23d5170947 --- /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. + * + * + *

Current filtering on the following fields are supported: + *

  • Service UUIDs which identify the bluetooth gatt services running on the device. + *
  • Name of remote Bluetooth LE device. + *
  • Mac address of the remote device. + *
  • Service data which is the data associated with a service. + *
  • 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 CREATOR = new Creator() { + @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 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. + * + *

    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. + * + *

    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. + * + *

    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 0000000000..103a27fe80 --- /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 mServiceUuids; + + // null when the scan record is not valid. + @Nullable + private final SparseArray mManufacturerSpecificData; + + // null when the scan record is not valid. + @Nullable + private final Map 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 serviceUuids, + @Nullable SparseArray manufacturerData, + @Nullable Map 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 getServiceUuids() { + return mServiceUuids; + } + + /** + * Returns a sparse array of manufacturer identifier and its corresponding manufacturer specific + * data. + */ + @Nullable + public SparseArray 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 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: + * + *

    pathloss = txPowerLevel - rssi + */ + 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}. + * + *

    The format is defined in Bluetooth 4.1 specification, Volume 3, Part C, Section 11 and 18. + * + *

    All numerical multi-byte entities and values shall use little-endian byte + * 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 serviceUuids = new ArrayList<>(); + String localName = null; + int txPowerLevel = Integer.MIN_VALUE; + + SparseArray manufacturerData = new SparseArray<>(); + Map 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 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 0000000000..71ec10cef1 --- /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 CREATOR = new Creator() { + @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. + *

    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 0000000000..9e795ac7a1 --- /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: + * + *

      + *
    • UUID (universally unique identifier), 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. + *
    • Instance a 32-bit unsigned integer that can be used to group related beacons that + * have the same UUID. + *
    • the mathematics of TX signal strength, used for proximity calculations. + *
    + * + * ...and others. + * + * @see BLE Glossary + * @see Bluetooth + * Data Types Specification + */ +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 0000000000..c1ff9fd31e --- /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. + * + *

    There are 2 different packet formats that are supported, which is used can be determined by + * packet length: + * + *

    For 3-byte packets, the full packet is the model ID. + * + *

    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 go/fast-pair-2-service-data + */ +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 getExtraFields(byte[] serviceData) + throws IllegalFormatException { + SparseArray 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 0000000000..f27899f3e9 --- /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. + * + *

    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, // <> + 0x01, // LE Limited Discoverable Mode + 0x0A, // Length of this Data + 0x09, // <> + '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, // <> + 127 // Level = 127 + }; + + // Example data including a service data block + public static byte[] sd1 = { + 0x02, // Length of this Data + 0x01, // <> + 0x04, // BR/EDR Not Supported. + 0x03, // Length of this Data + 0x02, // <> + 0x04, + 0x18, // TX Power Service UUID + 0x1e, // Length of this Data + (byte) 0x16, // <> + // 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 0000000000..321e125176 --- /dev/null +++ b/nearby/service/java/com/android/server/nearby/common/ble/util/RangingUtils.java @@ -0,0 +1,163 @@ +/* + * 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 { + /* + * Key to variable names used in this class (viz. Physics): + * + * 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 (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)) + * = -147.55 + * + * FSPL_DISTANCE_1M = 20*log10(1m) + * = 0 + * + * PATH_LOSS_AT_1M = FSPL_DISTANCE_1M + FSPL_FREQ + FSPL_LIGHT + * = 0 + 187.75 + (-147.55) + * = 40.20 [round to 41] + * + * Note that PATH_LOSS_AT_1M is rounded to 41 instead to the more natural 40. The first version + * of this file had a typo that caused the value to be close to 41; when this was discovered, + * the value 41 was already used in many places, and it was more important to be consistent + * rather than exact. + * + * Given this we can work out a formula for distance from a given RSSI (received signal strength + * indicator) and a given value for the expected strength at one meter from the beacon (aka + * calibrated transmission power). Both values are in dBm. + * + * FSPL = 20*log10(d) + PATH_LOSS_AT_1M = full_power - RSSI + * 20*log10(d) + PATH_LOSS_AT_1M = power_at_1m + PATH_LOSS_AT_1M - RSSI + * 20*log10(d) = power_at_1m - RSSI + * log10(d) = (power_at_1m - RSSI) / 20 + * d = 10 ^ ((power_at_1m - RSSI) / 20) + * + * Note: because of how logarithms work, units get a bit funny. If you take a two values x and y + * whose units are dBm, the value of x - y has units of dB, not dBm. Similarly, if x is dBm and + * y is in dB, then x - y will be in dBm. + */ + + /* (dBm) PATH_LOSS at 1m for isotropic antenna transmitting BLE */ + public static final int PATH_LOSS_AT_1M = 41; + + /** Different region categories, based on distance range. */ + public static final class Region { + + public static final int UNKNOWN = -1; + public static final int NEAR = 0; + public static final int MID = 1; + public static final int FAR = 2; + + private Region() {} + } + + // Cutoff distances between different regions. + public static final double NEAR_TO_MID_METERS = 0.5; + public static final double MID_TO_FAR_METERS = 2.0; + + public static final int DEFAULT_CALIBRATED_TX_POWER = -77; + + private RangingUtils() {} + + /** + * Convert RSSI to path loss using the free space path loss equation. See Free-space_path_loss + * + * @param rssi Received Signal Strength Indication (RSSI) in dBm + * @param calibratedTxPower the calibrated power of the transmitter (dBm) at 1 meter + * @return The calculated path loss. + */ + public static int pathLossFromRssi(int rssi, int calibratedTxPower) { + return calibratedTxPower + PATH_LOSS_AT_1M - rssi; + } + + /** + * Convert RSSI to distance using the free space path loss equation. See Free-space_path_loss + * + * @param rssi Received Signal Strength Indication (RSSI) in dBm + * @param calibratedTxPower the calibrated power of the transmitter (dBm) at 1 meter + * @return the distance at which that rssi value would occur in meters + */ + public static double distanceFromRssi(int rssi, int calibratedTxPower) { + return Math.pow(10, (calibratedTxPower - rssi) / 20.0); + } + + /** + * Determine the region of a beacon given its perceived distance. + * + * @param distance The measured distance in meters. + * @return the region as one of the constants in {@link Region}. + */ + public static int regionFromDistance(double distance) { + if (distance < 0) { + return Region.UNKNOWN; + } + if (distance <= NEAR_TO_MID_METERS) { + return Region.NEAR; + } + if (distance <= MID_TO_FAR_METERS) { + return Region.MID; + } + return Region.FAR; + } + + /** + * Convert distance to RSSI using the free space path loss equation. See Free-space_path_loss + * + * @param distanceInMeters distance in meters (m) + * @param calibratedTxPower transmitted power (dBm) calibrated to 1 meter + * @return the rssi (dBm) that would be measured at that distance + */ + public static int rssiFromDistance(double distanceInMeters, int calibratedTxPower) { + return distanceInMeters == 0 + ? calibratedTxPower + PATH_LOSS_AT_1M + : (int) (calibratedTxPower - (20 * Math.log10(distanceInMeters))); + } +} + 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 0000000000..4d90b6d554 --- /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 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 String toString(@Nullable Map map) { + if (map == null) { + return "null"; + } + if (map.isEmpty()) { + return "{}"; + } + StringBuilder buffer = new StringBuilder(); + buffer.append('{'); + Iterator> it = map.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry 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 0000000000..6d4275f943 --- /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 0000000000..0ccee97255 --- /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 0000000000..3a02b18fb8 --- /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 0000000000..db2e1ccfe3 --- /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 0000000000..5ac4882fc9 --- /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 0000000000..30fd1887ec --- /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 0000000000..249011a827 --- /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. + *

    + * 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. + *

    + * See reserved UUID org.bluetooth.service.device_information. + */ + public static final UUID DEVICE_INFORMATION = fromShortUuid((short) 0x180A); + + /** + * Generic attribute service. + *

    + * 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. + *

    + * See reserved UUID org.bluetooth.characteristic.firmware_revision_string. + */ + public static final UUID FIRMWARE_REVISION_STRING = fromShortUuid((short) 0x2A26); + + /** + * Service change characteristic. + *

    + * 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. + *

    + * 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 0000000000..28a9c33a4f --- /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 0000000000..c9ccfd5035 --- /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. + * + *

    Additional Data packet is: + * + *

      + *
    1. AdditionalData_Packet[0 - 7]: the first 8-byte of HMAC. + *
    2. AdditionalData_Packet[8 - var]: the encrypted message by AES-CTR, with 8-byte nonce + * appended to the front. + *
    + * + * 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 0000000000..50a818b722 --- /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: + * + *
      + *
    1. encryptedBlock[i] = clearBlock[i] ^ AES(counter), and + *
    2. concat(encryptedBlock[0], encryptedBlock[1],...) to create the encrypted result, where + *
    3. counter: the 16-byte input of AES. counter = iv + block_index. + *
    4. iv: extend 8-byte nonce to 16 bytes with zero padding. i.e. concat(0x0000000000000000, + * nonce). + *
    5. nonce: the cryptographically random 8 bytes, must never be reused with the same key. + *
    + */ +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 0000000000..547931e651 --- /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 0000000000..9bb5a8601c --- /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 0000000000..07306c199f --- /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. + * + *

    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 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 0000000000..6c467d3cf0 --- /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}). + * + *

    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 0000000000..c5475a69ed --- /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. + * + *

    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 0000000000..c26c6adf95 --- /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 0000000000..637cd03ba4 --- /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 0000000000..9c8d292b2c --- /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 0000000000..cfecd2f5be --- /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. + * + *

    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. + * + *

    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. + * + *

    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. + * + *

    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. + * + *

    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. + * + *

    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. + * + *

    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. + * + *

    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. + * + *

    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. + * + *

    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. + * + *

    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. + * + *

    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. + * + *

    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 PROFILES = + ImmutableMap.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. + * + *

    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. + * + *

    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 0000000000..d6aa3b2baa --- /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 0000000000..5bcf10ab2b --- /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 0000000000..dbcdf077ea --- /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 tryProvider(String provider) throws NoSuchAlgorithmException, NoSuchProviderException; + } + + private static T findProvider(ProviderConsumer 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 0000000000..0b50dfd009 --- /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 CREATOR = + new Creator() { + @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 0000000000..4fc1917a39 --- /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 0000000000..024bfdee8a --- /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 0000000000..c963aa6076 --- /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 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 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); + + /** 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 0000000000..0ff1bf2fc4 --- /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 0000000000..789ef5989f --- /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. + * + *

    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): + * + *

    + * {@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)
    + * }
    + * 
    + * + * The performance profiling result by {@link TimingLogger}: + * + *
    + *   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
    + * 
    + */ +// 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 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) { + 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: + * + *
      + *
    1. Get BrEdr handover information if requested + *
    2. Discover the device (on Android N and lower to work around a bug) + *
    3. Connect to the device + *
        + *
      • Attempt a direct connection to a supported profile if we're already bonded + *
      • Create a new bond with the not bonded device and then connect to a supported + * profile + *
      + *
    4. Write the account secret + *
    + * + *

    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 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 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 mHistoryItems; + + FastPairHistoryFinder(List 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 0000000000..e7748860e6 --- /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 0000000000..e7ce4bfde6 --- /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. + * + *

    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 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 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 0000000000..984133b153 --- /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 + * Fastpair Spec Procedure + */ +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 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> { + + 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 { + + @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 { + + 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 0000000000..26ff79fab5 --- /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 CREATOR = + new Creator() { + @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 0000000000..cc7a300dbb --- /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: + * + *

      + *
    1. SHA256(concat((key ^ opad),SHA256(concat((key ^ ipad), data)))), where + *
    2. key is the given secret extended to 64 bytes by concat(secret, ZEROS). + *
    3. opad is 64 bytes outer padding, consisting of repeated bytes valued 0x5c. + *
    4. ipad is 64 bytes inner padding, consisting of repeated bytes valued 0x36. + *
    + * + */ +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 0000000000..88c9484d34 --- /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 parse(byte[] bytes) throws ParseException { + List 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 0000000000..b04cf7352a --- /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. + * + *

    Encoded packet is: + * + *

      + *
    1. Packet[0 - (data length - 1)]: the raw data. + *
    2. Packet[data length - (data length + 7)]: the 8-byte message nonce. + *
    3. Packet[(data length + 8) - (data length + 15)]: the 8-byte of HMAC. + *
    + */ +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 0000000000..1521be6033 --- /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. + * + *

    Naming packet is: + * + *

      + *
    1. Naming_Packet[0 - 7]: the first 8-byte of HMAC. + *
    2. Naming_Packet[8 - var]: the encrypted name (with 8-byte nonce appended to the front). + *
    + */ +@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 0000000000..722dc85c83 --- /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 0000000000..270cb42fa7 --- /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 0000000000..f5807a3254 --- /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 0000000000..bb7b71b796 --- /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 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 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 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 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 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 0000000000..a2603b5246 --- /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. + * + *

    Example usage: + * + *

    {@code
    + * try {
    + *   Reflect.on(btAdapter)
    + *       .withMethod("setScanMode", int.class)
    + *       .invoke(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE)
    + * } catch (ReflectionException e) { }
    + * }
    + */ +// 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 0000000000..1c20c550a4 --- /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 0000000000..244ee66423 --- /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 0000000000..d0d2a5d636 --- /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 0000000000..7f525a7b31 --- /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: + * + *
      + *
    • Simpler to create and register, with a list of actions. + *
    • Implements AutoCloseable. If used as a resource in try-with-resources (available on + * KitKat+), unregisters itself automatically. + *
    • Lets you block waiting for your state transition with {@link #await}. + *
    + */ +// 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 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 0000000000..7382ff371e --- /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 0000000000..83ee309026 --- /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. + * + *

    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 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 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 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 0000000000..41ac9f512f --- /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 0000000000..de131e4ee6 --- /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 mChangeObservers = + new ConcurrentHashMap<>(); + + private final List 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 getCharacteristics(UUID serviceUuid) + throws BluetoothException { + if (!mServicesDiscovered) { + discoverServices(); + } + ArrayList 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(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(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(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(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(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(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(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( + 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( + 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(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 mValues = new LinkedBlockingDeque(); + + @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 0000000000..18a9f5f3bb --- /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 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(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(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 connectOperation = + new Operation(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 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 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 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( + 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(OperationType.DISCOVER_SERVICES_INTERNAL, gatt), status); + } + + @Override + public void onCharacteristicRead(BluetoothGattWrapper gatt, + BluetoothGattCharacteristic characteristic, int status) { + mBluetoothOperationExecutor.notifyCompletion( + new Operation(OperationType.READ_CHARACTERISTIC, gatt, characteristic), + status, characteristic.getValue()); + } + + @Override + public void onCharacteristicWrite(BluetoothGattWrapper gatt, + BluetoothGattCharacteristic characteristic, int status) { + mBluetoothOperationExecutor.notifyCompletion(new Operation( + OperationType.WRITE_CHARACTERISTIC, gatt, characteristic), status); + } + + @Override + public void onDescriptorRead(BluetoothGattWrapper gatt, BluetoothGattDescriptor descriptor, + int status) { + mBluetoothOperationExecutor.notifyCompletion( + new Operation(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(OperationType.WRITE_DESCRIPTOR, gatt, descriptor), status); + } + + @Override + public void onReadRemoteRssi(BluetoothGattWrapper gatt, int rssi, int status) { + mBluetoothOperationExecutor.notifyCompletion( + new Operation(OperationType.READ_RSSI, gatt), status, rssi); + } + + @Override + public void onReliableWriteCompleted(BluetoothGattWrapper gatt, int status) { + mBluetoothOperationExecutor.notifyCompletion( + new Operation(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(OperationType.SCAN), + new BluetoothException("Scan failed: " + errorMessage)); + } + + @Override + public void onScanResult(int callbackType, ScanResult result) { + mBluetoothOperationExecutor.notifySuccess( + new Operation(OperationType.SCAN), result.getDevice()); + } + } + + /** + * Options for {@link #connect}. + */ + public static class ConnectionOptions { + + private boolean mAutoConnect; + private long mConnectionTimeoutMillis; + private Optional mConnectionPriority; + private Optional mMtu; + + private ConnectionOptions(boolean autoConnect, long connectionTimeoutMillis, + Optional connectionPriority, + Optional mtu) { + this.mAutoConnect = autoConnect; + this.mConnectionTimeoutMillis = connectionTimeoutMillis; + this.mConnectionPriority = connectionPriority; + this.mMtu = mtu; + } + + boolean autoConnect() { + return mAutoConnect; + } + + long connectionTimeoutMillis() { + return mConnectionTimeoutMillis; + } + + Optional connectionPriority() { + return mConnectionPriority; + } + + Optional 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 mConnectionPriority = Optional.empty(); + private Optional 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 0000000000..16abd99052 --- /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 Type of provided instance. + */ +public interface NonnullProvider { + /** 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 0000000000..6cfdd784a4 --- /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 0000000000..a4de913d6a --- /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 0000000000..f46ea7ad05 --- /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 0000000000..afa2a1bc03 --- /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 getBondedDevices() { + Set bondedDevices = + mWrappedBluetoothAdapter.getBondedDevices(); + if (bondedDevices == null) { + return null; + } + Set result = new HashSet(); + 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 0000000000..5b45f618fc --- /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 0000000000..d36cfa2517 --- /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 0000000000..3f6f3617fe --- /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 0000000000..875dad562f --- /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 0000000000..453ee5d694 --- /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 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 0000000000..6fe44324a6 --- /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 0000000000..8a13abe5d3 --- /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 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 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 0000000000..70926a7719 --- /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)}. + */ + public void onBatchScanResults(List 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 results) { + List 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 0000000000..1a6b7b304b --- /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 0000000000..bb51920f18 --- /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 0000000000..fecf483624 --- /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> mBlockingQueueProvider; + private final TimeProvider mTimeProvider; + @VisibleForTesting + final Map, Queue> 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>() { + @Override + public BlockingQueue get() { + return new LinkedBlockingDeque(); + } + }); + } + + /** + * Constructor for unit tests. + */ + @VisibleForTesting + BluetoothOperationExecutor(Semaphore operationSemaphore, + TimeProvider timeProvider, + NonnullProvider> blockingQueueProvider) { + mOperationSemaphore = operationSemaphore; + mTimeProvider = timeProvider; + mBlockingQueueProvider = blockingQueueProvider; + } + + /** + * Executes the operation and waits for its completion. + */ + @Nullable + public T execute(Operation operation) throws BluetoothException { + return getResult(schedule(operation)); + } + + /** + * Executes the operation and waits for its completion and returns a non-null result. + */ + public T executeNonnull(Operation 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 execute(Operation 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 executeNonnull(Operation 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 Future schedule(Operation bluetoothOperation) { + BlockingQueue 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(resultQueue, bluetoothOperation, semaphoreAcquired); + } + + /** + * Notifies that this operation has completed with success. + */ + public void notifySuccess(Operation bluetoothOperation) { + postResult(bluetoothOperation, null); + } + + /** + * Notifies that this operation has completed with success and with a result. + */ + public void notifySuccess(Operation 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 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 void notifyCompletion(Operation 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 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 void waitFor(List> futures) throws BluetoothException { + for (Future future : futures) { + if (future == null) { + continue; + } + getResult(future); + } + } + + /** + * Waits with timeout for all future on the list to complete, ignoring the results. + */ + public void waitFor(List> futures, long timeoutMillis) + throws BluetoothException { + long startTime = mTimeProvider.getTimeMillis(); + for (Future 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 getResult(Future future) throws BluetoothException { + return getResultInternal(future, NO_TIMEOUT); + } + + /** + * Waits for a future to complete and returns the result with timeout. + */ + @Nullable + public static T getResult(Future future, long timeoutMillis) throws BluetoothException { + return getResultInternal(future, Math.max(0, timeoutMillis)); + } + + @Nullable + private static T getResultInternal(Future 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. + * + *

    An instance that doesn't implemented run() can be used to notify operation result. + * + * @param Type of provided instance. + */ + public static class Operation { + + 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 Type of provided instance. + */ + public static class SynchronousOperation extends Operation { + + 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. + * + *

  • Waits for operation to complete + *
  • Handles timeouts if needed + *
  • Queues identical Bluetooth operations + *
  • Unwraps Exceptions and null values + */ + private class BluetoothOperationFuture implements Future { + + 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 mResultQueue; + private final Operation mBluetoothOperation; + private final boolean mOperationExecuted; + private boolean mIsCancelled = false; + private boolean mIsDone = false; + + BluetoothOperationFuture(BlockingQueue resultQueue, + Operation 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 0000000000..44c9422203 --- /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 0000000000..c89366f3bb --- /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. + * + *

    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.). + * + *

    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 0000000000..018dcdb1ba --- /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. + * + *

    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.). + * + *

    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/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 0000000000..578e3f6298 --- /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 0000000000..35a1a9fa83 --- /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 0000000000..67d87e3378 --- /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 0000000000..f8b43a6ae2 --- /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, Object> mBindings = new HashMap<>(); + private final ArrayList 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. + * + *

    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 Locator bind(Class type, T instance) { + bindKeyValue(type, instance); + return this; + } + + /** For tests only. Disassociates the specified type from any instance. */ + @VisibleForTesting + public Locator overrideBindingForTest(Class 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 Locator removeBindingForTest(Class 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 get(Class 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 getOptional(Class 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 void bindKeyValue(Class 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 getInstance(Class 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 get(Context context, Class 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 getFromContextWrapper(LocatorContextWrapper wrapper, Class 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 getOptional(Context context, Class 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 0000000000..06eef8aacb --- /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 0000000000..03df33f843 --- /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. + * + *

    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 0000000000..0131c44c07 --- /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: + * + *

    {@code
    +     * void configure(Context context, Class type, Locator locator) {
    +     *   if (type == MyService.class) {
    +     *     locator.bind(MyService.class, new MyImplementation(context));
    +     *   }
    +     * }
    +     * }
    + * + *

    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. + * + *

    {@code
    +     * void destroy(Context context, Class type, Object instance) {
    +     *   if (type == MyService.class) {
    +     *     ((MyService) instance).destroy();
    +     *   }
    +     * }
    +     * }
    + * + *

    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 0000000000..80248e8bfc --- /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 { + + 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 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 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 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 0000000000..2c363f89d5 --- /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." + *

    + * 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 0000000000..7d1db5781f --- /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 0000000000..a86af85512 --- /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. + * + *

    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 type of bound service + */ + interface ServiceListener { + /** 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 type of bound service + */ + interface ServiceProvider { + /** + * 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 ServiceMonitor create( + Context context, + String tag, + ServiceProvider serviceProvider, + @Nullable ServiceListener serviceListener) { + return create(context, ForegroundThread.getHandler(), ForegroundThread.getExecutor(), tag, + serviceProvider, serviceListener); + } + + /** + * Creates a new ServiceMonitor instance that runs on the given handler. + */ + static ServiceMonitor create( + Context context, + Handler handler, + Executor executor, + String tag, + ServiceProvider serviceProvider, + @Nullable ServiceListener 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 0000000000..d0d6c3b7a7 --- /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 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 mServiceProvider; + final @Nullable ServiceListener 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 serviceProvider, + ServiceListener 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 0000000000..0695b5f7ca --- /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 0000000000..b1752345f8 --- /dev/null +++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java @@ -0,0 +1,215 @@ +/* + * 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.server.nearby.common.ble.decode.FastPairDecoder; +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; + + /** 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; + } + + /** + * 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(); + FastPairDataProvider dataProvider = FastPairDataProvider.getInstance(); + if (dataProvider == null) { + return; + } + List accountList = dataProvider.loadFastPairEligibleAccounts(); + 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 { + Rpcs.GetObservedDeviceResponse response = + dataProvider.loadFastPairAntispoofKeyDeviceMetadata(model); + if (response == null) { + Log.e(TAG, "server does not have model id " + + Hex.bytesToStringLowercase(model)); + 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 { + byte[] bloomFilterByteArray = FastPairDecoder + .getBloomFilter(fastPairDevice.getData()); + byte[] bloomFilterSalt = FastPairDecoder + .getBloomFilterSalt(fastPairDevice.getData()); + if (bloomFilterByteArray == null || bloomFilterByteArray.length == 0) { + return; + } + for (Account account : accountList) { + List listDevices = + dataProvider.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 if the device is already paired + List 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 resList = + dataProvider.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 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 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; + } + +} 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 0000000000..e1db7e56e6 --- /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 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 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 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 0000000000..b51203d814 --- /dev/null +++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java @@ -0,0 +1,460 @@ +/* + * 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)) { + Log.d(TAG, "onReceive: ACTION_SCREEN_ON."); + 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); + + mLocatorContextWrapper.getContext() + .registerReceiver(mScreenBroadcastReceiver, mIntentFilter); + + Locator.getFromContextWrapper(mLocatorContextWrapper, FastPairCacheManager.class); + // Default false for now. + mScanEnabled = NearbyManager.getFastPairScanEnabled(mLocatorContextWrapper, false); + 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 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, mScanEnabled)); + } + }; + 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) { + nearbyManager.startScan(new ScanRequest.Builder() + .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR).build(), + ForegroundThread.getExecutor(), + mScanCallback); + } else { + 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 0000000000..d7946d1e8e --- /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 0000000000..883a1f895f --- /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 getGattConnectionAndSecretHandshakeNoRetryGattError() { + ImmutableSet.Builder 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 0000000000..674633df4f --- /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 0000000000..b8a979656b --- /dev/null +++ b/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java @@ -0,0 +1,608 @@ +/* + * 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 { + + 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(); + } + + /** @return True if the item is currently nearby. */ + public boolean isNearby() { + return isNearby(mStoredDiscoveryItem, mClock.millis()); + } + + /** + * Checks if the item is Nearby + */ + static boolean isNearby(Cache.StoredDiscoveryItem item, long currentTimeMillis) { + // A notification may disappear early, if we get a lost callback from Messages. + // But regardless, if we haven't detected the thing in Xmin, consider it gone. + return !isLost(item) + && !isExpired( + currentTimeMillis, + item.getLastObservationTimestampMillis()); + } + + /** + * 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 pairing result to done. + */ + public void setPairingProcessDone(boolean success) { + setLastUserExperience(success ? Cache.StoredDiscoveryItem.ExperienceType.EXPERIENCE_GOOD + : Cache.StoredDiscoveryItem.ExperienceType.EXPERIENCE_BAD); + setLostMillis(mClock.millis()); + Log.d("FastPairDiscovery", + "FastPair: set Lost when pairing process done, " + getId()); + } + + /** + * Sets the store discovery item lost time. + */ + public void setLostMillis(long lostMillis) { + mStoredDiscoveryItem = mStoredDiscoveryItem.toBuilder().setLostMillis(lostMillis).build(); + + mStoredDiscoveryItem = mStoredDiscoveryItem.toBuilder().clearRssi().build(); + + mFastPairCacheManager.saveDiscoveryItem(this); + } + + /** + * Sets the store discovery item mac address. + */ + public void setMacAddress(String address) { + mStoredDiscoveryItem = mStoredDiscoveryItem.toBuilder().setMacAddress(address).build(); + + mFastPairCacheManager.saveDiscoveryItem(this); + } + + /** + * Sets the user experience to be good or bad. + */ + public void setLastUserExperience(Cache.StoredDiscoveryItem.ExperienceType experienceType) { + mStoredDiscoveryItem = mStoredDiscoveryItem.toBuilder() + .setLastUserExperience(experienceType).build(); + mFastPairCacheManager.saveDiscoveryItem(this); + } + /** + * Gets the user experience to be good or bad. + */ + public Cache.StoredDiscoveryItem.ExperienceType getLastUserExperience() { + return mStoredDiscoveryItem.getLastUserExperience(); + } + + /** + * Checks if the item is lost. + */ + private static boolean isLost(Cache.StoredDiscoveryItem item) { + return item.getLastObservationTimestampMillis() <= item.getLostMillis(); + } + + /** + * 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() { + if (isOffline()) { + return true; + } + boolean hasUrlOrPopularApp = !mStoredDiscoveryItem.getActionUrl().isEmpty(); + + return !TextUtils.isEmpty(mStoredDiscoveryItem.getTitle()) && hasUrlOrPopularApp; + } + + /** Checks if the item has server error */ + public boolean isDisabledByServer() { + return mStoredDiscoveryItem.getDebugCategory() + == Cache.StoredDiscoveryItem.DebugMessageCategory.STATUS_DISABLED_BY_SERVER; + } + + private boolean isOffline() { + return isOfflineType(getType()); + } + + /** Checks if the item can be generated on client side. */ + private static boolean isOfflineType(Cache.NearbyType type) { + return type == Cache.NearbyType.NEARBY_CHROMECAST || type == Cache.NearbyType.NEARBY_WEAR; + } + + /** Checks if the action url is app install */ + public boolean isApp() { + return mStoredDiscoveryItem.getActionUrlType() == Cache.ResolvedUrlType.APP; + } + + /** Checks if it's device item. e.g. Chromecast / Wear */ + public boolean isDevice() { + return isDeviceType(mStoredDiscoveryItem.getType()); + } + + /** 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() { + switch (mStoredDiscoveryItem.getType()) { + case NEARBY_CHROMECAST: + case NEARBY_WEAR: + // For the special-case device types, show one item per type. + return Objects.hashCode(mStoredDiscoveryItem.getType()); + case NEARBY_DEVICE: + case NEARBY_TYPE_UNKNOWN: + default: + return Objects.hash( + mStoredDiscoveryItem.getType(), + 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 type of store discovery item. + */ + @Nullable + public Cache.NearbyType getType() { + return mStoredDiscoveryItem.getType(); + } + + /** + * 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 feature graph url of discovery item. + */ + @Nullable + private String getHeroImage() { + return mStoredDiscoveryItem.getFeatureGraphicUrl(); + } + + /** + * 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 ble record of discovery item. + */ + @Nullable + public byte[] getBleRecordBytes() { + return mStoredDiscoveryItem.getBleRecordBytes().toByteArray(); + } + + /** + * 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.distanceFromRssi(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(); + } + + /** + * Gets group id of storedDiscoveryItem. + */ + @Nullable + public String getGroupId() { + return mStoredDiscoveryItem.getGroupId(); + } + + + /** + * 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. + * + *

    In the list view, we sort in descending order, i.e. we put the most relevant items first. + */ + @Override + public int compareTo(DiscoveryItem another) { + if (getType() != another.getType()) { + // For device type v.s. beacon type, rank device item higher. + return isDevice() ? 1 : -1; + } + // 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); + } + + + public Integer getTriggerIdAttachmentTypeHash() { + return Objects.hash(mStoredDiscoveryItem.getTriggerId(), + mStoredDiscoveryItem.getAttachmentType()); + } + + @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( + "[type=%s], [triggerId=%s], [id=%s], [title=%s], [url=%s], " + + "[ready=%s], [macAddress=%s]", + getType().name(), + 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 0000000000..61ca3fdd1e --- /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 0000000000..b8400913fc --- /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 getAllSavedStoreDiscoveryItem() { + List 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 getAllSavedStoredFastPairItem() { + List 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 0000000000..d950d8d9a6 --- /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 0000000000..99805654ae --- /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 0000000000..6c9aff0346 --- /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 0000000000..68217c1edf --- /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 0000000000..6f79e6e822 --- /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; + + FastPairService mFastPairService; + + public FastPairHalfSheetManager(Context context) { + this(new LocatorContextWrapper(context)); + } + + @VisibleForTesting + FastPairHalfSheetManager(LocatorContextWrapper locatorContextWrapper) { + mLocatorContextWrapper = locatorContextWrapper; + mFastPairService = new FastPairService(); + } + + /** + * Invokes half sheet in the other apk. This function can only be called in Nearby because other + * 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; + } + mFastPairService.setFastPairController( + mLocatorContextWrapper.getLocator().get(FastPairController.class)); + Bundle bundle = new Bundle(); + bundle.putBinder(EXTRA_BINDER, mFastPairService); + 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 = mFastPairService.getPairStatusCallback(); + if (pairStatusCallback != null) { + Log.v(TAG, "showPairingFailed: pairStatusCallback not NULL"); + pairStatusCallback.onPairUpdate(new FastPairDevice.Builder().build(), + new PairStatusMetadata(PairStatusMetadata.Status.FAIL)); + } else { + Log.w(TAG, "FastPairHalfSheetManager failed to show success half sheet because " + + "the pairStatusCallback is null"); + } + } + + /** + * 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 = mFastPairService.getPairStatusCallback(); + if (pairStatusCallback != null) { + pairStatusCallback.onPairUpdate( + new FastPairDevice.Builder().setBluetoothAddress(address).build(), + new PairStatusMetadata(PairStatusMetadata.Status.SUCCESS)); + } else { + Log.w(TAG, "FastPairHalfSheetManager failed to show success half sheet because " + + "the pairStatusCallback is null"); + } + } + + /** + * 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 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/FastPairService.java b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairService.java new file mode 100644 index 0000000000..76eabbf43e --- /dev/null +++ b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairService.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.nearby.fastpair.halfsheet; + +import static com.android.server.nearby.fastpair.Constant.TAG; + +import android.nearby.FastPairDevice; +import android.nearby.FastPairStatusCallback; +import android.nearby.PairStatusMetadata; +import android.nearby.aidl.IFastPairClient; +import android.nearby.aidl.IFastPairStatusCallback; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import com.android.server.nearby.fastpair.FastPairController; + +/** + * Service implementing Fast Pair functionality. + * + * @hide + */ +public class FastPairService extends IFastPairClient.Stub { + + private IBinder mStatusCallbackProxy; + private FastPairController mFastPairController; + private FastPairStatusCallback mFastPairStatusCallback; + + /** + * Registers the Binder call back in the server notifies the proxy when there is an update + * in the server. + */ + @Override + public void registerHalfSheet(IFastPairStatusCallback iFastPairStatusCallback) { + mStatusCallbackProxy = iFastPairStatusCallback.asBinder(); + mFastPairStatusCallback = new FastPairStatusCallback() { + @Override + public void onPairUpdate(FastPairDevice fastPairDevice, + PairStatusMetadata pairStatusMetadata) { + try { + iFastPairStatusCallback.onPairUpdate(fastPairDevice, pairStatusMetadata); + } catch (RemoteException e) { + Log.w(TAG, "Failed to update pair status.", e); + } + } + }; + } + + /** + * Unregisters the Binder call back in the server. + */ + @Override + public void unregisterHalfSheet(IFastPairStatusCallback iFastPairStatusCallback) { + mStatusCallbackProxy = null; + mFastPairStatusCallback = null; + } + + /** + * Asks the Fast Pair service to pair the device. 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."); + } + } + + 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 0000000000..b1ae573bd3 --- /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 0000000000..c95f74f4e8 --- /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 0000000000..d469c4572f --- /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 0000000000..ccd7e5e610 --- /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: + *

  • 1, optIn footprint for initial pairing. + *
  • 2, write the device name to provider + *
  • 2.1, generate default personalized name for initial pairing or get the personalized name + * from footprint for subsequent pairing. + *
  • 2.2, set alias name for the bluetooth device. + *
  • 2.3, update the device name for connection to write into provider for initial pair. + *
  • 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. + * + *

    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 0000000000..9af0227227 --- /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 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> queryNanoApps(ContextHubInfo hubInfo) { + return mManager.queryNanoApps(hubInfo); + } + + /** + * Creates and registers a client and its callback with the Context Hub Service. + * + *

    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 0000000000..f990dc93b9 --- /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 0000000000..8bb7980dd2 --- /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 0000000000..91bf49a6a9 --- /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 0000000000..75815f154c --- /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/ChreCommunication.java b/nearby/service/java/com/android/server/nearby/presence/ChreCommunication.java new file mode 100644 index 0000000000..fc9863e792 --- /dev/null +++ b/nearby/service/java/com/android/server/nearby/presence/ChreCommunication.java @@ -0,0 +1,261 @@ +/* + * 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.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); + } + + public static final String TAG = "PresenceService"; + @Nullable private final ContextHubManagerAdapter mManager; + private final Executor mExecutor; + + private boolean mStarted = false; + @Nullable private ContextHubCommsCallback mCallback; + @Nullable private ContextHubClient mContextHubClient; + + public ChreCommunication(Injector injector, Executor executor) { + this.mManager = injector.getContextHubManagerAdapter(); + mExecutor = executor; + } + + /** + * 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 nanoAppIds) { + if (this.mManager == null) { + Log.e(TAG, "ContexHub not available in this device"); + return; + } else { + Log.i(TAG, "Start ChreCommunication"); + } + Preconditions.checkNotNull(callback); + if (nanoAppIds.isEmpty() || mManager == null) { + callback.started(false); + return; + } + if (mStarted) { + this.mCallback.started(true); + return; + } + + // Use this to indicate whether stop was called before the transaction below + // completes. + mStarted = true; + this.mCallback = callback; + + List contextHubs = mManager.getContextHubs(); + + // Make a copy of the list so we can modify it during our async callbacks (in case the code + // is still iterating) + List validContextHubs = new ArrayList<>(contextHubs); + + for (ContextHubInfo info : contextHubs) { + ContextHubTransaction> transaction = mManager.queryNanoApps(info); + Log.i(TAG, "After query Nano Apps "); + transaction.setOnCompleteListener( + new OnQueryCompleteListener(info, validContextHubs, nanoAppIds), mExecutor); + } + } + + /** + * Closes the connection to the {@link ContextHub} chosen during start. + * + *

    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> { + + private final ContextHubInfo mQueriedContextHub; + private final List mContextHubs; + private final Set mNanoAppIds; + + OnQueryCompleteListener( + ContextHubInfo queriedContextHub, + List contextHubs, + Set nanoAppIds) { + this.mQueriedContextHub = queriedContextHub; + this.mContextHubs = contextHubs; + this.mNanoAppIds = nanoAppIds; + } + + @Override + public void onComplete( + ContextHubTransaction> transaction, + ContextHubTransaction.Response> 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/presence/FastAdvertisement.java b/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java new file mode 100644 index 0000000000..e4df673bb9 --- /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. + * + *

    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 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 mActions; + + @Nullable + private final Byte mTxPower; + + FastAdvertisement( + @PresenceCredential.IdentityType int identityType, + byte[] identity, + byte[] salt, + List 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 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: mPresenceActions; + private final PublicCredential mPublicCredential; + + private PresenceDiscoveryResult(int txPower, int rssi, byte[] salt, + List 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 filterActions) { + if (filterActions.isEmpty()) { + return true; + } + return filterActions.stream().anyMatch(mPresenceActions::contains); + } + + private boolean credentialMatches(List 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 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 0000000000..66d486432b --- /dev/null +++ b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java @@ -0,0 +1,167 @@ +/* + * 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.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.location.NanoAppMessage; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; + +import java.util.Collections; + +import service.proto.Blefilter; + +/** PresenceManager is the class initiated in nearby service to handle presence related work. */ +public class PresenceManager { + /** Callback that receives filter results from CHRE Nanoapp. */ + public interface PresenceCallback { + /** Called when {@link BleFilterResults} has been received. */ + void onFilterResults(Blefilter.BleFilterResults filterResults); + } + + private static final String TAG = "PresenceService"; + // 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 final Context mContext; + private final PresenceCallback mPresenceCallback; + private final ChreCallback mChreCallback; + private ChreCommunication mChreCommunication; + + private Blefilter.BleFilters mFilters = null; + private boolean mChreStarted = false; + + private final IntentFilter mIntentFilter; + + private final BroadcastReceiver mScreenBroadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) { + // TODO(b/221082271): removed this faked data once hooked with API codes. + Log.d(TAG, "Update Presence CHRE filter"); + ByteString mac_addr = ByteString.copyFrom(new byte[] {1, 2, 3, 4, 5, 6}); + Blefilter.BleFilter filter = + Blefilter.BleFilter.newBuilder() + .setId(0) + .setUuid(0xFCF1) + .setIntent(1) + .setMacAddress(mac_addr) + .build(); + Blefilter.BleFilters filters = + Blefilter.BleFilters.newBuilder().addFilter(filter).build(); + updateFilters(filters); + } + } + }; + + public PresenceManager(Context context, PresenceCallback presenceCallback) { + mContext = context; + mPresenceCallback = presenceCallback; + mChreCallback = new ChreCallback(); + mIntentFilter = new IntentFilter(); + } + + /** Function called when nearby service start. */ + public void initiate(ChreCommunication chreCommunication) { + mChreCommunication = chreCommunication; + mChreCommunication.start(mChreCallback, Collections.singleton(NANOAPP_ID)); + + mIntentFilter.addAction(Intent.ACTION_SCREEN_ON); + mContext.registerReceiver(mScreenBroadcastReceiver, mIntentFilter); + } + + /** Updates the fitlers in Context Hub. */ + public synchronized void updateFilters(Blefilter.BleFilters filters) { + mFilters = filters; + 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 (PresenceManager.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 (message.getMessageType() == NANOAPP_MESSAGE_TYPE_FILTER_RESULT) { + try { + Blefilter.BleFilterResults results = + Blefilter.BleFilterResults.parseFrom(message.getMessageBody()); + mPresenceCallback.onFilterResults(results); + } 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/AbstractDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java new file mode 100644 index 0000000000..7cc859cb41 --- /dev/null +++ b/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java @@ -0,0 +1,138 @@ +/* + * 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.ScanRequest; +import android.util.Log; + +import androidx.annotation.Nullable; + +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; + + /** + * 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; + } + } +} 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 0000000000..360278735c --- /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 0000000000..a989143830 --- /dev/null +++ b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java @@ -0,0 +1,201 @@ +/* + * 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()) + .setBluetoothAddress(scanResult.getDevice().getAddress()); + + ScanRecord record = scanResult.getScanRecord(); + if (record != null) { + String deviceName = record.getDeviceName(); + if (deviceName != null) { + builder.setName(record.getDeviceName()); + } + Map 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 getScanFilters() { + List 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; + } + bluetoothLeScanner.stopScan(mScanCallback); + } + + @Override + protected void invalidateScanMode() { + onStop(); + onStart(); + } + + private void startScan( + List 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 0000000000..72fe29a0c1 --- /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/DiscoveryProviderController.java b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java new file mode 100644 index 0000000000..469f623d49 --- /dev/null +++ b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java @@ -0,0 +1,58 @@ +/* + * 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.ScanRequest; + +/** + * 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(); +} 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 0000000000..7ff3110bbe --- /dev/null +++ b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java @@ -0,0 +1,253 @@ +/* + * 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.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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +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; + private @ScanRequest.ScanMode int mScanMode; + + @GuardedBy("mLock") + private Map 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 presenceFilters = + record.getScanRequest().getScanFilters().stream().filter( + scanFilter -> scanFilter.getType() + == SCAN_TYPE_NEARBY_PRESENCE).collect( + Collectors.toList()); + if (!presenceFilterMatches(nearbyDevice, presenceFilters)) { + continue; + } + } + try { + record.getScanListener().onDiscovered( + PrivacyFilter.filter(record.getScanRequest().getScanType(), + 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); + 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) { + 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; + } + } + + if (!startProviders(scanRequest)) { + return false; + } + + ScanListenerRecord scanListenerRecord = new ScanListenerRecord(scanRequest, listener); + mScanTypeScanListenerRecordMap.put(listenerBinder, scanListenerRecord); + 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); + NearbyMetrics.logScanStopped( + removedRecord.hashCode(), removedRecord.getScanRequest()); + if (mScanTypeScanListenerRecordMap.isEmpty()) { + stopProviders(); + return; + } + + // Removes current highest scan mode requested and sets the next highest scan mode. + if (removedRecord.getScanRequest().getScanMode() == mScanMode) { + @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()) { + 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 stopProviders() { + stopBleProvider(); + } + + private void stopBleProvider() { + mBleDiscoveryProvider.getController().stop(); + } + + private void invalidateProviderScanMode() { + if (!mBleDiscoveryProvider.getController().isStarted()) { + Log.d(TAG, + "Skip invalidating BleDiscoveryProvider scan mode because the provider not " + + "started."); + return; + } + mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode); + } + + private static boolean presenceFilterMatches(NearbyDeviceParcelable device, + List scanFilters) { + if (scanFilters.isEmpty()) { + return true; + } + PresenceDiscoveryResult discoveryResult = PresenceDiscoveryResult.fromScanData( + device.getData(), device.getRssi()); + for (ScanFilter scanFilter : scanFilters) { + PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter; + if (discoveryResult.matches(presenceScanFilter)) { + return true; + } + } + return false; + } + + private static class ScanListenerRecord { + + private final ScanRequest mScanRequest; + + 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 0000000000..0f99a2f614 --- /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 loadFastPairDeviceWithAccountKey( + Account account) { + return loadFastPairDeviceWithAccountKey(account, new ArrayList(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 loadFastPairDeviceWithAccountKey( + Account account, List 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 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 0000000000..5c37f6899f --- /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 0000000000..f0ade6cb49 --- /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 { + + 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 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 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 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 0000000000..22e31cdb52 --- /dev/null +++ b/nearby/service/java/com/android/server/nearby/provider/Utils.java @@ -0,0 +1,601 @@ +/* + * 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 convertToFastPairDevicesWithAccountKey( + @Nullable FastPairAccountKeyDeviceMetadataParcel[] metadataParcels) { + if (metadataParcels == null) { + return new ArrayList(0); + } + + List 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); + } + Cache.DiscoveryAttachmentType attachmentType = + Cache.DiscoveryAttachmentType.forNumber( + metadataParcel.discoveryItem.attachmentType); + if (attachmentType != null) { + storedDiscoveryItemBuilder.setAttachmentType(attachmentType); + } + if (metadataParcel.discoveryItem.authenticationPublicKeySecp256r1 != null) { + storedDiscoveryItemBuilder.setAuthenticationPublicKeySecp256R1( + ByteString.copyFrom( + metadataParcel.discoveryItem.authenticationPublicKeySecp256r1)); + } + if (metadataParcel.discoveryItem.bleRecordBytes != null) { + storedDiscoveryItemBuilder.setBleRecordBytes( + ByteString.copyFrom(metadataParcel.discoveryItem.bleRecordBytes)); + } + Cache.StoredDiscoveryItem.DebugMessageCategory debugMessageCategory = + Cache.StoredDiscoveryItem.DebugMessageCategory.forNumber( + metadataParcel.discoveryItem.debugCategory); + if (debugMessageCategory != null) { + storedDiscoveryItemBuilder.setDebugCategory(debugMessageCategory); + } + if (metadataParcel.discoveryItem.debugMessage != null) { + storedDiscoveryItemBuilder.setDebugMessage( + metadataParcel.discoveryItem.debugMessage); + } + 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); + } + if (metadataParcel.discoveryItem.entityId != null) { + storedDiscoveryItemBuilder.setEntityId( + metadataParcel.discoveryItem.entityId); + } + if (metadataParcel.discoveryItem.featureGraphicUrl != null) { + storedDiscoveryItemBuilder.setFeatureGraphicUrl( + metadataParcel.discoveryItem.featureGraphicUrl); + } + storedDiscoveryItemBuilder.setFirstObservationTimestampMillis( + metadataParcel.discoveryItem.firstObservationTimestampMillis); + if (metadataParcel.discoveryItem.groupId != null) { + storedDiscoveryItemBuilder.setGroupId(metadataParcel.discoveryItem.groupId); + } + 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); + Cache.StoredDiscoveryItem.ExperienceType experienceType = + Cache.StoredDiscoveryItem.ExperienceType.forNumber( + metadataParcel.discoveryItem.lastUserExperience); + if (experienceType != null) { + storedDiscoveryItemBuilder.setLastUserExperience(experienceType); + } + storedDiscoveryItemBuilder.setLostMillis(metadataParcel.discoveryItem.lostMillis); + 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); + Cache.NearbyType type = + Cache.NearbyType.forNumber(metadataParcel.discoveryItem.type); + if (type != null) { + storedDiscoveryItemBuilder.setType(type); + } + } + if (metadataParcel.metadata != null) { + FastPairStrings.Builder stringsBuilder = FastPairStrings.newBuilder(); + if (metadataParcel.metadata.assistantSetupHalfSheet != null) { + stringsBuilder.setAssistantHalfSheetDescription( + metadataParcel.metadata.assistantSetupHalfSheet); + } + if (metadataParcel.metadata.assistantSetupNotification != null) { + stringsBuilder.setAssistantNotificationDescription( + metadataParcel.metadata.assistantSetupNotification); + } + if (metadataParcel.metadata.confirmPinDescription != null) { + stringsBuilder.setConfirmPinDescription( + metadataParcel.metadata.confirmPinDescription); + } + if (metadataParcel.metadata.confirmPinTitle != null) { + stringsBuilder.setConfirmPinTitle( + metadataParcel.metadata.confirmPinTitle); + } + 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.fastPairTvConnectDeviceNoAccountDescription != null) { + stringsBuilder.setFastPairTvConnectDeviceNoAccountDescription( + metadataParcel.metadata.fastPairTvConnectDeviceNoAccountDescription); + } + 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.syncContactsDescription != null) { + stringsBuilder.setSyncContactsDescription( + metadataParcel.metadata.syncContactsDescription); + } + if (metadataParcel.metadata.syncContactsTitle != null) { + stringsBuilder.setSyncContactsTitle( + metadataParcel.metadata.syncContactsTitle); + } + if (metadataParcel.metadata.syncSmsDescription != null) { + stringsBuilder.setSyncSmsDescription( + metadataParcel.metadata.syncSmsDescription); + } + if (metadataParcel.metadata.syncSmsTitle != null) { + stringsBuilder.setSyncSmsTitle(metadataParcel.metadata.syncSmsTitle); + } + 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 convertToAccountList( + @Nullable FastPairEligibleAccountParcel[] accountParcels) { + if (accountParcels == null) { + return new ArrayList(0); + } + List accounts = new ArrayList(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.assistantSetupHalfSheet != null) { + stringsBuilder + .setAssistantSetupHalfSheet(metadata.deviceMetadata.assistantSetupHalfSheet); + } + if (metadata.deviceMetadata.assistantSetupNotification != null) { + stringsBuilder.setAssistantSetupNotification( + metadata.deviceMetadata.assistantSetupNotification); + } + if (metadata.deviceMetadata.confirmPinDescription != null) { + stringsBuilder.setConfirmPinDescription(metadata.deviceMetadata.confirmPinDescription); + } + if (metadata.deviceMetadata.confirmPinTitle != null) { + stringsBuilder.setConfirmPinTitle(metadata.deviceMetadata.confirmPinTitle); + } + 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.fastPairTvConnectDeviceNoAccountDescription != null) { + stringsBuilder.setFastPairTvConnectDeviceNoAccountDescription( + metadata.deviceMetadata.fastPairTvConnectDeviceNoAccountDescription); + } + 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.locale != null) { + stringsBuilder.setLocale(metadata.deviceMetadata.locale); + } + 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.syncContactsDescription != null) { + stringsBuilder.setSyncContactsDescription( + metadata.deviceMetadata.syncContactsDescription); + } + if (metadata.deviceMetadata.syncContactsTitle != null) { + stringsBuilder.setSyncContactsTitle( + metadata.deviceMetadata.syncContactsTitle); + } + if (metadata.deviceMetadata.syncSmsDescription != null) { + stringsBuilder.setSyncSmsDescription( + metadata.deviceMetadata.syncSmsDescription); + } + if (metadata.deviceMetadata.syncSmsTitle != null) { + stringsBuilder.setSyncSmsTitle( + metadata.deviceMetadata.syncSmsTitle); + } + 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.attachmentType = storedDiscoveryItem.getAttachmentType().getNumber(); + discoveryItemParcel.authenticationPublicKeySecp256r1 = + storedDiscoveryItem.getAuthenticationPublicKeySecp256R1().toByteArray(); + discoveryItemParcel.bleRecordBytes = storedDiscoveryItem.getBleRecordBytes().toByteArray(); + discoveryItemParcel.debugCategory = storedDiscoveryItem.getDebugCategory().getNumber(); + discoveryItemParcel.debugMessage = storedDiscoveryItem.getDebugMessage(); + discoveryItemParcel.description = storedDiscoveryItem.getDescription(); + discoveryItemParcel.deviceName = storedDiscoveryItem.getDeviceName(); + discoveryItemParcel.displayUrl = storedDiscoveryItem.getDisplayUrl(); + discoveryItemParcel.entityId = storedDiscoveryItem.getEntityId(); + discoveryItemParcel.featureGraphicUrl = storedDiscoveryItem.getFeatureGraphicUrl(); + discoveryItemParcel.firstObservationTimestampMillis = + storedDiscoveryItem.getFirstObservationTimestampMillis(); + discoveryItemParcel.groupId = storedDiscoveryItem.getGroupId(); + discoveryItemParcel.iconFifeUrl = storedDiscoveryItem.getIconFifeUrl(); + discoveryItemParcel.iconPng = storedDiscoveryItem.getIconPng().toByteArray(); + discoveryItemParcel.id = storedDiscoveryItem.getId(); + discoveryItemParcel.lastObservationTimestampMillis = + storedDiscoveryItem.getLastObservationTimestampMillis(); + discoveryItemParcel.lastUserExperience = + storedDiscoveryItem.getLastUserExperience().getNumber(); + discoveryItemParcel.lostMillis = storedDiscoveryItem.getLostMillis(); + 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(); + discoveryItemParcel.type = storedDiscoveryItem.getType().getNumber(); + + 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.assistantSetupHalfSheet = fpStrings.getAssistantHalfSheetDescription(); + metadataParcel.assistantSetupNotification = fpStrings.getAssistantNotificationDescription(); + metadataParcel.confirmPinDescription = fpStrings.getConfirmPinDescription(); + metadataParcel.confirmPinTitle = fpStrings.getConfirmPinTitle(); + metadataParcel.connectSuccessCompanionAppInstalled = + fpStrings.getPairingFinishedCompanionAppInstalled(); + metadataParcel.connectSuccessCompanionAppNotInstalled = + fpStrings.getPairingFinishedCompanionAppNotInstalled(); + metadataParcel.failConnectGoToSettingsDescription = fpStrings.getPairingFailDescription(); + metadataParcel.fastPairTvConnectDeviceNoAccountDescription = + fpStrings.getFastPairTvConnectDeviceNoAccountDescription(); + metadataParcel.initialNotificationDescription = fpStrings.getTapToPairWithAccount(); + metadataParcel.initialNotificationDescriptionNoAccount = + fpStrings.getTapToPairWithoutAccount(); + metadataParcel.initialPairingDescription = fpStrings.getInitialPairingDescription(); + metadataParcel.retroactivePairingDescription = fpStrings.getRetroactivePairingDescription(); + metadataParcel.subsequentPairingDescription = fpStrings.getSubsequentPairingDescription(); + metadataParcel.syncContactsDescription = fpStrings.getSyncContactsDescription(); + metadataParcel.syncContactsTitle = fpStrings.getSyncContactsTitle(); + metadataParcel.syncSmsDescription = fpStrings.getSyncSmsDescription(); + metadataParcel.syncSmsTitle = fpStrings.getSyncSmsTitle(); + 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 0000000000..599843c9a0 --- /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 0000000000..0b01bc0b49 --- /dev/null +++ b/nearby/service/java/com/android/server/nearby/util/DataUtils.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.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() + + ", assistantHalfSheetDescription=" + + fastPairStrings.getAssistantHalfSheetDescription() + + ", assistantNotificationDescription=" + + fastPairStrings.getAssistantNotificationDescription() + + ", fastPairTvConnectDeviceNoAccountDescription=" + + fastPairStrings.getFastPairTvConnectDeviceNoAccountDescription() + + "]"; + } + + 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()) + .setAssistantHalfSheetDescription(strings.getAssistantSetupHalfSheet()) + .setAssistantNotificationDescription(strings.getAssistantSetupNotification()) + .setFastPairTvConnectDeviceNoAccountDescription( + strings.getFastPairTvConnectDeviceNoAccountDescription()) + .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 0000000000..d397862b77 --- /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 0000000000..6021ff6986 --- /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. + * + *

    There are 2 different packet formats that are supported, which is used can be determined by + * packet length: + * + *

    For 3-byte packets, the full packet is the model ID. + * + *

    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 getExtraFields(byte[] serviceData) + throws IllegalFormatException { + SparseArray 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 0000000000..793ab9a1e7 --- /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 0000000000..1d1d855692 --- /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 0000000000..1b00cf6784 --- /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 0000000000..12731fb78a --- /dev/null +++ b/nearby/service/proto/src/fastpair/cache.proto @@ -0,0 +1,461 @@ +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 + 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; + + // 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; + + // ID used for associating several DiscoveryItems. These items may be + // visually displayed together. + string group_id = 26; + + // 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; + + // Whether the attachment is created in debug namespace + DiscoveryAttachmentType attachment_type = 29; + + // Package name of the App that owns this item. + string package_name = 30; + + // The average star rating of the app. + float star_rating = 31; + + // The "feature" graphic image url used for large sized list view entries. + string feature_graphic_url = 32; + + // 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; + + // Message written to bugreport for 3P developers.(No sensitive info) + // null if the item is valid + string debug_message = 37; + + // Weather the item is filtered out on server. + DebugMessageCategory debug_category = 38; + + // Client timestamp when the trigger (e.g. beacon) was last lost (e.g. when + // Messages told us the beacon's no longer nearby). + int64 lost_millis = 41; + + // The kind of expereince the user last had with this (e.g. if they dismissed + // the notification, that's bad; but if they tapped it, that's good). + ExperienceType last_user_experience = 42; + + // The most recent BLE advertisement related to this item. + bytes ble_record_bytes = 43; + + // An ID generated on the server to uniquely identify content. + string entity_id = 44; + + // 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 3, 12, 13, 14, 15, 16, 18, 21, 24, 27, 33, 35, 39, 40; +} +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 0000000000..6f4faddd1f --- /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 0000000000..6dfc19aaa3 --- /dev/null +++ b/nearby/service/proto/src/fastpair/fast_pair_string.proto @@ -0,0 +1,65 @@ +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; + + // Title to ask the user to confirm the pin code. + optional string confirm_pin_title = 10; + + // Description to ask the user to confirm the pin code. + optional string confirm_pin_description = 11; + + // The title of the UI to ask the user to confirm to sync contacts. + optional string sync_contacts_title = 12; + + // The description of the UI to ask the user to confirm to sync contacts. + optional string sync_contacts_description = 13; + + // The title of the UI to ask the user to confirm to sync SMS. + optional string sync_sms_title = 14; + + // The description of the UI to ask the user to confirm to sync SMS. + optional string sync_sms_description = 15; + + // The description for half sheet to ask user to setup google assistant. + optional string assistant_half_sheet_description = 16; + + // The description for notification to ask user to setup google assistant. + optional string assistant_notification_description = 17; + + // Description of the connect device action on TV, when user is not logged in. + optional string fast_pair_tv_connect_device_no_account_description = 18; +} diff --git a/nearby/service/proto/src/fastpair/rpcs.proto b/nearby/service/proto/src/fastpair/rpcs.proto new file mode 100644 index 0000000000..0399d09dd0 --- /dev/null +++ b/nearby/service/proto/src/fastpair/rpcs.proto @@ -0,0 +1,329 @@ +// 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 locale of all of the strings. + string locale = 1; + + // 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; + + // The title of the UI to ask the user to confirm the pin code. + string confirm_pin_title = 16; + + // The description of the UI to ask the user to confirm the pin code. + string confirm_pin_description = 17; + + // The title of the UI to ask the user to confirm to sync contacts. + string sync_contacts_title = 18; + + // The description of the UI to ask the user to confirm to sync contacts. + string sync_contacts_description = 19; + + // The title of the UI to ask the user to confirm to sync SMS. + string sync_sms_title = 20; + + // The description of the UI to ask the user to confirm to sync SMS. + string sync_sms_description = 21; + + // The description in half sheet to ask user setup google assistant + string assistant_setup_half_sheet = 22; + + // The description in notification to ask user setup google assistant + string assistant_setup_notification = 23; + + // Description of the connect device action on TV, when user is not logged in. + string fast_pair_tv_connect_device_no_account_description = 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 0000000000..da56522635 --- /dev/null +++ b/nearby/service/proto/src/presence/blefilter.proto @@ -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. + */ + +// 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 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. + // TODO(b/193756395): replace with BLE event proto. + optional bytes raw_data = 2; +} + +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 0000000000..845ed84252 --- /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 0000000000..ce841f20b4 --- /dev/null +++ b/nearby/tests/cts/fastpair/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + diff --git a/nearby/tests/cts/fastpair/AndroidTest.xml b/nearby/tests/cts/fastpair/AndroidTest.xml new file mode 100644 index 0000000000..360bbf3f4a --- /dev/null +++ b/nearby/tests/cts/fastpair/AndroidTest.xml @@ -0,0 +1,36 @@ + + + + diff --git a/nearby/tests/cts/fastpair/OWNERS b/nearby/tests/cts/fastpair/OWNERS new file mode 100644 index 0000000000..1756bba70f --- /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 0000000000..aacb6d80aa --- /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 0000000000..ec6e89ae94 --- /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/FastPairAntispoofKeyDeviceMetadataTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairAntispoofKeyDeviceMetadataTest.java new file mode 100644 index 0000000000..226efbb43d --- /dev/null +++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairAntispoofKeyDeviceMetadataTest.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 android.nearby.cts; + +import static com.google.common.truth.Truth.assertThat; + +import android.nearby.FastPairAntispoofKeyDeviceMetadata; +import android.nearby.FastPairDeviceMetadata; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SdkSuppress; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class FastPairAntispoofKeyDeviceMetadataTest { + + 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 float DELTA = 0.001f; + private static final int DEVICE_TYPE = 7; + private static final String DOWNLOAD_COMPANION_APP_DESCRIPTION = + "DOWNLOAD_COMPANION_APP_DESCRIPTION"; + 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[] ANTI_SPOOFING_KEY = new byte[] {4, 5, 6}; + private static final String NAME = "NAME"; + + @Test + @SdkSuppress(minSdkVersion = 32, codeName = "T") + public void testSetGetFastPairAntispoofKeyDeviceMetadataNotNull() { + FastPairDeviceMetadata fastPairDeviceMetadata = genFastPairDeviceMetadata(); + FastPairAntispoofKeyDeviceMetadata fastPairAntispoofKeyDeviceMetadata = + genFastPairAntispoofKeyDeviceMetadata(ANTI_SPOOFING_KEY, fastPairDeviceMetadata); + + assertThat(fastPairAntispoofKeyDeviceMetadata.getAntispoofPublicKey()).isEqualTo( + ANTI_SPOOFING_KEY); + ensureFastPairDeviceMetadataAsExpected( + fastPairAntispoofKeyDeviceMetadata.getFastPairDeviceMetadata()); + } + + @Test + @SdkSuppress(minSdkVersion = 32, codeName = "T") + public void testSetGetFastPairAntispoofKeyDeviceMetadataNull() { + FastPairAntispoofKeyDeviceMetadata fastPairAntispoofKeyDeviceMetadata = + genFastPairAntispoofKeyDeviceMetadata(null, null); + assertThat(fastPairAntispoofKeyDeviceMetadata.getAntispoofPublicKey()).isEqualTo( + null); + assertThat(fastPairAntispoofKeyDeviceMetadata.getFastPairDeviceMetadata()).isEqualTo( + null); + } + + /* Verifies DeviceMetadata. */ + private static void ensureFastPairDeviceMetadataAsExpected(FastPairDeviceMetadata metadata) { + assertThat(metadata.getAssistantSetupHalfSheet()).isEqualTo(ASSISTANT_SETUP_HALFSHEET); + assertThat(metadata.getAssistantSetupNotification()) + .isEqualTo(ASSISTANT_SETUP_NOTIFICATION); + assertThat(metadata.getBleTxPower()).isEqualTo(BLE_TX_POWER); + assertThat(metadata.getConfirmPinDescription()).isEqualTo(CONFIRM_PIN_DESCRIPTION); + assertThat(metadata.getConfirmPinTitle()).isEqualTo(CONFIRM_PIN_TITLE); + assertThat(metadata.getConnectSuccessCompanionAppInstalled()) + .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_INSTALLED); + assertThat(metadata.getConnectSuccessCompanionAppNotInstalled()) + .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED); + assertThat(metadata.getDeviceType()).isEqualTo(DEVICE_TYPE); + assertThat(metadata.getDownloadCompanionAppDescription()) + .isEqualTo(DOWNLOAD_COMPANION_APP_DESCRIPTION); + assertThat(metadata.getFailConnectGoToSettingsDescription()) + .isEqualTo(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION); + assertThat(metadata.getFastPairTvConnectDeviceNoAccountDescription()) + .isEqualTo(FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION); + assertThat(metadata.getImage()).isEqualTo(IMAGE); + assertThat(metadata.getImageUrl()).isEqualTo(IMAGE_URL); + assertThat(metadata.getInitialNotificationDescription()) + .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION); + assertThat(metadata.getInitialNotificationDescriptionNoAccount()) + .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT); + assertThat(metadata.getInitialPairingDescription()).isEqualTo(INITIAL_PAIRING_DESCRIPTION); + assertThat(metadata.getIntentUri()).isEqualTo(INTENT_URI); + assertThat(metadata.getLocale()).isEqualTo(LOCALE); + assertThat(metadata.getName()).isEqualTo(NAME); + assertThat(metadata.getOpenCompanionAppDescription()) + .isEqualTo(OPEN_COMPANION_APP_DESCRIPTION); + assertThat(metadata.getRetroactivePairingDescription()) + .isEqualTo(RETRO_ACTIVE_PAIRING_DESCRIPTION); + assertThat(metadata.getSubsequentPairingDescription()) + .isEqualTo(SUBSEQUENT_PAIRING_DESCRIPTION); + assertThat(metadata.getSyncContactsDescription()).isEqualTo(SYNC_CONTACT_DESCRPTION); + assertThat(metadata.getSyncContactsTitle()).isEqualTo(SYNC_CONTACTS_TITLE); + assertThat(metadata.getSyncSmsDescription()).isEqualTo(SYNC_SMS_DESCRIPTION); + assertThat(metadata.getSyncSmsTitle()).isEqualTo(SYNC_SMS_TITLE); + assertThat(metadata.getTriggerDistance()).isWithin(DELTA).of(TRIGGER_DISTANCE); + assertThat(metadata.getTrueWirelessImageUrlCase()).isEqualTo(TRUE_WIRELESS_IMAGE_URL_CASE); + assertThat(metadata.getTrueWirelessImageUrlLeftBud()) + .isEqualTo(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD); + assertThat(metadata.getTrueWirelessImageUrlRightBud()) + .isEqualTo(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD); + assertThat(metadata.getUnableToConnectDescription()) + .isEqualTo(UNABLE_TO_CONNECT_DESCRIPTION); + assertThat(metadata.getUnableToConnectTitle()).isEqualTo(UNABLE_TO_CONNECT_TITLE); + assertThat(metadata.getUpdateCompanionAppDescription()) + .isEqualTo(UPDATE_COMPANION_APP_DESCRIPTION); + assertThat(metadata.getWaitLaunchCompanionAppDescription()) + .isEqualTo(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION); + } + + /* Generates FastPairAntispoofKeyDeviceMetadata. */ + private static FastPairAntispoofKeyDeviceMetadata genFastPairAntispoofKeyDeviceMetadata( + byte[] antispoofPublicKey, FastPairDeviceMetadata deviceMetadata) { + FastPairAntispoofKeyDeviceMetadata.Builder builder = + new FastPairAntispoofKeyDeviceMetadata.Builder(); + builder.setAntispoofPublicKey(antispoofPublicKey); + builder.setFastPairDeviceMetadata(deviceMetadata); + + return builder.build(); + } + + /* Generates FastPairDeviceMetadata. */ + private static FastPairDeviceMetadata genFastPairDeviceMetadata() { + FastPairDeviceMetadata.Builder builder = new FastPairDeviceMetadata.Builder(); + builder.setAssistantSetupHalfSheet(ASSISTANT_SETUP_HALFSHEET); + builder.setAssistantSetupNotification(ASSISTANT_SETUP_NOTIFICATION); + builder.setBleTxPower(BLE_TX_POWER); + builder.setConfirmPinDescription(CONFIRM_PIN_DESCRIPTION); + builder.setConfirmPinTitle(CONFIRM_PIN_TITLE); + builder.setConnectSuccessCompanionAppInstalled(CONNECT_SUCCESS_COMPANION_APP_INSTALLED); + builder.setConnectSuccessCompanionAppNotInstalled( + CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED); + builder.setDeviceType(DEVICE_TYPE); + builder.setDownloadCompanionAppDescription(DOWNLOAD_COMPANION_APP_DESCRIPTION); + builder.setFailConnectGoToSettingsDescription(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION); + builder.setFastPairTvConnectDeviceNoAccountDescription( + FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION); + builder.setImage(IMAGE); + builder.setImageUrl(IMAGE_URL); + builder.setInitialNotificationDescription(INITIAL_NOTIFICATION_DESCRIPTION); + builder.setInitialNotificationDescriptionNoAccount( + INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT); + builder.setInitialPairingDescription(INITIAL_PAIRING_DESCRIPTION); + builder.setIntentUri(INTENT_URI); + builder.setLocale(LOCALE); + builder.setName(NAME); + builder.setOpenCompanionAppDescription(OPEN_COMPANION_APP_DESCRIPTION); + builder.setRetroactivePairingDescription(RETRO_ACTIVE_PAIRING_DESCRIPTION); + builder.setSubsequentPairingDescription(SUBSEQUENT_PAIRING_DESCRIPTION); + builder.setSyncContactsDescription(SYNC_CONTACT_DESCRPTION); + builder.setSyncContactsTitle(SYNC_CONTACTS_TITLE); + builder.setSyncSmsDescription(SYNC_SMS_DESCRIPTION); + builder.setSyncSmsTitle(SYNC_SMS_TITLE); + builder.setTriggerDistance(TRIGGER_DISTANCE); + builder.setTrueWirelessImageUrlCase(TRUE_WIRELESS_IMAGE_URL_CASE); + builder.setTrueWirelessImageUrlLeftBud(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD); + builder.setTrueWirelessImageUrlRightBud(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD); + builder.setUnableToConnectDescription(UNABLE_TO_CONNECT_DESCRIPTION); + builder.setUnableToConnectTitle(UNABLE_TO_CONNECT_TITLE); + builder.setUpdateCompanionAppDescription(UPDATE_COMPANION_APP_DESCRIPTION); + builder.setWaitLaunchCompanionAppDescription(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION); + + return builder.build(); + } +} diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairDataProviderServiceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairDataProviderServiceTest.java new file mode 100644 index 0000000000..171b6e8d43 --- /dev/null +++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairDataProviderServiceTest.java @@ -0,0 +1,1084 @@ +/* + * 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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; + +import android.accounts.Account; +import android.content.Intent; +import android.nearby.FastPairAccountKeyDeviceMetadata; +import android.nearby.FastPairAntispoofKeyDeviceMetadata; +import android.nearby.FastPairDataProviderService; +import android.nearby.FastPairDeviceMetadata; +import android.nearby.FastPairDiscoveryItem; +import android.nearby.FastPairEligibleAccount; +import android.nearby.aidl.ByteArrayParcel; +import android.nearby.aidl.FastPairAccountDevicesMetadataRequestParcel; +import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel; +import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel; +import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataRequestParcel; +import android.nearby.aidl.FastPairDeviceMetadataParcel; +import android.nearby.aidl.FastPairDiscoveryItemParcel; +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 androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SdkSuppress; + +import com.google.common.collect.ImmutableList; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; + +@RunWith(AndroidJUnit4.class) +public class FastPairDataProviderServiceTest { + + private static final String TAG = "FastPairDataProviderServiceTest"; + + 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 float DELTA = 0.001f; + private static final int DEVICE_TYPE = 7; + 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 boolean ELIGIBLE_ACCOUNT_1_OPT_IN = true; + private static final Account ELIGIBLE_ACCOUNT_2 = new Account("def@gmail.com", "type2"); + private static final boolean ELIGIBLE_ACCOUNT_2_OPT_IN = false; + private static final Account MANAGE_ACCOUNT = new Account("ghi@gmail.com", "type3"); + private static final Account ACCOUNTDEVICES_METADATA_ACCOUNT = + new Account("jk@gmail.com", "type4"); + private static final int NUM_ACCOUNT_DEVICES = 2; + + private static final int ERROR_CODE_BAD_REQUEST = + FastPairDataProviderService.ERROR_CODE_BAD_REQUEST; + private static final int MANAGE_ACCOUNT_REQUEST_TYPE = + FastPairDataProviderService.MANAGE_REQUEST_ADD; + private static final String ERROR_STRING = "ERROR_STRING"; + 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[] ACCOUNT_KEY_2 = new byte[] {9, 3}; + private static final byte[] SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS = new byte[] {2, 8}; + private static final byte[] REQUEST_MODEL_ID = new byte[] {1, 2, 3}; + 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 = 5; + private static final String APP_NAME = "APP_NAME"; + private static final int ATTACHMENT_TYPE = 8; + 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 = 9; + 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 = 93; + 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 = 63; + private static final String TITLE = "TITLE"; + private static final String TRIGGER_ID = "TRIGGER_ID"; + private static final int TX_POWER = 62; + private static final int TYPE = 73; + private static final String BLE_ADDRESS = "BLE_ADDRESS"; + + private static final int ELIGIBLE_ACCOUNTS_NUM = 2; + private static final ImmutableList ELIGIBLE_ACCOUNTS = + ImmutableList.of( + genHappyPathFastPairEligibleAccount(ELIGIBLE_ACCOUNT_1, + ELIGIBLE_ACCOUNT_1_OPT_IN), + genHappyPathFastPairEligibleAccount(ELIGIBLE_ACCOUNT_2, + ELIGIBLE_ACCOUNT_2_OPT_IN)); + private static final int ACCOUNTKEY_DEVICE_NUM = 2; + private static final ImmutableList + FAST_PAIR_ACCOUNT_DEVICES_METADATA = + ImmutableList.of( + genHappyPathFastPairAccountkeyDeviceMetadata(), + genHappyPathFastPairAccountkeyDeviceMetadata()); + + private static final FastPairAntispoofKeyDeviceMetadataRequestParcel + FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA_REQUEST_PARCEL = + genFastPairAntispoofKeyDeviceMetadataRequestParcel(); + private static final FastPairAccountDevicesMetadataRequestParcel + FAST_PAIR_ACCOUNT_DEVICES_METADATA_REQUEST_PARCEL = + genFastPairAccountDevicesMetadataRequestParcel(); + private static final FastPairEligibleAccountsRequestParcel + FAST_PAIR_ELIGIBLE_ACCOUNTS_REQUEST_PARCEL = + genFastPairEligibleAccountsRequestParcel(); + private static final FastPairManageAccountRequestParcel + FAST_PAIR_MANAGE_ACCOUNT_REQUEST_PARCEL = + genFastPairManageAccountRequestParcel(); + private static final FastPairManageAccountDeviceRequestParcel + FAST_PAIR_MANAGE_ACCOUNT_DEVICE_REQUEST_PARCEL = + genFastPairManageAccountDeviceRequestParcel(); + private static final FastPairAntispoofKeyDeviceMetadata + HAPPY_PATH_FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA = + genHappyPathFastPairAntispoofKeyDeviceMetadata(); + + @Captor private ArgumentCaptor + mFastPairEligibleAccountParcelsArgumentCaptor; + @Captor private ArgumentCaptor + mFastPairAccountKeyDeviceMetadataParcelsArgumentCaptor; + + @Mock private FastPairDataProviderService mMockFastPairDataProviderService; + @Mock private IFastPairAntispoofKeyDeviceMetadataCallback.Stub + mAntispoofKeyDeviceMetadataCallback; + @Mock private IFastPairAccountDevicesMetadataCallback.Stub mAccountDevicesMetadataCallback; + @Mock private IFastPairEligibleAccountsCallback.Stub mEligibleAccountsCallback; + @Mock private IFastPairManageAccountCallback.Stub mManageAccountCallback; + @Mock private IFastPairManageAccountDeviceCallback.Stub mManageAccountDeviceCallback; + + private MyHappyPathProvider mHappyPathFastPairDataProvider; + private MyErrorPathProvider mErrorPathFastPairDataProvider; + + @Before + public void setUp() throws Exception { + initMocks(this); + + mHappyPathFastPairDataProvider = + new MyHappyPathProvider(TAG, mMockFastPairDataProviderService); + mErrorPathFastPairDataProvider = + new MyErrorPathProvider(TAG, mMockFastPairDataProviderService); + } + + @Test + @SdkSuppress(minSdkVersion = 32, codeName = "T") + public void testHappyPathLoadFastPairAntispoofKeyDeviceMetadata() throws Exception { + // AOSP sends calls to OEM via Parcelable. + mHappyPathFastPairDataProvider.asProvider().loadFastPairAntispoofKeyDeviceMetadata( + FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA_REQUEST_PARCEL, + mAntispoofKeyDeviceMetadataCallback); + + // OEM receives request and verifies that it is as expected. + final ArgumentCaptor + fastPairAntispoofKeyDeviceMetadataRequestCaptor = + ArgumentCaptor.forClass( + FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest.class + ); + verify(mMockFastPairDataProviderService).onLoadFastPairAntispoofKeyDeviceMetadata( + fastPairAntispoofKeyDeviceMetadataRequestCaptor.capture(), + any(FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataCallback.class)); + ensureHappyPathAsExpected(fastPairAntispoofKeyDeviceMetadataRequestCaptor.getValue()); + + // AOSP receives responses and verifies that it is as expected. + final ArgumentCaptor + fastPairAntispoofKeyDeviceMetadataParcelCaptor = + ArgumentCaptor.forClass(FastPairAntispoofKeyDeviceMetadataParcel.class); + verify(mAntispoofKeyDeviceMetadataCallback).onFastPairAntispoofKeyDeviceMetadataReceived( + fastPairAntispoofKeyDeviceMetadataParcelCaptor.capture()); + ensureHappyPathAsExpected(fastPairAntispoofKeyDeviceMetadataParcelCaptor.getValue()); + } + + @Test + @SdkSuppress(minSdkVersion = 32, codeName = "T") + public void testHappyPathLoadFastPairAccountDevicesMetadata() throws Exception { + // AOSP sends calls to OEM via Parcelable. + mHappyPathFastPairDataProvider.asProvider().loadFastPairAccountDevicesMetadata( + FAST_PAIR_ACCOUNT_DEVICES_METADATA_REQUEST_PARCEL, + mAccountDevicesMetadataCallback); + + // OEM receives request and verifies that it is as expected. + final ArgumentCaptor + fastPairAccountDevicesMetadataRequestCaptor = + ArgumentCaptor.forClass( + FastPairDataProviderService.FastPairAccountDevicesMetadataRequest.class); + verify(mMockFastPairDataProviderService).onLoadFastPairAccountDevicesMetadata( + fastPairAccountDevicesMetadataRequestCaptor.capture(), + any(FastPairDataProviderService.FastPairAccountDevicesMetadataCallback.class)); + ensureHappyPathAsExpected(fastPairAccountDevicesMetadataRequestCaptor.getValue()); + + // AOSP receives responses and verifies that it is as expected. + verify(mAccountDevicesMetadataCallback).onFastPairAccountDevicesMetadataReceived( + mFastPairAccountKeyDeviceMetadataParcelsArgumentCaptor.capture()); + ensureHappyPathAsExpected( + mFastPairAccountKeyDeviceMetadataParcelsArgumentCaptor.getValue()); + } + + @Test + @SdkSuppress(minSdkVersion = 32, codeName = "T") + public void testHappyPathLoadFastPairEligibleAccounts() throws Exception { + // AOSP sends calls to OEM via Parcelable. + mHappyPathFastPairDataProvider.asProvider().loadFastPairEligibleAccounts( + FAST_PAIR_ELIGIBLE_ACCOUNTS_REQUEST_PARCEL, + mEligibleAccountsCallback); + + // OEM receives request and verifies that it is as expected. + final ArgumentCaptor + fastPairEligibleAccountsRequestCaptor = + ArgumentCaptor.forClass( + FastPairDataProviderService.FastPairEligibleAccountsRequest.class); + verify(mMockFastPairDataProviderService).onLoadFastPairEligibleAccounts( + fastPairEligibleAccountsRequestCaptor.capture(), + any(FastPairDataProviderService.FastPairEligibleAccountsCallback.class)); + ensureHappyPathAsExpected(fastPairEligibleAccountsRequestCaptor.getValue()); + + // AOSP receives responses and verifies that it is as expected. + verify(mEligibleAccountsCallback).onFastPairEligibleAccountsReceived( + mFastPairEligibleAccountParcelsArgumentCaptor.capture()); + ensureHappyPathAsExpected(mFastPairEligibleAccountParcelsArgumentCaptor.getValue()); + } + + @Test + @SdkSuppress(minSdkVersion = 32, codeName = "T") + public void testHappyPathManageFastPairAccount() throws Exception { + // AOSP sends calls to OEM via Parcelable. + mHappyPathFastPairDataProvider.asProvider().manageFastPairAccount( + FAST_PAIR_MANAGE_ACCOUNT_REQUEST_PARCEL, + mManageAccountCallback); + + // OEM receives request and verifies that it is as expected. + final ArgumentCaptor + fastPairManageAccountRequestCaptor = + ArgumentCaptor.forClass( + FastPairDataProviderService.FastPairManageAccountRequest.class); + verify(mMockFastPairDataProviderService).onManageFastPairAccount( + fastPairManageAccountRequestCaptor.capture(), + any(FastPairDataProviderService.FastPairManageActionCallback.class)); + ensureHappyPathAsExpected(fastPairManageAccountRequestCaptor.getValue()); + + // AOSP receives SUCCESS response. + verify(mManageAccountCallback).onSuccess(); + } + + @Test + @SdkSuppress(minSdkVersion = 32, codeName = "T") + public void testHappyPathManageFastPairAccountDevice() throws Exception { + // AOSP sends calls to OEM via Parcelable. + mHappyPathFastPairDataProvider.asProvider().manageFastPairAccountDevice( + FAST_PAIR_MANAGE_ACCOUNT_DEVICE_REQUEST_PARCEL, + mManageAccountDeviceCallback); + + // OEM receives request and verifies that it is as expected. + final ArgumentCaptor + fastPairManageAccountDeviceRequestCaptor = + ArgumentCaptor.forClass( + FastPairDataProviderService.FastPairManageAccountDeviceRequest.class); + verify(mMockFastPairDataProviderService).onManageFastPairAccountDevice( + fastPairManageAccountDeviceRequestCaptor.capture(), + any(FastPairDataProviderService.FastPairManageActionCallback.class)); + ensureHappyPathAsExpected(fastPairManageAccountDeviceRequestCaptor.getValue()); + + // AOSP receives SUCCESS response. + verify(mManageAccountDeviceCallback).onSuccess(); + } + + @Test + @SdkSuppress(minSdkVersion = 32, codeName = "T") + public void testErrorPathLoadFastPairAntispoofKeyDeviceMetadata() throws Exception { + mErrorPathFastPairDataProvider.asProvider().loadFastPairAntispoofKeyDeviceMetadata( + FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA_REQUEST_PARCEL, + mAntispoofKeyDeviceMetadataCallback); + verify(mMockFastPairDataProviderService).onLoadFastPairAntispoofKeyDeviceMetadata( + any(FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest.class), + any(FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataCallback.class)); + verify(mAntispoofKeyDeviceMetadataCallback).onError( + eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING)); + } + + @Test + @SdkSuppress(minSdkVersion = 32, codeName = "T") + public void testErrorPathLoadFastPairAccountDevicesMetadata() throws Exception { + mErrorPathFastPairDataProvider.asProvider().loadFastPairAccountDevicesMetadata( + FAST_PAIR_ACCOUNT_DEVICES_METADATA_REQUEST_PARCEL, + mAccountDevicesMetadataCallback); + verify(mMockFastPairDataProviderService).onLoadFastPairAccountDevicesMetadata( + any(FastPairDataProviderService.FastPairAccountDevicesMetadataRequest.class), + any(FastPairDataProviderService.FastPairAccountDevicesMetadataCallback.class)); + verify(mAccountDevicesMetadataCallback).onError( + eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING)); + } + + @Test + @SdkSuppress(minSdkVersion = 32, codeName = "T") + public void testErrorPathLoadFastPairEligibleAccounts() throws Exception { + mErrorPathFastPairDataProvider.asProvider().loadFastPairEligibleAccounts( + FAST_PAIR_ELIGIBLE_ACCOUNTS_REQUEST_PARCEL, + mEligibleAccountsCallback); + verify(mMockFastPairDataProviderService).onLoadFastPairEligibleAccounts( + any(FastPairDataProviderService.FastPairEligibleAccountsRequest.class), + any(FastPairDataProviderService.FastPairEligibleAccountsCallback.class)); + verify(mEligibleAccountsCallback).onError( + eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING)); + } + + @Test + @SdkSuppress(minSdkVersion = 32, codeName = "T") + public void testErrorPathManageFastPairAccount() throws Exception { + mErrorPathFastPairDataProvider.asProvider().manageFastPairAccount( + FAST_PAIR_MANAGE_ACCOUNT_REQUEST_PARCEL, + mManageAccountCallback); + verify(mMockFastPairDataProviderService).onManageFastPairAccount( + any(FastPairDataProviderService.FastPairManageAccountRequest.class), + any(FastPairDataProviderService.FastPairManageActionCallback.class)); + verify(mManageAccountCallback).onError(eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING)); + } + + @Test + @SdkSuppress(minSdkVersion = 32, codeName = "T") + public void testErrorPathManageFastPairAccountDevice() throws Exception { + mErrorPathFastPairDataProvider.asProvider().manageFastPairAccountDevice( + FAST_PAIR_MANAGE_ACCOUNT_DEVICE_REQUEST_PARCEL, + mManageAccountDeviceCallback); + verify(mMockFastPairDataProviderService).onManageFastPairAccountDevice( + any(FastPairDataProviderService.FastPairManageAccountDeviceRequest.class), + any(FastPairDataProviderService.FastPairManageActionCallback.class)); + verify(mManageAccountDeviceCallback).onError(eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING)); + } + + public static class MyHappyPathProvider extends FastPairDataProviderService { + + private final FastPairDataProviderService mMockFastPairDataProviderService; + + public MyHappyPathProvider(@NonNull String tag, FastPairDataProviderService mock) { + super(tag); + mMockFastPairDataProviderService = mock; + } + + public IFastPairDataProvider asProvider() { + Intent intent = new Intent(); + return IFastPairDataProvider.Stub.asInterface(onBind(intent)); + } + + @Override + public void onLoadFastPairAntispoofKeyDeviceMetadata( + @NonNull FastPairAntispoofKeyDeviceMetadataRequest request, + @NonNull FastPairAntispoofKeyDeviceMetadataCallback callback) { + mMockFastPairDataProviderService.onLoadFastPairAntispoofKeyDeviceMetadata( + request, callback); + callback.onFastPairAntispoofKeyDeviceMetadataReceived( + HAPPY_PATH_FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA); + } + + @Override + public void onLoadFastPairAccountDevicesMetadata( + @NonNull FastPairAccountDevicesMetadataRequest request, + @NonNull FastPairAccountDevicesMetadataCallback callback) { + mMockFastPairDataProviderService.onLoadFastPairAccountDevicesMetadata( + request, callback); + callback.onFastPairAccountDevicesMetadataReceived(FAST_PAIR_ACCOUNT_DEVICES_METADATA); + } + + @Override + public void onLoadFastPairEligibleAccounts( + @NonNull FastPairEligibleAccountsRequest request, + @NonNull FastPairEligibleAccountsCallback callback) { + mMockFastPairDataProviderService.onLoadFastPairEligibleAccounts( + request, callback); + callback.onFastPairEligibleAccountsReceived(ELIGIBLE_ACCOUNTS); + } + + @Override + public void onManageFastPairAccount( + @NonNull FastPairManageAccountRequest request, + @NonNull FastPairManageActionCallback callback) { + mMockFastPairDataProviderService.onManageFastPairAccount(request, callback); + callback.onSuccess(); + } + + @Override + public void onManageFastPairAccountDevice( + @NonNull FastPairManageAccountDeviceRequest request, + @NonNull FastPairManageActionCallback callback) { + mMockFastPairDataProviderService.onManageFastPairAccountDevice(request, callback); + callback.onSuccess(); + } + } + + public static class MyErrorPathProvider extends FastPairDataProviderService { + + private final FastPairDataProviderService mMockFastPairDataProviderService; + + public MyErrorPathProvider(@NonNull String tag, FastPairDataProviderService mock) { + super(tag); + mMockFastPairDataProviderService = mock; + } + + public IFastPairDataProvider asProvider() { + Intent intent = new Intent(); + return IFastPairDataProvider.Stub.asInterface(onBind(intent)); + } + + @Override + public void onLoadFastPairAntispoofKeyDeviceMetadata( + @NonNull FastPairAntispoofKeyDeviceMetadataRequest request, + @NonNull FastPairAntispoofKeyDeviceMetadataCallback callback) { + mMockFastPairDataProviderService.onLoadFastPairAntispoofKeyDeviceMetadata( + request, callback); + callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING); + } + + @Override + public void onLoadFastPairAccountDevicesMetadata( + @NonNull FastPairAccountDevicesMetadataRequest request, + @NonNull FastPairAccountDevicesMetadataCallback callback) { + mMockFastPairDataProviderService.onLoadFastPairAccountDevicesMetadata( + request, callback); + callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING); + } + + @Override + public void onLoadFastPairEligibleAccounts( + @NonNull FastPairEligibleAccountsRequest request, + @NonNull FastPairEligibleAccountsCallback callback) { + mMockFastPairDataProviderService.onLoadFastPairEligibleAccounts(request, callback); + callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING); + } + + @Override + public void onManageFastPairAccount( + @NonNull FastPairManageAccountRequest request, + @NonNull FastPairManageActionCallback callback) { + mMockFastPairDataProviderService.onManageFastPairAccount(request, callback); + callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING); + } + + @Override + public void onManageFastPairAccountDevice( + @NonNull FastPairManageAccountDeviceRequest request, + @NonNull FastPairManageActionCallback callback) { + mMockFastPairDataProviderService.onManageFastPairAccountDevice(request, callback); + callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING); + } + } + + /* Generates AntispoofKeyDeviceMetadataRequestParcel. */ + private static FastPairAntispoofKeyDeviceMetadataRequestParcel + genFastPairAntispoofKeyDeviceMetadataRequestParcel() { + FastPairAntispoofKeyDeviceMetadataRequestParcel requestParcel = + new FastPairAntispoofKeyDeviceMetadataRequestParcel(); + requestParcel.modelId = REQUEST_MODEL_ID; + + return requestParcel; + } + + /* Generates AccountDevicesMetadataRequestParcel. */ + private static FastPairAccountDevicesMetadataRequestParcel + genFastPairAccountDevicesMetadataRequestParcel() { + FastPairAccountDevicesMetadataRequestParcel requestParcel = + new FastPairAccountDevicesMetadataRequestParcel(); + + requestParcel.account = ACCOUNTDEVICES_METADATA_ACCOUNT; + requestParcel.deviceAccountKeys = new ByteArrayParcel[NUM_ACCOUNT_DEVICES]; + requestParcel.deviceAccountKeys[0] = new ByteArrayParcel(); + requestParcel.deviceAccountKeys[1] = new ByteArrayParcel(); + requestParcel.deviceAccountKeys[0].byteArray = ACCOUNT_KEY; + requestParcel.deviceAccountKeys[1].byteArray = ACCOUNT_KEY_2; + + return requestParcel; + } + + /* Generates FastPairEligibleAccountsRequestParcel. */ + private static FastPairEligibleAccountsRequestParcel + genFastPairEligibleAccountsRequestParcel() { + FastPairEligibleAccountsRequestParcel requestParcel = + new FastPairEligibleAccountsRequestParcel(); + // No fields since FastPairEligibleAccountsRequestParcel is just a place holder now. + return requestParcel; + } + + /* Generates FastPairManageAccountRequestParcel. */ + private static FastPairManageAccountRequestParcel + genFastPairManageAccountRequestParcel() { + FastPairManageAccountRequestParcel requestParcel = + new FastPairManageAccountRequestParcel(); + requestParcel.account = MANAGE_ACCOUNT; + requestParcel.requestType = MANAGE_ACCOUNT_REQUEST_TYPE; + + return requestParcel; + } + + /* Generates FastPairManageAccountDeviceRequestParcel. */ + private static FastPairManageAccountDeviceRequestParcel + genFastPairManageAccountDeviceRequestParcel() { + FastPairManageAccountDeviceRequestParcel requestParcel = + new FastPairManageAccountDeviceRequestParcel(); + requestParcel.account = MANAGE_ACCOUNT; + requestParcel.requestType = MANAGE_ACCOUNT_REQUEST_TYPE; + requestParcel.bleAddress = BLE_ADDRESS; + requestParcel.accountKeyDeviceMetadata = + genHappyPathFastPairAccountkeyDeviceMetadataParcel(); + + return requestParcel; + } + + /* Generates Happy Path AntispoofKeyDeviceMetadata. */ + private static FastPairAntispoofKeyDeviceMetadata + genHappyPathFastPairAntispoofKeyDeviceMetadata() { + FastPairAntispoofKeyDeviceMetadata.Builder builder = + new FastPairAntispoofKeyDeviceMetadata.Builder(); + builder.setAntispoofPublicKey(ANTI_SPOOFING_KEY); + builder.setFastPairDeviceMetadata(genHappyPathFastPairDeviceMetadata()); + + return builder.build(); + } + + /* Generates Happy Path FastPairAccountKeyDeviceMetadata. */ + private static FastPairAccountKeyDeviceMetadata + genHappyPathFastPairAccountkeyDeviceMetadata() { + FastPairAccountKeyDeviceMetadata.Builder builder = + new FastPairAccountKeyDeviceMetadata.Builder(); + builder.setDeviceAccountKey(ACCOUNT_KEY); + builder.setFastPairDeviceMetadata(genHappyPathFastPairDeviceMetadata()); + builder.setSha256DeviceAccountKeyPublicAddress(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS); + builder.setFastPairDiscoveryItem(genHappyPathFastPairDiscoveryItem()); + + return builder.build(); + } + + /* Generates Happy Path FastPairAccountKeyDeviceMetadataParcel. */ + 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; + } + + /* Generates Happy Path DiscoveryItem. */ + private static FastPairDiscoveryItem genHappyPathFastPairDiscoveryItem() { + FastPairDiscoveryItem.Builder builder = new FastPairDiscoveryItem.Builder(); + + builder.setActionUrl(ACTION_URL); + builder.setActionUrlType(ACTION_URL_TYPE); + builder.setAppName(APP_NAME); + builder.setAttachmentType(ATTACHMENT_TYPE); + builder.setAuthenticationPublicKeySecp256r1(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1); + builder.setBleRecordBytes(BLE_RECORD_BYTES); + builder.setDebugCategory(DEBUG_CATEGORY); + builder.setDebugMessage(DEBUG_MESSAGE); + builder.setDescription(DESCRIPTION); + builder.setDeviceName(DEVICE_NAME); + builder.setDisplayUrl(DISPLAY_URL); + builder.setEntityId(ENTITY_ID); + builder.setFeatureGraphicUrl(FEATURE_GRAPHIC_URL); + builder.setFirstObservationTimestampMillis(FIRST_OBSERVATION_TIMESTAMP_MILLIS); + builder.setGroupId(GROUP_ID); + builder.setIconFfeUrl(ICON_FIFE_URL); + builder.setIconPng(ICON_PNG); + builder.setId(ID); + builder.setLastObservationTimestampMillis(LAST_OBSERVATION_TIMESTAMP_MILLIS); + builder.setLastUserExperience(LAST_USER_EXPERIENCE); + builder.setLostMillis(LOST_MILLIS); + builder.setMacAddress(MAC_ADDRESS); + builder.setPackageName(PACKAGE_NAME); + builder.setPendingAppInstallTimestampMillis(PENDING_APP_INSTALL_TIMESTAMP_MILLIS); + builder.setRssi(RSSI); + builder.setState(STATE); + builder.setTitle(TITLE); + builder.setTriggerId(TRIGGER_ID); + builder.setTxPower(TX_POWER); + builder.setType(TYPE); + + return builder.build(); + } + + /* Generates Happy Path DiscoveryItemParcel. */ + private static FastPairDiscoveryItemParcel genHappyPathFastPairDiscoveryItemParcel() { + FastPairDiscoveryItemParcel parcel = new FastPairDiscoveryItemParcel(); + + parcel.actionUrl = ACTION_URL; + parcel.actionUrlType = ACTION_URL_TYPE; + parcel.appName = APP_NAME; + parcel.attachmentType = ATTACHMENT_TYPE; + parcel.authenticationPublicKeySecp256r1 = AUTHENTICATION_PUBLIC_KEY_SEC_P256R1; + parcel.bleRecordBytes = BLE_RECORD_BYTES; + parcel.debugCategory = DEBUG_CATEGORY; + parcel.debugMessage = DEBUG_MESSAGE; + parcel.description = DESCRIPTION; + parcel.deviceName = DEVICE_NAME; + parcel.displayUrl = DISPLAY_URL; + parcel.entityId = ENTITY_ID; + parcel.featureGraphicUrl = FEATURE_GRAPHIC_URL; + parcel.firstObservationTimestampMillis = FIRST_OBSERVATION_TIMESTAMP_MILLIS; + parcel.groupId = GROUP_ID; + parcel.iconFifeUrl = ICON_FIFE_URL; + parcel.iconPng = ICON_PNG; + parcel.id = ID; + parcel.lastObservationTimestampMillis = LAST_OBSERVATION_TIMESTAMP_MILLIS; + parcel.lastUserExperience = LAST_USER_EXPERIENCE; + parcel.lostMillis = LOST_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; + parcel.type = TYPE; + + return parcel; + } + + /* Generates Happy Path DeviceMetadata. */ + private static FastPairDeviceMetadata genHappyPathFastPairDeviceMetadata() { + FastPairDeviceMetadata.Builder builder = new FastPairDeviceMetadata.Builder(); + builder.setAssistantSetupHalfSheet(ASSISTANT_SETUP_HALFSHEET); + builder.setAssistantSetupNotification(ASSISTANT_SETUP_NOTIFICATION); + builder.setBleTxPower(BLE_TX_POWER); + builder.setConfirmPinDescription(CONFIRM_PIN_DESCRIPTION); + builder.setConfirmPinTitle(CONFIRM_PIN_TITLE); + builder.setConnectSuccessCompanionAppInstalled(CONNECT_SUCCESS_COMPANION_APP_INSTALLED); + builder.setConnectSuccessCompanionAppNotInstalled( + CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED); + builder.setDeviceType(DEVICE_TYPE); + builder.setDownloadCompanionAppDescription(DOWNLOAD_COMPANION_APP_DESCRIPTION); + builder.setFailConnectGoToSettingsDescription(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION); + builder.setFastPairTvConnectDeviceNoAccountDescription( + FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION); + builder.setImage(IMAGE); + builder.setImageUrl(IMAGE_URL); + builder.setInitialNotificationDescription(INITIAL_NOTIFICATION_DESCRIPTION); + builder.setInitialNotificationDescriptionNoAccount( + INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT); + builder.setInitialPairingDescription(INITIAL_PAIRING_DESCRIPTION); + builder.setIntentUri(INTENT_URI); + builder.setLocale(LOCALE); + builder.setName(NAME); + builder.setOpenCompanionAppDescription(OPEN_COMPANION_APP_DESCRIPTION); + builder.setRetroactivePairingDescription(RETRO_ACTIVE_PAIRING_DESCRIPTION); + builder.setSubsequentPairingDescription(SUBSEQUENT_PAIRING_DESCRIPTION); + builder.setSyncContactsDescription(SYNC_CONTACT_DESCRPTION); + builder.setSyncContactsTitle(SYNC_CONTACTS_TITLE); + builder.setSyncSmsDescription(SYNC_SMS_DESCRIPTION); + builder.setSyncSmsTitle(SYNC_SMS_TITLE); + builder.setTriggerDistance(TRIGGER_DISTANCE); + builder.setTrueWirelessImageUrlCase(TRUE_WIRELESS_IMAGE_URL_CASE); + builder.setTrueWirelessImageUrlLeftBud(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD); + builder.setTrueWirelessImageUrlRightBud(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD); + builder.setUnableToConnectDescription(UNABLE_TO_CONNECT_DESCRIPTION); + builder.setUnableToConnectTitle(UNABLE_TO_CONNECT_TITLE); + builder.setUpdateCompanionAppDescription(UPDATE_COMPANION_APP_DESCRIPTION); + builder.setWaitLaunchCompanionAppDescription(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION); + + return builder.build(); + } + + /* Generates Happy Path DeviceMetadataParcel. */ + private static FastPairDeviceMetadataParcel genHappyPathFastPairDeviceMetadataParcel() { + FastPairDeviceMetadataParcel parcel = new FastPairDeviceMetadataParcel(); + + parcel.assistantSetupHalfSheet = ASSISTANT_SETUP_HALFSHEET; + parcel.assistantSetupNotification = ASSISTANT_SETUP_NOTIFICATION; + parcel.bleTxPower = BLE_TX_POWER; + parcel.confirmPinDescription = CONFIRM_PIN_DESCRIPTION; + parcel.confirmPinTitle = CONFIRM_PIN_TITLE; + 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.fastPairTvConnectDeviceNoAccountDescription = + FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_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.locale = LOCALE; + parcel.name = NAME; + parcel.openCompanionAppDescription = OPEN_COMPANION_APP_DESCRIPTION; + parcel.retroactivePairingDescription = RETRO_ACTIVE_PAIRING_DESCRIPTION; + parcel.subsequentPairingDescription = SUBSEQUENT_PAIRING_DESCRIPTION; + parcel.syncContactsDescription = SYNC_CONTACT_DESCRPTION; + parcel.syncContactsTitle = SYNC_CONTACTS_TITLE; + parcel.syncSmsDescription = SYNC_SMS_DESCRIPTION; + parcel.syncSmsTitle = SYNC_SMS_TITLE; + 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; + } + + /* Generates Happy Path FastPairEligibleAccount. */ + private static FastPairEligibleAccount genHappyPathFastPairEligibleAccount( + Account account, boolean optIn) { + FastPairEligibleAccount.Builder builder = new FastPairEligibleAccount.Builder(); + builder.setAccount(account); + builder.setOptIn(optIn); + + return builder.build(); + } + + /* Verifies Happy Path AntispoofKeyDeviceMetadataRequest. */ + private static void ensureHappyPathAsExpected( + FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest request) { + assertThat(request.getModelId()).isEqualTo(REQUEST_MODEL_ID); + } + + /* Verifies Happy Path AccountDevicesMetadataRequest. */ + private static void ensureHappyPathAsExpected( + FastPairDataProviderService.FastPairAccountDevicesMetadataRequest request) { + assertThat(request.getAccount()).isEqualTo(ACCOUNTDEVICES_METADATA_ACCOUNT); + assertThat(request.getDeviceAccountKeys().size()).isEqualTo(ACCOUNTKEY_DEVICE_NUM); + assertThat(request.getDeviceAccountKeys()).contains(ACCOUNT_KEY); + assertThat(request.getDeviceAccountKeys()).contains(ACCOUNT_KEY_2); + } + + /* Verifies Happy Path FastPairEligibleAccountsRequest. */ + @SuppressWarnings("UnusedVariable") + private static void ensureHappyPathAsExpected( + FastPairDataProviderService.FastPairEligibleAccountsRequest request) { + // No fields since FastPairEligibleAccountsRequest is just a place holder now. + } + + /* Verifies Happy Path FastPairManageAccountRequest. */ + private static void ensureHappyPathAsExpected( + FastPairDataProviderService.FastPairManageAccountRequest request) { + assertThat(request.getAccount()).isEqualTo(MANAGE_ACCOUNT); + assertThat(request.getRequestType()).isEqualTo(MANAGE_ACCOUNT_REQUEST_TYPE); + } + + /* Verifies Happy Path FastPairManageAccountDeviceRequest. */ + private static void ensureHappyPathAsExpected( + FastPairDataProviderService.FastPairManageAccountDeviceRequest request) { + assertThat(request.getAccount()).isEqualTo(MANAGE_ACCOUNT); + assertThat(request.getRequestType()).isEqualTo(MANAGE_ACCOUNT_REQUEST_TYPE); + assertThat(request.getBleAddress()).isEqualTo(BLE_ADDRESS); + ensureHappyPathAsExpected(request.getAccountKeyDeviceMetadata()); + } + + /* Verifies Happy Path AntispoofKeyDeviceMetadataParcel. */ + private static void ensureHappyPathAsExpected( + FastPairAntispoofKeyDeviceMetadataParcel metadataParcel) { + assertThat(metadataParcel).isNotNull(); + assertThat(metadataParcel.antispoofPublicKey).isEqualTo(ANTI_SPOOFING_KEY); + ensureHappyPathAsExpected(metadataParcel.deviceMetadata); + } + + /* Verifies Happy Path FastPairAccountKeyDeviceMetadataParcel[]. */ + private static void ensureHappyPathAsExpected( + FastPairAccountKeyDeviceMetadataParcel[] metadataParcels) { + assertThat(metadataParcels).isNotNull(); + assertThat(metadataParcels).hasLength(ACCOUNTKEY_DEVICE_NUM); + for (FastPairAccountKeyDeviceMetadataParcel parcel: metadataParcels) { + ensureHappyPathAsExpected(parcel); + } + } + + /* Verifies Happy Path FastPairAccountKeyDeviceMetadataParcel. */ + 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); + } + + /* Verifies Happy Path FastPairAccountKeyDeviceMetadata. */ + private static void ensureHappyPathAsExpected( + FastPairAccountKeyDeviceMetadata metadata) { + assertThat(metadata.getDeviceAccountKey()).isEqualTo(ACCOUNT_KEY); + assertThat(metadata.getSha256DeviceAccountKeyPublicAddress()) + .isEqualTo(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS); + ensureHappyPathAsExpected(metadata.getFastPairDeviceMetadata()); + ensureHappyPathAsExpected(metadata.getFastPairDiscoveryItem()); + } + + /* Verifies Happy Path DeviceMetadataParcel. */ + private static void ensureHappyPathAsExpected(FastPairDeviceMetadataParcel metadataParcel) { + assertThat(metadataParcel).isNotNull(); + + assertThat(metadataParcel.assistantSetupHalfSheet).isEqualTo(ASSISTANT_SETUP_HALFSHEET); + assertThat(metadataParcel.assistantSetupNotification).isEqualTo( + ASSISTANT_SETUP_NOTIFICATION); + + assertThat(metadataParcel.bleTxPower).isEqualTo(BLE_TX_POWER); + + assertThat(metadataParcel.confirmPinDescription).isEqualTo(CONFIRM_PIN_DESCRIPTION); + assertThat(metadataParcel.confirmPinTitle).isEqualTo(CONFIRM_PIN_TITLE); + 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.downloadCompanionAppDescription).isEqualTo( + DOWNLOAD_COMPANION_APP_DESCRIPTION); + + assertThat(metadataParcel.failConnectGoToSettingsDescription).isEqualTo( + FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION); + assertThat(metadataParcel.fastPairTvConnectDeviceNoAccountDescription).isEqualTo( + FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION); + + assertThat(metadataParcel.image).isEqualTo(IMAGE); + assertThat(metadataParcel.imageUrl).isEqualTo(IMAGE_URL); + 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.intentUri).isEqualTo(INTENT_URI); + + assertThat(metadataParcel.locale).isEqualTo(LOCALE); + assertThat(metadataParcel.name).isEqualTo(NAME); + + assertThat(metadataParcel.openCompanionAppDescription).isEqualTo( + OPEN_COMPANION_APP_DESCRIPTION); + + assertThat(metadataParcel.retroactivePairingDescription).isEqualTo( + RETRO_ACTIVE_PAIRING_DESCRIPTION); + + assertThat(metadataParcel.subsequentPairingDescription).isEqualTo( + SUBSEQUENT_PAIRING_DESCRIPTION); + assertThat(metadataParcel.syncContactsDescription).isEqualTo(SYNC_CONTACT_DESCRPTION); + assertThat(metadataParcel.syncContactsTitle).isEqualTo(SYNC_CONTACTS_TITLE); + assertThat(metadataParcel.syncSmsDescription).isEqualTo(SYNC_SMS_DESCRIPTION); + assertThat(metadataParcel.syncSmsTitle).isEqualTo(SYNC_SMS_TITLE); + + assertThat(metadataParcel.triggerDistance).isWithin(DELTA).of(TRIGGER_DISTANCE); + 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.unableToConnectDescription).isEqualTo( + UNABLE_TO_CONNECT_DESCRIPTION); + assertThat(metadataParcel.unableToConnectTitle).isEqualTo(UNABLE_TO_CONNECT_TITLE); + assertThat(metadataParcel.updateCompanionAppDescription).isEqualTo( + UPDATE_COMPANION_APP_DESCRIPTION); + + assertThat(metadataParcel.waitLaunchCompanionAppDescription).isEqualTo( + WAIT_LAUNCH_COMPANION_APP_DESCRIPTION); + } + + /* Verifies Happy Path DeviceMetadata. */ + private static void ensureHappyPathAsExpected(FastPairDeviceMetadata metadata) { + assertThat(metadata.getAssistantSetupHalfSheet()).isEqualTo(ASSISTANT_SETUP_HALFSHEET); + assertThat(metadata.getAssistantSetupNotification()) + .isEqualTo(ASSISTANT_SETUP_NOTIFICATION); + assertThat(metadata.getBleTxPower()).isEqualTo(BLE_TX_POWER); + assertThat(metadata.getConfirmPinDescription()).isEqualTo(CONFIRM_PIN_DESCRIPTION); + assertThat(metadata.getConfirmPinTitle()).isEqualTo(CONFIRM_PIN_TITLE); + assertThat(metadata.getConnectSuccessCompanionAppInstalled()) + .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_INSTALLED); + assertThat(metadata.getConnectSuccessCompanionAppNotInstalled()) + .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED); + assertThat(metadata.getDeviceType()).isEqualTo(DEVICE_TYPE); + assertThat(metadata.getDownloadCompanionAppDescription()) + .isEqualTo(DOWNLOAD_COMPANION_APP_DESCRIPTION); + assertThat(metadata.getFailConnectGoToSettingsDescription()) + .isEqualTo(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION); + assertThat(metadata.getFastPairTvConnectDeviceNoAccountDescription()) + .isEqualTo(FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION); + assertThat(metadata.getImage()).isEqualTo(IMAGE); + assertThat(metadata.getImageUrl()).isEqualTo(IMAGE_URL); + assertThat(metadata.getInitialNotificationDescription()) + .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION); + assertThat(metadata.getInitialNotificationDescriptionNoAccount()) + .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT); + assertThat(metadata.getInitialPairingDescription()).isEqualTo(INITIAL_PAIRING_DESCRIPTION); + assertThat(metadata.getIntentUri()).isEqualTo(INTENT_URI); + assertThat(metadata.getLocale()).isEqualTo(LOCALE); + assertThat(metadata.getName()).isEqualTo(NAME); + assertThat(metadata.getOpenCompanionAppDescription()) + .isEqualTo(OPEN_COMPANION_APP_DESCRIPTION); + assertThat(metadata.getRetroactivePairingDescription()) + .isEqualTo(RETRO_ACTIVE_PAIRING_DESCRIPTION); + assertThat(metadata.getSubsequentPairingDescription()) + .isEqualTo(SUBSEQUENT_PAIRING_DESCRIPTION); + assertThat(metadata.getSyncContactsDescription()).isEqualTo(SYNC_CONTACT_DESCRPTION); + assertThat(metadata.getSyncContactsTitle()).isEqualTo(SYNC_CONTACTS_TITLE); + assertThat(metadata.getSyncSmsDescription()).isEqualTo(SYNC_SMS_DESCRIPTION); + assertThat(metadata.getSyncSmsTitle()).isEqualTo(SYNC_SMS_TITLE); + assertThat(metadata.getTriggerDistance()).isWithin(DELTA).of(TRIGGER_DISTANCE); + assertThat(metadata.getTrueWirelessImageUrlCase()).isEqualTo(TRUE_WIRELESS_IMAGE_URL_CASE); + assertThat(metadata.getTrueWirelessImageUrlLeftBud()) + .isEqualTo(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD); + assertThat(metadata.getTrueWirelessImageUrlRightBud()) + .isEqualTo(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD); + assertThat(metadata.getUnableToConnectDescription()) + .isEqualTo(UNABLE_TO_CONNECT_DESCRIPTION); + assertThat(metadata.getUnableToConnectTitle()).isEqualTo(UNABLE_TO_CONNECT_TITLE); + assertThat(metadata.getUpdateCompanionAppDescription()) + .isEqualTo(UPDATE_COMPANION_APP_DESCRIPTION); + assertThat(metadata.getWaitLaunchCompanionAppDescription()) + .isEqualTo(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION); + } + + /* Verifies Happy Path FastPairDiscoveryItemParcel. */ + 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.attachmentType).isEqualTo(ATTACHMENT_TYPE); + assertThat(itemParcel.authenticationPublicKeySecp256r1) + .isEqualTo(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1); + assertThat(itemParcel.bleRecordBytes).isEqualTo(BLE_RECORD_BYTES); + assertThat(itemParcel.debugCategory).isEqualTo(DEBUG_CATEGORY); + assertThat(itemParcel.debugMessage).isEqualTo(DEBUG_MESSAGE); + assertThat(itemParcel.description).isEqualTo(DESCRIPTION); + assertThat(itemParcel.deviceName).isEqualTo(DEVICE_NAME); + assertThat(itemParcel.displayUrl).isEqualTo(DISPLAY_URL); + assertThat(itemParcel.entityId).isEqualTo(ENTITY_ID); + assertThat(itemParcel.featureGraphicUrl).isEqualTo(FEATURE_GRAPHIC_URL); + assertThat(itemParcel.firstObservationTimestampMillis) + .isEqualTo(FIRST_OBSERVATION_TIMESTAMP_MILLIS); + assertThat(itemParcel.groupId).isEqualTo(GROUP_ID); + 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.lastUserExperience).isEqualTo(LAST_USER_EXPERIENCE); + assertThat(itemParcel.lostMillis).isEqualTo(LOST_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); + assertThat(itemParcel.type).isEqualTo(TYPE); + } + + /* Verifies Happy Path FastPairDiscoveryItem. */ + private static void ensureHappyPathAsExpected(FastPairDiscoveryItem item) { + assertThat(item.getActionUrl()).isEqualTo(ACTION_URL); + assertThat(item.getActionUrlType()).isEqualTo(ACTION_URL_TYPE); + assertThat(item.getAppName()).isEqualTo(APP_NAME); + assertThat(item.getAttachmentType()).isEqualTo(ATTACHMENT_TYPE); + assertThat(item.getAuthenticationPublicKeySecp256r1()) + .isEqualTo(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1); + assertThat(item.getBleRecordBytes()).isEqualTo(BLE_RECORD_BYTES); + assertThat(item.getDebugCategory()).isEqualTo(DEBUG_CATEGORY); + assertThat(item.getDebugMessage()).isEqualTo(DEBUG_MESSAGE); + assertThat(item.getDescription()).isEqualTo(DESCRIPTION); + assertThat(item.getDeviceName()).isEqualTo(DEVICE_NAME); + assertThat(item.getDisplayUrl()).isEqualTo(DISPLAY_URL); + assertThat(item.getEntityId()).isEqualTo(ENTITY_ID); + assertThat(item.getFeatureGraphicUrl()).isEqualTo(FEATURE_GRAPHIC_URL); + assertThat(item.getFirstObservationTimestampMillis()) + .isEqualTo(FIRST_OBSERVATION_TIMESTAMP_MILLIS); + assertThat(item.getGroupId()).isEqualTo(GROUP_ID); + assertThat(item.getIconFfeUrl()).isEqualTo(ICON_FIFE_URL); + assertThat(item.getIconPng()).isEqualTo(ICON_PNG); + assertThat(item.getId()).isEqualTo(ID); + assertThat(item.getLastObservationTimestampMillis()) + .isEqualTo(LAST_OBSERVATION_TIMESTAMP_MILLIS); + assertThat(item.getLastUserExperience()).isEqualTo(LAST_USER_EXPERIENCE); + assertThat(item.getLostMillis()).isEqualTo(LOST_MILLIS); + assertThat(item.getMacAddress()).isEqualTo(MAC_ADDRESS); + assertThat(item.getPackageName()).isEqualTo(PACKAGE_NAME); + assertThat(item.getPendingAppInstallTimestampMillis()) + .isEqualTo(PENDING_APP_INSTALL_TIMESTAMP_MILLIS); + assertThat(item.getRssi()).isEqualTo(RSSI); + assertThat(item.getState()).isEqualTo(STATE); + assertThat(item.getTitle()).isEqualTo(TITLE); + assertThat(item.getTriggerId()).isEqualTo(TRIGGER_ID); + assertThat(item.getTxPower()).isEqualTo(TX_POWER); + assertThat(item.getType()).isEqualTo(TYPE); + } + + /* Verifies Happy Path EligibleAccountParcel[]. */ + private static void ensureHappyPathAsExpected(FastPairEligibleAccountParcel[] accountsParcel) { + assertThat(accountsParcel).hasLength(ELIGIBLE_ACCOUNTS_NUM); + + assertThat(accountsParcel[0].account).isEqualTo(ELIGIBLE_ACCOUNT_1); + assertThat(accountsParcel[0].optIn).isEqualTo(ELIGIBLE_ACCOUNT_1_OPT_IN); + + assertThat(accountsParcel[1].account).isEqualTo(ELIGIBLE_ACCOUNT_2); + assertThat(accountsParcel[1].optIn).isEqualTo(ELIGIBLE_ACCOUNT_2_OPT_IN); + } +} diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairEligibleAccountTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairEligibleAccountTest.java new file mode 100644 index 0000000000..0d91d4e591 --- /dev/null +++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/FastPairEligibleAccountTest.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 android.nearby.cts; + +import static com.google.common.truth.Truth.assertThat; + +import android.accounts.Account; +import android.nearby.FastPairEligibleAccount; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SdkSuppress; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class FastPairEligibleAccountTest { + + private static final Account ACCOUNT = new Account("abc@google.com", "type1"); + private static final Account ACCOUNT_NULL = null; + + private static final boolean OPT_IN_TRUE = true; + + @Test + @SdkSuppress(minSdkVersion = 32, codeName = "T") + public void testSetGetFastPairEligibleAccountNotNull() { + FastPairEligibleAccount eligibleAccount = + genFastPairEligibleAccount(ACCOUNT, OPT_IN_TRUE); + + assertThat(eligibleAccount.getAccount()).isEqualTo(ACCOUNT); + assertThat(eligibleAccount.isOptIn()).isEqualTo(OPT_IN_TRUE); + } + + @Test + @SdkSuppress(minSdkVersion = 32, codeName = "T") + public void testSetGetFastPairEligibleAccountNull() { + FastPairEligibleAccount eligibleAccount = + genFastPairEligibleAccount(ACCOUNT_NULL, OPT_IN_TRUE); + + assertThat(eligibleAccount.getAccount()).isEqualTo(ACCOUNT_NULL); + assertThat(eligibleAccount.isOptIn()).isEqualTo(OPT_IN_TRUE); + } + + /* Generates FastPairEligibleAccount. */ + private static FastPairEligibleAccount genFastPairEligibleAccount( + Account account, boolean optIn) { + FastPairEligibleAccount.Builder builder = new FastPairEligibleAccount.Builder(); + builder.setAccount(account); + builder.setOptIn(optIn); + + return builder.build(); + } +} 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 0000000000..82e6615ac5 --- /dev/null +++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java @@ -0,0 +1,138 @@ +/* + * 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.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 = 32, codeName = "T") + public void testToString() { + NearbyDeviceParcelable nearbyDeviceParcelable = + mBuilder.setFastPairModelId(null).setData(null).build(); + + assertThat(nearbyDeviceParcelable.toString()).isEqualTo( + "NearbyDeviceParcelable[name=testDevice, medium=BLE, rssi=-60, " + + "bluetoothAddress=" + + BLUETOOTH_ADDRESS + ", fastPairModelId=null, data=null]"); + } + + @Test + @SdkSuppress(minSdkVersion = 32, 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 = 32, 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 = 32, 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 = 32, 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 0000000000..f37800abde --- /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 0000000000..cf43cb1e09 --- /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 0000000000..cd61cad746 --- /dev/null +++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java @@ -0,0 +1,153 @@ +/* + * 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); + } + + @Test + @SdkSuppress(minSdkVersion = 32, codeName = "T") + public void testSettingsEnable() { + NearbyManager.setFastPairScanEnabled(mContext, false); + assertThat(NearbyManager.getFastPairScanEnabled(mContext, true)).isFalse(); + } + + 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 0000000000..1daa41080b --- /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 0000000000..5fefc6843b --- /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 0000000000..b7fe40adfe --- /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 0000000000..10b3cf2caf --- /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 = 32, 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 = 32, 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 0000000000..b4a34cc4ca --- /dev/null +++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java @@ -0,0 +1,104 @@ +/* + * 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(); + } +} 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 0000000000..019f8ecc4c --- /dev/null +++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java @@ -0,0 +1,199 @@ +/* + * 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_EXPOSURE_NOTIFICATION; +import static android.nearby.ScanRequest.SCAN_TYPE_FAST_PAIR; +import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE; +import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_SHARE; + +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_EXPOSURE_NOTIFICATION) + .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_NEARBY_SHARE) + .setScanMode(SCAN_MODE_BALANCED) + .setBleEnabled(true) + .setWorkSource(workSource) + .build(); + + assertThat(request.toString()).isEqualTo( + "Request[scanType=2, 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_SHARE)).isTrue(); + assertThat(ScanRequest.isValidScanType(SCAN_TYPE_NEARBY_PRESENCE)).isTrue(); + assertThat(ScanRequest.isValidScanType(SCAN_TYPE_EXPOSURE_NOTIFICATION)).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 0000000000..f4dbde2621 --- /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 0000000000..e3250f6266 --- /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 0000000000..00845f1aa5 --- /dev/null +++ b/nearby/tests/integration/privileged/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + 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 0000000000..af3f75f4f7 --- /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() + 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 0000000000..3b6337a336 --- /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() + 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 0000000000..53dbfb755d --- /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 0000000000..d73f6b2ca4 --- /dev/null +++ b/nearby/tests/integration/untrusted/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + 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 0000000000..c549073edb --- /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().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 { + 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 0000000000..aa6aa2ab96 --- /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't get Nearby service. */ + @Test + fun testContextGetNearbyService_fromUnTrustedApp_returnsNull() { + val appContext = ApplicationProvider.getApplicationContext() + assertThat(appContext.getSystemService(Context.NEARBY_SERVICE)).isNull() + } +} diff --git a/nearby/tests/multidevices/OWNERS b/nearby/tests/multidevices/OWNERS new file mode 100644 index 0000000000..f4dbde2621 --- /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 0000000000..579baa501c --- /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: true, + 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 0000000000..86c10b2fb5 --- /dev/null +++ b/nearby/tests/multidevices/clients/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nearby/tests/multidevices/clients/proguard.flags b/nearby/tests/multidevices/clients/proguard.flags new file mode 100644 index 0000000000..fd494a8f66 --- /dev/null +++ b/nearby/tests/multidevices/clients/proguard.flags @@ -0,0 +1,29 @@ +# Keep all snippet classes. +-keep class android.nearby.multidevices.** { + *; +} + +# Keep simulator reflection callback. +-keep class android.nearby.fastpair.provider.** { + *; +} + +# 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/FastPairProviderSimulatorController.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorController.kt new file mode 100644 index 0000000000..e700144182 --- /dev/null +++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/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 + +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 + +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 = BaseEncoding.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() + ) + + // TODO(b/222070055): Workaround the FATAL EXCEPTION after the end of initial pairing. + simulator!!.setSuppressSubsequentPairingNotification(true) + } + + fun getProviderSimulatorBleAddress() = simulator!!.bleAddress!! + + /** + * 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/FastPairProviderSimulatorSnippet.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorSnippet.kt new file mode 100644 index 0000000000..39edfe4b31 --- /dev/null +++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorSnippet.kt @@ -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.multidevices.fastpair.provider + +import android.annotation.TargetApi +import android.content.Context +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() + } +} diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/ProviderStatusEvents.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/ProviderStatusEvents.kt new file mode 100644 index 0000000000..efa4f021ce --- /dev/null +++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/ProviderStatusEvents.kt @@ -0,0 +1,49 @@ +/* + * 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 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) + } + } +} 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 0000000000..617eac1805 --- /dev/null +++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/FastPairSeekerSnippet.kt @@ -0,0 +1,148 @@ +/* + * 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.ui.CheckNearbyHalfSheetUiTest +import android.nearby.multidevices.fastpair.seeker.ui.DismissNearbyHalfSheetUiTest +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() + 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 Fast Pair 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 Fast Pair 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.sendAccountKeyDeviceMetadata(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.") + NearbyManager.setFastPairScanEnabled(appContext, enable) + } + + /** 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() + } + + /** Invokes when the snippet runner shutting down. */ + override fun shutdown() { + super.shutdown() + + Log.i("Resets the Fast Pair test data cache.") + fastPairTestDataManager.sendResetCache() + } +} \ No newline at end of file diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ScanCallbackEvents.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ScanCallbackEvents.kt new file mode 100644 index 0000000000..5385238a2c --- /dev/null +++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/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 + +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/data/FastPairTestDataManager.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/data/FastPairTestDataManager.kt new file mode 100644 index 0000000000..291aad8752 --- /dev/null +++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/data/FastPairTestDataManager.kt @@ -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 android.nearby.multidevices.fastpair.seeker.data + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.nearby.fastpair.seeker.* +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() + + /** 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 to local and remote cache. + * + * @param json a string of FastPairAccountKeyDeviceMetadata JSON array. + */ + fun sendAccountKeyDeviceMetadata(json: String) { + Intent().also { intent -> + intent.action = ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA + intent.putExtra(DATA_JSON_STRING_KEY, json) + context.sendBroadcast(intent) + } + testDataCache.putAccountKeyDeviceMetadata(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.putAccountKeyDeviceMetadata(json) + } + 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/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 0000000000..84b5e894d6 --- /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 0000000000..1d99d26e06 --- /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 0000000000..c94ff0181a --- /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/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 0000000000..a6d51cad55 --- /dev/null +++ b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_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: "NearbyFastPairSeekerSharedLib", + srcs: ["shared/**/*.kt"], + sdk_version: "test_current", + static_libs: [ + "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 0000000000..1d62f04c94 --- /dev/null +++ b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + 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 0000000000..15debab5e9 --- /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 0000000000..60701403d8 --- /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 0000000000..80ca8559eb --- /dev/null +++ b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/FastPairTestDataCache.kt @@ -0,0 +1,319 @@ +/* + * 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.Gson +import com.google.gson.annotations.SerializedName + +/** Manage a cache of Fast Pair test data for testing. */ +class FastPairTestDataCache { + private val gson = Gson() + private val accountKeyDeviceMetadataList = mutableListOf() + private val antispoofKeyDeviceMetadataDataMap = + mutableMapOf() + + fun putAccountKeyDeviceMetadata(json: String) { + accountKeyDeviceMetadataList += + gson.fromJson(json, Array::class.java) + .map { it.toFastPairAccountKeyDeviceMetadata() } + } + + fun putAccountKeyDeviceMetadata(accountKeyDeviceMetadata: FastPairAccountKeyDeviceMetadata) { + accountKeyDeviceMetadataList += accountKeyDeviceMetadata + } + + fun getAccountKeyDeviceMetadataList(): List = + 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("assistant_setup_half_sheet") val assistantSetupHalfSheet: String?, + @SerializedName("assistant_setup_notification") val assistantSetupNotification: String?, + @SerializedName("ble_tx_power") val bleTxPower: Int, + @SerializedName("confirm_pin_description") val confirmPinDescription: String?, + @SerializedName("confirm_pin_title") val confirmPinTitle: String?, + @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("fast_pair_tv_connect_device_no_account_description") val accDes: 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("locale") val locale: 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("sync_contacts_description") val syncContactsDescription: String?, + @SerializedName("sync_contacts_title") val syncContactsTitle: String?, + @SerializedName("sync_sms_description") val syncSmsDescription: String?, + @SerializedName("sync_sms_title") val syncSmsTitle: 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( + assistantSetupHalfSheet = meta.assistantSetupHalfSheet, + assistantSetupNotification = meta.assistantSetupNotification, + bleTxPower = meta.bleTxPower, + confirmPinDescription = meta.confirmPinDescription, + confirmPinTitle = meta.confirmPinTitle, + compAppInstalled = meta.connectSuccessCompanionAppInstalled, + comAppNotIns = meta.connectSuccessCompanionAppNotInstalled, + deviceType = meta.deviceType, + downloadComApp = meta.downloadCompanionAppDescription, + failConnectDes = meta.failConnectGoToSettingsDescription, + accDes = meta.fastPairTvConnectDeviceNoAccountDescription, + imageUrl = meta.imageUrl, + initNotification = meta.initialNotificationDescription, + initNoAccount = meta.initialNotificationDescriptionNoAccount, + initialPairingDescription = meta.initialPairingDescription, + intentUri = meta.intentUri, + locale = meta.locale, + name = meta.name, + openCompanionAppDescription = meta.openCompanionAppDescription, + retroactivePairingDes = meta.retroactivePairingDescription, + subsequentPairingDescription = meta.subsequentPairingDescription, + syncContactsDescription = meta.syncContactsDescription, + syncContactsTitle = meta.syncContactsTitle, + syncSmsDescription = meta.syncSmsDescription, + syncSmsTitle = meta.syncSmsTitle, + 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() + .setAssistantSetupHalfSheet(assistantSetupHalfSheet) + .setAssistantSetupNotification(assistantSetupNotification) + .setBleTxPower(bleTxPower) + .setConfirmPinDescription(confirmPinDescription) + .setConfirmPinTitle(confirmPinTitle) + .setConnectSuccessCompanionAppInstalled(compAppInstalled) + .setConnectSuccessCompanionAppNotInstalled(comAppNotIns) + .setDeviceType(deviceType) + .setDownloadCompanionAppDescription(downloadComApp) + .setFailConnectGoToSettingsDescription(failConnectDes) + .setFastPairTvConnectDeviceNoAccountDescription(accDes) + .setImageUrl(imageUrl) + .setInitialNotificationDescription(initNotification) + .setInitialNotificationDescriptionNoAccount(initNoAccount) + .setInitialPairingDescription(initialPairingDescription) + .setIntentUri(intentUri) + .setLocale(locale) + .setName(name) + .setOpenCompanionAppDescription(openCompanionAppDescription) + .setRetroactivePairingDescription(retroactivePairingDes) + .setSubsequentPairingDescription(subsequentPairingDescription) + .setSyncContactsDescription(syncContactsDescription) + .setSyncContactsTitle(syncContactsTitle) + .setSyncSmsDescription(syncSmsDescription) + .setSyncSmsTitle(syncSmsTitle) + .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("attachment_type") val attachmentType: Int, + @SerializedName("authentication_public_key_secp256r1") val authenticationPublicKey: String?, + @SerializedName("ble_record_bytes") val bleRecordBytes: String?, + @SerializedName("debug_category") val debugCategory: Int, + @SerializedName("debug_message") val debugMessage: String?, + @SerializedName("description") val description: String?, + @SerializedName("device_name") val deviceName: String?, + @SerializedName("display_url") val displayUrl: String?, + @SerializedName("entity_id") val entityId: String?, + @SerializedName("feature_graphic_url") val featureGraphicUrl: String?, + @SerializedName("first_observation_timestamp_millis") val firstObservationMs: Long, + @SerializedName("group_id") val groupId: String?, + @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("last_user_experience") val lastUserExperience: Int, + @SerializedName("lost_millis") val lostMillis: 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, + @SerializedName("type") val type: Int, + ) { + constructor(item: FastPairDiscoveryItem) : this( + actionUrl = item.actionUrl, + actionUrlType = item.actionUrlType, + appName = item.appName, + attachmentType = item.attachmentType, + authenticationPublicKey = item.authenticationPublicKeySecp256r1?.base64Encode(), + bleRecordBytes = item.bleRecordBytes?.base64Encode(), + debugCategory = item.debugCategory, + debugMessage = item.debugMessage, + description = item.description, + deviceName = item.deviceName, + displayUrl = item.displayUrl, + entityId = item.entityId, + featureGraphicUrl = item.featureGraphicUrl, + firstObservationMs = item.firstObservationTimestampMillis, + groupId = item.groupId, + iconFfeUrl = item.iconFfeUrl, + iconPng = item.iconPng?.base64Encode(), + id = item.id, + lastObservationMs = item.lastObservationTimestampMillis, + lastUserExperience = item.lastUserExperience, + lostMillis = item.lostMillis, + macAddress = item.macAddress, + packageName = item.packageName, + pendingAppInstallMs = item.pendingAppInstallTimestampMillis, + rssi = item.rssi, + state = item.state, + title = item.title, + triggerId = item.triggerId, + txPower = item.txPower, + type = item.type, + ) + + fun toFastPairDiscoveryItem(): FastPairDiscoveryItem { + return FastPairDiscoveryItem.Builder() + .setActionUrl(actionUrl) + .setActionUrlType(actionUrlType) + .setAppName(appName) + .setAttachmentType(attachmentType) + .setAuthenticationPublicKeySecp256r1(authenticationPublicKey?.base64Decode()) + .setBleRecordBytes(bleRecordBytes?.base64Decode()) + .setDebugCategory(debugCategory) + .setDebugMessage(debugMessage) + .setDescription(description) + .setDeviceName(deviceName) + .setDisplayUrl(displayUrl) + .setEntityId(entityId) + .setFeatureGraphicUrl(featureGraphicUrl) + .setFirstObservationTimestampMillis(firstObservationMs) + .setGroupId(groupId) + .setIconFfeUrl(iconFfeUrl) + .setIconPng(iconPng?.base64Decode()) + .setId(id) + .setLastObservationTimestampMillis(lastObservationMs) + .setLastUserExperience(lastUserExperience) + .setLostMillis(lostMillis) + .setMacAddress(macAddress) + .setPackageName(packageName) + .setPendingAppInstallTimestampMillis(pendingAppInstallMs) + .setRssi(rssi) + .setState(state) + .setTitle(title) + .setTriggerId(triggerId) + .setTxPower(txPower) + .setType(type) + .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 0000000000..ffc02a0b50 --- /dev/null +++ b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/data/FastPairTestDataManager.kt @@ -0,0 +1,78 @@ +/* + * 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.* +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. + */ + fun writeAccountKeyDeviceMetadata(accountKeyDeviceMetadata: FastPairAccountKeyDeviceMetadata) { + 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) + } + } + + /** + * 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.putAccountKeyDeviceMetadata(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 0000000000..cec0607cea --- /dev/null +++ b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/dataprovider/FastPairTestDataProviderService.kt @@ -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 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.* +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 requestedBleAddress = request.bleAddress + val requestedAccountKeyDeviceMetadata = request.accountKeyDeviceMetadata + Log.d( + TAG, + "onManageFastPairAccountDevice(requestedAccount: $requestedAccount, " + + "requestType: $requestTypeString," + ) + Log.d(TAG, "requestedBleAddress: $requestedBleAddress,") + Log.d(TAG, "requestedAccountKeyDeviceMetadata: $requestedAccountKeyDeviceMetadata)") + + testDataManager.writeAccountKeyDeviceMetadata(requestedAccountKeyDeviceMetadata) + + 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 0000000000..920834a3f5 --- /dev/null +++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp @@ -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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_library { + name: "NearbyFastPairProviderLib", + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + sdk_version: "test_current", + static_libs: [ + "NearbyFastPairProviderLiteProtos", + "androidx.test.core", + "error_prone_annotations", + "fast-pair-lite-protos", + "framework-annotations-lib", + "kotlin-stdlib", + "service-nearby-pre-jarjar", + ], +} 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 0000000000..400a434c7b --- /dev/null +++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + 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 0000000000..7ae43e5954 --- /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 0000000000..54db34a0af --- /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 0000000000..f3eed51f05 --- /dev/null +++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/Android.bp @@ -0,0 +1,45 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_app { + name: "NearbyFastPairProviderSimulatorApp", + sdk_version: "test_current", + static_libs: ["NearbyFastPairProviderSimulatorLib"], + optimize: { + enabled: true, + shrink: true, + proguard_flags_files: ["proguard.flags"], + }, +} + +android_library { + name: "NearbyFastPairProviderSimulatorLib", + sdk_version: "test_current", + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + static_libs: [ + "NearbyFastPairProviderLib", + "NearbyFastPairProviderLiteProtos", + "NearbyFastPairProviderSimulatorLiteProtos", + "androidx.annotation_annotation", + "error_prone_annotations", + "fast-pair-lite-protos", + ], +} \ No newline at end of file diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/AndroidManifest.xml b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/AndroidManifest.xml new file mode 100644 index 0000000000..8880b11071 --- /dev/null +++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + 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 0000000000..28680b33bb --- /dev/null +++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proguard.flags @@ -0,0 +1,19 @@ +# Keep simulator reflection callback. +-keep class android.nearby.fastpair.provider.** { + *; +} + +# Keep names for easy debugging. +-dontobfuscate + +# Necessary to allow debugging. +-keepattributes * + +# By default, proguard leaves all classes in their original package, which +# needlessly repeats com.google.android.apps.etc. +-repackageclasses "" + +# Allows proguard to make private and protected methods and fields public as +# part of optimization. This lets proguard inline trivial getter/setter +# methods. +-allowaccessmodification \ No newline at end of file diff --git a/nearby/tests/multidevices/clients/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 0000000000..e9648004d0 --- /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 0000000000..9b17fda9b0 --- /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 0000000000..b7e85eb13b --- /dev/null +++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/res/layout/activity_main.xml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +