Create a Mobly snippet for Nearby Mainline Fast Pair multidevice tests.
Design doc: go/nearby-mainline-snippet Test: atest --host NearbyMultiDevicesClientsRoboTest Ignore-AOSP-First: Nearby is not yet in AOSP. Bug: 214015364 Change-Id: Id8f551db9cbbe60f4ba0303851de66a644f55c4c
This commit is contained in:
parent
9911962766
commit
c7a4d1bb74
39 changed files with 6457 additions and 1 deletions
|
|
@ -19,7 +19,7 @@ This directory contains code for the AOSP Nearby mainline module.
|
|||
- Server side implementation for nearby module services.
|
||||
|
||||
`tests`
|
||||
- Unit tests for Nearby module (both Java and native code).
|
||||
- Unit/Multi devices tests for Nearby module (both Java and native code).
|
||||
|
||||
## IDE setup
|
||||
|
||||
|
|
|
|||
4
nearby/tests/multidevices/OWNERS
Normal file
4
nearby/tests/multidevices/OWNERS
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Bug component: 1092133
|
||||
|
||||
ericth@google.com
|
||||
ryancllin@google.com
|
||||
48
nearby/tests/multidevices/clients/Android.bp
Normal file
48
nearby/tests/multidevices/clients/Android.bp
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright (C) 2022 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package {
|
||||
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||
}
|
||||
|
||||
android_library {
|
||||
name: "NearbyMultiDevicesClientsLib",
|
||||
srcs: [
|
||||
"src/**/*.java",
|
||||
"src/**/*.kt",
|
||||
],
|
||||
sdk_version: "test_current",
|
||||
static_libs: [
|
||||
"NearbyMultiDevicesClientsFastPairLiteProtos",
|
||||
"androidx.test.core",
|
||||
"error_prone_annotations",
|
||||
"fast-pair-lite-protos",
|
||||
"framework-annotations-lib",
|
||||
"kotlin-stdlib",
|
||||
"mobly-snippet-lib",
|
||||
"service-nearby",
|
||||
],
|
||||
}
|
||||
|
||||
android_app {
|
||||
name: "NearbyMultiDevicesClientsSnippets",
|
||||
sdk_version: "test_current",
|
||||
certificate: "platform",
|
||||
static_libs: ["NearbyMultiDevicesClientsLib"],
|
||||
optimize: {
|
||||
enabled: true,
|
||||
shrink: true,
|
||||
proguard_flags_files: ["proguard.flags"],
|
||||
},
|
||||
}
|
||||
69
nearby/tests/multidevices/clients/AndroidManifest.xml
Normal file
69
nearby/tests/multidevices/clients/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2022 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="android.nearby.multidevices">
|
||||
|
||||
<uses-feature android:name="android.hardware.bluetooth" />
|
||||
<uses-feature android:name="android.hardware.bluetooth_le" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
|
||||
|
||||
<application>
|
||||
<meta-data
|
||||
android:name="mobly-log-tag"
|
||||
android:value="NearbyMainlineSnippet" />
|
||||
<meta-data
|
||||
android:name="mobly-snippets"
|
||||
android:value="android.nearby.multidevices.fastpair.seeker.FastPairSeekerSnippet,
|
||||
android.nearby.multidevices.fastpair.provider.FastPairProviderSimulatorSnippet" />
|
||||
|
||||
<!-- Fast Pair Data Provider Service which acts as an "overlay" to the
|
||||
framework Fast Pair Data Provider. Only supported on Android T and later.
|
||||
All overlays are protected from non-system access via WRITE_SECURE_SETTINGS.
|
||||
Must stay in the same process as Nearby Discovery Service.
|
||||
-->
|
||||
<service
|
||||
android:name=".fastpair.seeker.FastPairTestDataProviderService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.WRITE_SECURE_SETTINGS"
|
||||
android:visibleToInstantApps="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.nearby.action.FAST_PAIR_DATA_PROVIDER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="instantapps.clients.allowed"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="serviceVersion"
|
||||
android:value="1" />
|
||||
</service>
|
||||
</application>
|
||||
|
||||
<instrumentation
|
||||
android:name="com.google.android.mobly.snippet.SnippetRunner"
|
||||
android:label="Nearby Mainline Module Mobly Snippet"
|
||||
android:targetPackage="android.nearby.multidevices" />
|
||||
|
||||
</manifest>
|
||||
24
nearby/tests/multidevices/clients/proguard.flags
Normal file
24
nearby/tests/multidevices/clients/proguard.flags
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Keep all snippet classes.
|
||||
-keep class android.nearby.multidevices.** {
|
||||
*;
|
||||
}
|
||||
|
||||
# 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
|
||||
30
nearby/tests/multidevices/clients/proto/Android.bp
Normal file
30
nearby/tests/multidevices/clients/proto/Android.bp
Normal file
|
|
@ -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: "NearbyMultiDevicesClientsFastPairLiteProtos",
|
||||
proto: {
|
||||
type: "lite",
|
||||
canonical_path_from_root: false,
|
||||
},
|
||||
sdk_version: "system_current",
|
||||
min_sdk_version: "30",
|
||||
srcs: ["src/*/*.proto"],
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
syntax = "proto2";
|
||||
|
||||
package android.nearby.multidevices.fastpair;
|
||||
|
||||
option java_package = "android.nearby.multidevices.fastpair";
|
||||
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;
|
||||
}
|
||||
|
|
@ -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.common
|
||||
|
||||
import android.os.Bundle
|
||||
import com.google.android.mobly.snippet.event.EventCache
|
||||
import com.google.android.mobly.snippet.event.SnippetEvent
|
||||
|
||||
/**
|
||||
* Posts an {@link SnippetEvent} to the event cache with data bundle [fill] by the given function.
|
||||
*
|
||||
* This is a helper function to make your client side codes more concise. Sample usage:
|
||||
* ```
|
||||
* postSnippetEvent(callbackId, "onReceiverFound") {
|
||||
* putLong("discoveryTimeMs", discoveryTimeMs)
|
||||
* putBoolean("isKnown", isKnown)
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param callbackId the callbackId passed to the {@link
|
||||
* com.google.android.mobly.snippet.rpc.AsyncRpc} method.
|
||||
* @param eventName the name of the event.
|
||||
* @param fill the function to fill the data bundle.
|
||||
*/
|
||||
fun postSnippetEvent(callbackId: String, eventName: String, fill: Bundle.() -> Unit) {
|
||||
val eventData = Bundle().apply(fill)
|
||||
val snippetEvent = SnippetEvent(callbackId, eventName).apply { data.putAll(eventData) }
|
||||
EventCache.getInstance().postEvent(snippetEvent)
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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.Manifest.permission.BLUETOOTH_CONNECT
|
||||
import android.Manifest.permission.BLUETOOTH_SCAN
|
||||
import android.annotation.TargetApi
|
||||
import android.bluetooth.BluetoothClass
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.annotation.VisibleForTesting
|
||||
|
||||
/** Maintains an environment for Bluetooth A2DP sink profile. */
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
class BluetoothA2dpSinkService(private val context: Context) {
|
||||
private val bluetoothAdapter =
|
||||
(context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter!!
|
||||
private var a2dpSinkProxy: BluetoothProfile? = null
|
||||
|
||||
/**
|
||||
* Starts the Bluetooth A2DP sink profile proxy.
|
||||
*
|
||||
* @param onServiceConnected the callback for the first time onServiceConnected.
|
||||
*/
|
||||
fun start(onServiceConnected: () -> Unit) {
|
||||
// Get the A2DP proxy before continuing with initialization.
|
||||
bluetoothAdapter.getProfileProxy(
|
||||
context,
|
||||
object : BluetoothProfile.ServiceListener {
|
||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||
// When Bluetooth turns off and then on again, this is called again. But we only care
|
||||
// the first time. There doesn't seem to be a way to unregister our listener.
|
||||
if (a2dpSinkProxy == null) {
|
||||
a2dpSinkProxy = proxy
|
||||
onServiceConnected()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(profile: Int) {}
|
||||
},
|
||||
BLUETOOTH_PROFILE_A2DP_SINK
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the device is paired or not.
|
||||
*
|
||||
* @param remoteBluetoothDevice the device to check is paired or not.
|
||||
*/
|
||||
@RequiresPermission(BLUETOOTH_CONNECT)
|
||||
fun isPaired(remoteBluetoothDevice: BluetoothDevice?): Boolean =
|
||||
bluetoothAdapter.bondedDevices.contains(remoteBluetoothDevice)
|
||||
|
||||
/**
|
||||
* Gets the current Bluetooth scan mode of the local Bluetooth adapter.
|
||||
*/
|
||||
@RequiresPermission(BLUETOOTH_SCAN)
|
||||
fun getScanMode(): Int = bluetoothAdapter.scanMode
|
||||
|
||||
/**
|
||||
* Clears the bounded devices.
|
||||
*
|
||||
* @param removeBondDevice the callback to remove bounded devices.
|
||||
*/
|
||||
@RequiresPermission(BLUETOOTH_CONNECT)
|
||||
fun clearBoundedDevices(removeBondDevice: (BluetoothDevice) -> Unit) {
|
||||
for (device in bluetoothAdapter.bondedDevices) {
|
||||
if (device.bluetoothClass.majorDeviceClass == BluetoothClass.Device.Major.PHONE) {
|
||||
removeBondDevice(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the connected but unbounded devices.
|
||||
*
|
||||
* Sometimes a device will still be connected even though it's not bonded. :( Clear that too.
|
||||
*
|
||||
* @param disconnectDevice the callback to clear connected but unbounded devices.
|
||||
*/
|
||||
fun clearConnectedUnboundedDevices(
|
||||
disconnectDevice: (BluetoothProfile, BluetoothDevice) -> Unit,
|
||||
) {
|
||||
for (device in a2dpSinkProxy!!.connectedDevices) {
|
||||
disconnectDevice(a2dpSinkProxy!!, device)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Hidden SystemApi field in [android.bluetooth.BluetoothProfile] interface. */
|
||||
@VisibleForTesting
|
||||
const val BLUETOOTH_PROFILE_A2DP_SINK = 11
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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.Manifest.permission.BLUETOOTH
|
||||
import android.Manifest.permission.BLUETOOTH_CONNECT
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE
|
||||
import android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE
|
||||
import android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.annotation.VisibleForTesting
|
||||
|
||||
/** Processes the state of the local Bluetooth adapter. */
|
||||
class BluetoothStateChangeReceiver(private val context: Context) : BroadcastReceiver() {
|
||||
@VisibleForTesting
|
||||
var listener: EventListener? = null
|
||||
|
||||
/**
|
||||
* Registers this Bluetooth state change receiver.
|
||||
*
|
||||
* @param listener the listener for Bluetooth state events.
|
||||
*/
|
||||
fun register(listener: EventListener) {
|
||||
this.listener = listener
|
||||
val bondStateFilter =
|
||||
IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED).apply {
|
||||
addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
|
||||
addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)
|
||||
}
|
||||
context.registerReceiver(
|
||||
this,
|
||||
bondStateFilter,
|
||||
/* broadcastPermission= */ null,
|
||||
/* scheduler= */ null
|
||||
)
|
||||
}
|
||||
|
||||
/** Unregisters this Bluetooth state change receiver. */
|
||||
fun unregister() {
|
||||
context.unregisterReceiver(this)
|
||||
this.listener = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method for receiving Intent broadcast for Bluetooth state.
|
||||
*
|
||||
* See [android.content.BroadcastReceiver#onReceive].
|
||||
*
|
||||
* @param context the Context in which the receiver is running.
|
||||
* @param intent the Intent being received.
|
||||
*/
|
||||
@RequiresPermission(allOf = [BLUETOOTH, BLUETOOTH_CONNECT])
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Log.i(TAG, "BluetoothStateChangeReceiver received intent, action=${intent.action}")
|
||||
|
||||
when (intent.action) {
|
||||
BluetoothAdapter.ACTION_SCAN_MODE_CHANGED -> {
|
||||
val scanMode =
|
||||
intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, BluetoothAdapter.SCAN_MODE_NONE)
|
||||
val scanModeStr = scanModeToString(scanMode)
|
||||
Log.i(TAG, "ACTION_SCAN_MODE_CHANGED, the new scanMode: $scanModeStr")
|
||||
listener?.onScanModeChange(scanModeStr)
|
||||
}
|
||||
BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {
|
||||
val remoteDevice = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
|
||||
val remoteDeviceString =
|
||||
if (remoteDevice != null) "${remoteDevice.name}-${remoteDevice.address}" else "none"
|
||||
var boundStateString = "ERROR"
|
||||
when (intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)) {
|
||||
BluetoothDevice.BOND_NONE -> {
|
||||
boundStateString = "BOND_NONE"
|
||||
}
|
||||
BluetoothDevice.BOND_BONDING -> {
|
||||
boundStateString = "BOND_BONDING"
|
||||
}
|
||||
BluetoothDevice.BOND_BONDED -> {
|
||||
boundStateString = "BOND_BONDED"
|
||||
}
|
||||
}
|
||||
Log.i(
|
||||
TAG,
|
||||
"The bound state of the remote device ($remoteDeviceString) change to $boundStateString."
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scanModeToString(scanMode: Int): String {
|
||||
return when (scanMode) {
|
||||
SCAN_MODE_CONNECTABLE_DISCOVERABLE -> "DISCOVERABLE"
|
||||
SCAN_MODE_CONNECTABLE -> "CONNECTABLE"
|
||||
SCAN_MODE_NONE -> "NOT CONNECTABLE"
|
||||
else -> "UNKNOWN($scanMode)"
|
||||
}
|
||||
}
|
||||
|
||||
/** Interface for listening the events from Bluetooth adapter. */
|
||||
interface EventListener {
|
||||
/**
|
||||
* Reports the current scan mode of the local Adapter.
|
||||
*
|
||||
* @param mode the current scan mode in string.
|
||||
*/
|
||||
fun onScanModeChange(mode: String)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "BluetoothStateReceiver"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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.bluetooth.le.AdvertiseSettings
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.android.server.nearby.common.bluetooth.fastpair.testing.FastPairSimulator
|
||||
import 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
|
||||
import com.google.common.io.BaseEncoding.base64
|
||||
|
||||
/** 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 bluetoothA2dpSinkService = BluetoothA2dpSinkService(context)
|
||||
private val bluetoothStateChangeReceiver = BluetoothStateChangeReceiver(context)
|
||||
private lateinit var fastPairSimulator: FastPairSimulator
|
||||
private lateinit var providerStatusEvents: ProviderStatusEvents
|
||||
|
||||
/**
|
||||
* Starts the Fast Pair provider simulator.
|
||||
*
|
||||
* @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 FP provider simulator for seekers to discover.")
|
||||
fun startProviderSimulator(callbackId: String, modelId: String, antiSpoofingKeyString: String) {
|
||||
providerStatusEvents = ProviderStatusEvents(callbackId)
|
||||
bluetoothStateChangeReceiver.register(listener = providerStatusEvents)
|
||||
bluetoothA2dpSinkService.start { createFastPairSimulator(modelId, antiSpoofingKeyString) }
|
||||
}
|
||||
|
||||
/** Stops the Fast Pair provider simulator. */
|
||||
@Rpc(description = "Stops FP provider simulator.")
|
||||
fun stopProviderSimulator() {
|
||||
fastPairSimulator.destroy()
|
||||
bluetoothStateChangeReceiver.unregister()
|
||||
}
|
||||
|
||||
/** 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 fastPairSimulator.bleAddress!!
|
||||
}
|
||||
|
||||
private fun createFastPairSimulator(modelId: String, antiSpoofingKeyString: String) {
|
||||
val antiSpoofingKey = base64().decode(antiSpoofingKeyString)
|
||||
fastPairSimulator =
|
||||
FastPairSimulator(
|
||||
context,
|
||||
FastPairSimulator.Options.builder(modelId)
|
||||
.setAdvertisingModelId(modelId)
|
||||
.setBluetoothAddress(null)
|
||||
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
|
||||
.setCallback {
|
||||
val isAdvertising = fastPairSimulator.isAdvertising
|
||||
Log.i("FastPairSimulator callback(), isAdvertising: $isAdvertising")
|
||||
providerStatusEvents.onAdvertisingChange(isAdvertising)
|
||||
}
|
||||
.setAntiSpoofingPrivateKey(antiSpoofingKey)
|
||||
.setUseRandomSaltForAccountKeyRotation(false)
|
||||
.setDataOnlyConnection(false)
|
||||
.setIsMemoryTest(false)
|
||||
.setShowsPasskeyConfirmation(false)
|
||||
.setRemoveAllDevicesDuringPairing(true)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.nearby.multidevices.fastpair.provider
|
||||
|
||||
import android.nearby.multidevices.common.postSnippetEvent
|
||||
|
||||
/** The Mobly snippet events to report to the Python side. */
|
||||
class ProviderStatusEvents(private val callbackId: String) :
|
||||
BluetoothStateChangeReceiver.EventListener {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
fun onAdvertisingChange(isAdvertising: Boolean) {
|
||||
postSnippetEvent(callbackId, "onAdvertisingChange") {
|
||||
putBoolean("isAdvertising", isAdvertising)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.nearby.multidevices.fastpair.seeker
|
||||
|
||||
fun generateCompanionAppLaunchIntentUri(
|
||||
companionAppPackageName: String? = null,
|
||||
activityName: String? = null,
|
||||
action: String? = null
|
||||
): String {
|
||||
if (companionAppPackageName.isNullOrEmpty() || activityName.isNullOrEmpty()) {
|
||||
return ""
|
||||
}
|
||||
var intentUriString = "intent:#Intent;"
|
||||
if (!action.isNullOrEmpty()) {
|
||||
intentUriString += "action=$action;"
|
||||
}
|
||||
intentUriString += "package=$companionAppPackageName;"
|
||||
intentUriString += "component=$companionAppPackageName/$activityName;"
|
||||
return "${intentUriString}end"
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.nearby.multidevices.fastpair.seeker
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.nearby.NearbyManager
|
||||
import android.nearby.ScanCallback
|
||||
import android.nearby.ScanRequest
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.google.android.mobly.snippet.Snippet
|
||||
import com.google.android.mobly.snippet.rpc.AsyncRpc
|
||||
import com.google.android.mobly.snippet.rpc.Rpc
|
||||
import com.google.android.mobly.snippet.util.Log
|
||||
|
||||
/** Expose Mobly RPC methods for Python side to test fast pair seeker role. */
|
||||
class FastPairSeekerSnippet : Snippet {
|
||||
private val appContext = ApplicationProvider.getApplicationContext<Context>()
|
||||
private val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
|
||||
private 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)
|
||||
.setEnableBle(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)
|
||||
}
|
||||
|
||||
/** Starts the Fast Pair seeker pairing. */
|
||||
@Rpc(description = "Starts the Fast Pair seeker pairing.")
|
||||
fun startPairing(modelId: String, address: String) {
|
||||
Log.i("Starts the Fast Pair seeker pairing.")
|
||||
|
||||
val scanIntent = Intent().apply {
|
||||
action = FAST_PAIR_MANAGER_ACTION_START_PAIRING
|
||||
putExtra(FAST_PAIR_MANAGER_EXTRA_MODEL_ID, modelId.toByteArray())
|
||||
putExtra(FAST_PAIR_MANAGER_EXTRA_ADDRESS, address)
|
||||
}
|
||||
appContext.sendBroadcast(scanIntent)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FAST_PAIR_MANAGER_ACTION_START_PAIRING = "NEARBY_START_PAIRING"
|
||||
private const val FAST_PAIR_MANAGER_EXTRA_MODEL_ID = "MODELID"
|
||||
private const val FAST_PAIR_MANAGER_EXTRA_ADDRESS = "ADDRESS"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES 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.accounts.Account
|
||||
import android.nearby.*
|
||||
import android.util.Log
|
||||
import service.proto.Cache
|
||||
import service.proto.Rpcs.DeviceType
|
||||
import java.util.*
|
||||
|
||||
class FastPairTestDataProvider : FastPairDataProviderBase(TAG) {
|
||||
private val fastPairDeviceMetadata = FastPairDeviceMetadata.Builder()
|
||||
.setAssistantSetupHalfSheet(ASSISTANT_SETUP_HALF_SHEET_TEST_CONSTANT)
|
||||
.setAssistantSetupNotification(ASSISTANT_SETUP_NOTIFICATION_TEST_CONSTANT)
|
||||
.setBleTxPower(BLE_TX_POWER_TEST_CONSTANT)
|
||||
.setConfirmPinDescription(CONFIRM_PIN_DESCRIPTION_TEST_CONSTANT)
|
||||
.setConfirmPinTitle(CONFIRM_PIN_TITLE_TEST_CONSTANT)
|
||||
.setConnectSuccessCompanionAppInstalled(CONNECT_SUCCESS_COMPANION_APP_INSTALLED_TEST_CONSTANT)
|
||||
.setConnectSuccessCompanionAppNotInstalled(CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED_TEST_CONSTANT)
|
||||
.setDeviceType(DEVICE_TYPE_HEAD_PHONES_TEST_CONSTANT)
|
||||
.setDownloadCompanionAppDescription(DOWNLOAD_COMPANION_APP_DESCRIPTION_TEST_CONSTANT)
|
||||
.setFailConnectGoToSettingsDescription(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION_TEST_CONSTANT)
|
||||
.setFastPairTvConnectDeviceNoAccountDescription(TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION_TEST_CONSTANT)
|
||||
.setInitialNotificationDescription(INITIAL_NOTIFICATION_DESCRIPTION_TEST_CONSTANT)
|
||||
.setInitialNotificationDescriptionNoAccount(INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT_TEST_CONSTANT)
|
||||
.setInitialPairingDescription(INITIAL_PAIRING_DESCRIPTION_TEST_CONSTANT)
|
||||
.setLocale(LOCALE_US_LANGUAGE_TEST_CONSTANT)
|
||||
.setImage(IMAGE_BYTE_ARRAY_FAKE_TEST_CONSTANT)
|
||||
.setImageUrl(IMAGE_URL_TEST_CONSTANT)
|
||||
.setIntentUri(
|
||||
generateCompanionAppLaunchIntentUri(
|
||||
companionAppPackageName = COMPANION_APP_PACKAGE_TEST_CONSTANT,
|
||||
activityName = COMPANION_APP_ACTIVITY_TEST_CONSTANT,
|
||||
)
|
||||
)
|
||||
.setOpenCompanionAppDescription(OPEN_COMPANION_APP_DESCRIPTION_TEST_CONSTANT)
|
||||
.setRetroactivePairingDescription(RETRO_ACTIVE_PAIRING_DESCRIPTION_TEST_CONSTANT)
|
||||
.setSubsequentPairingDescription(SUBSEQUENT_PAIRING_DESCRIPTION_TEST_CONSTANT)
|
||||
.setSyncContactsDescription(SYNC_CONTACT_DESCRIPTION_TEST_CONSTANT)
|
||||
.setSyncContactsTitle(SYNC_CONTACTS_TITLE_TEST_CONSTANT)
|
||||
.setSyncSmsDescription(SYNC_SMS_DESCRIPTION_TEST_CONSTANT)
|
||||
.setSyncSmsTitle(SYNC_SMS_TITLE_TEST_CONSTANT)
|
||||
.setTriggerDistance(TRIGGER_DISTANCE_TEST_CONSTANT)
|
||||
.setTrueWirelessImageUrlCase(TRUE_WIRELESS_IMAGE_URL_CASE_TEST_CONSTANT)
|
||||
.setTrueWirelessImageUrlLeftBud(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD_TEST_CONSTANT)
|
||||
.setTrueWirelessImageUrlRightBud(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD_TEST_CONSTANT)
|
||||
.setUnableToConnectDescription(UNABLE_TO_CONNECT_DESCRIPTION_TEST_CONSTANT)
|
||||
.setUnableToConnectTitle(UNABLE_TO_CONNECT_TITLE_TEST_CONSTANT)
|
||||
.setUpdateCompanionAppDescription(UPDATE_COMPANION_APP_DESCRIPTION_TEST_CONSTANT)
|
||||
.setWaitLaunchCompanionAppDescription(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION_TEST_CONSTANT)
|
||||
.build()
|
||||
|
||||
override fun onLoadFastPairAntispoofkeyDeviceMetadata(
|
||||
request: FastPairAntispoofkeyDeviceMetadataRequest,
|
||||
callback: FastPairAntispoofkeyDeviceMetadataCallback
|
||||
) {
|
||||
val requestedModelId = request.modelId.bytesToStringLowerCase()
|
||||
Log.d(TAG, "onLoadFastPairAntispoofkeyDeviceMetadata(modelId: $requestedModelId)")
|
||||
|
||||
val fastPairAntiSpoofKeyDeviceMetadata =
|
||||
FastPairAntispoofkeyDeviceMetadata.Builder()
|
||||
.setAntiSpoofPublicKey(ANTI_SPOOF_PUBLIC_KEY_BYTE_ARRAY)
|
||||
.setFastPairDeviceMetadata(fastPairDeviceMetadata)
|
||||
.build()
|
||||
|
||||
callback.onFastPairAntispoofkeyDeviceMetadataReceived(fastPairAntiSpoofKeyDeviceMetadata)
|
||||
}
|
||||
|
||||
override fun onLoadFastPairAccountDevicesMetadata(
|
||||
request: FastPairAccountDevicesMetadataRequest,
|
||||
callback: FastPairAccountDevicesMetadataCallback
|
||||
) {
|
||||
val requestedAccount = request.account
|
||||
Log.d(TAG, "onLoadFastPairAccountDevicesMetadata(account: $requestedAccount)")
|
||||
val discoveryItem = FastPairDiscoveryItem.Builder()
|
||||
.setActionUrl(ACTION_URL_TEST_CONSTANT)
|
||||
.setActionUrlType(ACTION_URL_TYPE_TEST_CONSTANT)
|
||||
.setAppName(APP_NAME_TEST_CONSTANT)
|
||||
.setAttachmentType(ATTACHMENT_TYPE_TEST_CONSTANT)
|
||||
.setAuthenticationPublicKeySecp256r1(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1_TEST_CONSTANT)
|
||||
.setBleRecordBytes(BLE_RECORD_BYTES_TEST_CONSTANT)
|
||||
.setDebugCategory(DEBUG_CATEGORY_TEST_CONSTANT)
|
||||
.setDebugMessage(DEBUG_MESSAGE_TEST_CONSTANT)
|
||||
.setDescription(DESCRIPTION_TEST_CONSTANT)
|
||||
.setDeviceName(DEVICE_NAME_TEST_CONSTANT)
|
||||
.setDisplayUrl(DISPLAY_URL_TEST_CONSTANT)
|
||||
.setEntityId(ENTITY_ID_TEST_CONSTANT)
|
||||
.setFeatureGraphicUrl(FEATURE_GRAPHIC_URL_TEST_CONSTANT)
|
||||
.setFirstObservationTimestampMillis(FIRST_OBSERVATION_TIMESTAMP_MILLIS_TEST_CONSTANT)
|
||||
.setGroupId(GROUP_ID_TEST_CONSTANT)
|
||||
.setIconFfeUrl(ICON_FIFE_URL_TEST_CONSTANT)
|
||||
.setIconPng(ICON_PNG_TEST_CONSTANT)
|
||||
.setId(ID_TEST_CONSTANT)
|
||||
.setLastObservationTimestampMillis(LAST_OBSERVATION_TIMESTAMP_MILLIS_TEST_CONSTANT)
|
||||
.setLastUserExperience(LAST_USER_EXPERIENCE_TEST_CONSTANT)
|
||||
.setLostMillis(LOST_MILLIS_TEST_CONSTANT)
|
||||
.setMacAddress(MAC_ADDRESS_TEST_CONSTANT)
|
||||
.setPackageName(PACKAGE_NAME_TEST_CONSTANT)
|
||||
.setPendingAppInstallTimestampMillis(PENDING_APP_INSTALL_TIMESTAMP_MILLIS_TEST_CONSTANT)
|
||||
.setRssi(RSSI_TEST_CONSTANT)
|
||||
.setState(STATE_TEST_CONSTANT)
|
||||
.setTitle(TITLE_TEST_CONSTANT)
|
||||
.setTriggerId(TRIGGER_ID_TEST_CONSTANT)
|
||||
.setTxPower(TX_POWER_TEST_CONSTANT)
|
||||
.setType(TYPE_TEST_CONSTANT)
|
||||
.build()
|
||||
val accountDevicesMetadataList = listOf(
|
||||
FastPairAccountKeyDeviceMetadata.Builder()
|
||||
.setAccountKey(ACCOUNT_KEY_TEST_CONSTANT)
|
||||
.setFastPairDeviceMetadata(fastPairDeviceMetadata)
|
||||
.setSha256AccountKeyPublicAddress(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS_TEST_CONSTANT)
|
||||
.setFastPairDiscoveryItem(discoveryItem)
|
||||
.build()
|
||||
)
|
||||
|
||||
callback.onFastPairAccountDevicesMetadataReceived(accountDevicesMetadataList)
|
||||
}
|
||||
|
||||
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 requestedBleAddress = request.bleAddress
|
||||
val requestedAccountKeyDeviceMetadata = request.accountKeyDeviceMetadata
|
||||
Log.d(TAG, "onManageFastPairAccountDevice(requestedAccount: $requestedAccount, requestType: $requestType,")
|
||||
Log.d(TAG, "requestedBleAddress: $requestedBleAddress,")
|
||||
Log.d(TAG, "requestedAccountKeyDeviceMetadata: $requestedAccountKeyDeviceMetadata)")
|
||||
|
||||
callback.onSuccess()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FastPairTestDataProvider"
|
||||
|
||||
private const val BLE_TX_POWER_TEST_CONSTANT = 5
|
||||
private const val TRIGGER_DISTANCE_TEST_CONSTANT = 10f
|
||||
private const val ACTION_URL_TEST_CONSTANT = "ACTION_URL_TEST_CONSTANT"
|
||||
private const val ACTION_URL_TYPE_TEST_CONSTANT = Cache.ResolvedUrlType.APP_VALUE
|
||||
private const val APP_NAME_TEST_CONSTANT = "Nearby Mainline Mobly Test Snippet"
|
||||
private const val ATTACHMENT_TYPE_TEST_CONSTANT =
|
||||
Cache.DiscoveryAttachmentType.DISCOVERY_ATTACHMENT_TYPE_NORMAL_VALUE
|
||||
private const val DEBUG_CATEGORY_TEST_CONSTANT =
|
||||
Cache.StoredDiscoveryItem.DebugMessageCategory.STATUS_VALID_NOTIFICATION_VALUE
|
||||
private const val DEBUG_MESSAGE_TEST_CONSTANT = "DEBUG_MESSAGE_TEST_CONSTANT"
|
||||
private const val DESCRIPTION_TEST_CONSTANT = "DESCRIPTION_TEST_CONSTANT"
|
||||
private const val DEVICE_NAME_TEST_CONSTANT = "Fast Pair Headphone Simulator"
|
||||
private const val DISPLAY_URL_TEST_CONSTANT = "DISPLAY_URL_TEST_CONSTANT"
|
||||
private const val ENTITY_ID_TEST_CONSTANT = "ENTITY_ID_TEST_CONSTANT"
|
||||
private const val FEATURE_GRAPHIC_URL_TEST_CONSTANT = "FEATURE_GRAPHIC_URL_TEST_CONSTANT"
|
||||
private const val FIRST_OBSERVATION_TIMESTAMP_MILLIS_TEST_CONSTANT = 8_393L
|
||||
private const val GROUP_ID_TEST_CONSTANT = "GROUP_ID_TEST_CONSTANT"
|
||||
private const val ICON_FIFE_URL_TEST_CONSTANT = "ICON_FIFE_URL_TEST_CONSTANT"
|
||||
private const val ID_TEST_CONSTANT = "ID_TEST_CONSTANT"
|
||||
private const val LAST_OBSERVATION_TIMESTAMP_MILLIS_TEST_CONSTANT = 934_234L
|
||||
private const val LAST_USER_EXPERIENCE_TEST_CONSTANT =
|
||||
Cache.StoredDiscoveryItem.ExperienceType.EXPERIENCE_GOOD_VALUE
|
||||
private const val LOST_MILLIS_TEST_CONSTANT = 393_284L
|
||||
private const val MAC_ADDRESS_TEST_CONSTANT = "11:aa:22:bb:34:cd"
|
||||
private const val PACKAGE_NAME_TEST_CONSTANT = "android.nearby.package.name.test.constant"
|
||||
private const val PENDING_APP_INSTALL_TIMESTAMP_MILLIS_TEST_CONSTANT = 832_393L
|
||||
private const val RSSI_TEST_CONSTANT = 9
|
||||
private const val STATE_TEST_CONSTANT = Cache.StoredDiscoveryItem.State.STATE_ENABLED_VALUE
|
||||
private const val TITLE_TEST_CONSTANT = "TITLE_TEST_CONSTANT"
|
||||
private const val TRIGGER_ID_TEST_CONSTANT = "TRIGGER_ID_TEST_CONSTANT"
|
||||
private const val TX_POWER_TEST_CONSTANT = 62
|
||||
private const val TYPE_TEST_CONSTANT = Cache.NearbyType.NEARBY_DEVICE_VALUE
|
||||
|
||||
private val ANTI_SPOOF_PUBLIC_KEY_BYTE_ARRAY =
|
||||
"Cbj9eCJrTdDgSYxLkqtfADQi86vIaMvxJsQ298sZYWE=".toByteArray()
|
||||
private val LOCALE_US_LANGUAGE_TEST_CONSTANT = Locale.US.language
|
||||
private val IMAGE_BYTE_ARRAY_FAKE_TEST_CONSTANT = byteArrayOf(7, 9)
|
||||
private val IMAGE_URL_TEST_CONSTANT =
|
||||
"2l9cq8LFjK4D7EvPiFAq08DMpUA1b2SoPv9FPw3q6iiwjDvh-hLfKsPCFy0j36rfjDNjSULvRgOodRDxfRHHxA".toFifeUrlString()
|
||||
private val TRUE_WIRELESS_IMAGE_URL_CASE_TEST_CONSTANT =
|
||||
"oNv4-sFfa0tM1uA7vZ8r7UJPBV8OreiKOFl-_KlFwrqnDD7MoOV4uX8NwGUdYb1dcMm7cfwjZ04628WTeS40".toFifeUrlString()
|
||||
private val TRUE_WIRELESS_IMAGE_URL_LEFT_BUD_TEST_CONSTANT =
|
||||
"RcGxVZRObx9Avn9AHwSMM4WvDbVNyYlqigW7PlDHL4RLU8W9lcENDMyaTWM9O7JIu1ewSX-FIe_GkQfDlItQkg".toFifeUrlString()
|
||||
private val TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD_TEST_CONSTANT =
|
||||
"S7AuFqmr_hEqEFo_qfjxAiPz9moae0dkXUSUJV4gVFcysYpn4C95P77egPnuu35C3Eh_UY6_yNpQkmmUqn4N".toFifeUrlString()
|
||||
private val ELIGIBLE_ACCOUNTS_TEST_CONSTANT = listOf(
|
||||
FastPairEligibleAccount.Builder()
|
||||
.setAccount(Account("nearby-mainline-fpseeker@google.com", "TestAccount"))
|
||||
.setOptIn(true)
|
||||
.build(),
|
||||
)
|
||||
private val ACCOUNT_KEY_TEST_CONSTANT = byteArrayOf(3)
|
||||
private val SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS_TEST_CONSTANT = byteArrayOf(2, 8)
|
||||
private val AUTHENTICATION_PUBLIC_KEY_SEC_P256R1_TEST_CONSTANT = byteArrayOf(5, 7)
|
||||
private val BLE_RECORD_BYTES_TEST_CONSTANT = byteArrayOf(2, 4)
|
||||
private val ICON_PNG_TEST_CONSTANT = byteArrayOf(2, 5)
|
||||
|
||||
private const val DEVICE_TYPE_HEAD_PHONES_TEST_CONSTANT = DeviceType.HEADPHONES_VALUE
|
||||
private const val ASSISTANT_SETUP_HALF_SHEET_TEST_CONSTANT =
|
||||
"This is a test description in half sheet to ask user setup google assistant."
|
||||
private const val ASSISTANT_SETUP_NOTIFICATION_TEST_CONSTANT =
|
||||
"This is a test description in notification to ask user setup google assistant."
|
||||
private const val CONFIRM_PIN_DESCRIPTION_TEST_CONSTANT =
|
||||
"Please confirm the pin code to fast pair your device."
|
||||
private const val CONFIRM_PIN_TITLE_TEST_CONSTANT = "PIN code confirmation"
|
||||
private const val CONNECT_SUCCESS_COMPANION_APP_INSTALLED_TEST_CONSTANT =
|
||||
"This is a test description that let user open the companion app."
|
||||
private const val CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED_TEST_CONSTANT =
|
||||
"This is a test description that let user download the companion app."
|
||||
private const val DOWNLOAD_COMPANION_APP_DESCRIPTION_TEST_CONSTANT =
|
||||
"This is a test description for downloading companion app."
|
||||
private const val FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION_TEST_CONSTANT = "This is a " +
|
||||
"test description that indicates go to bluetooth settings when connection fail."
|
||||
private const val TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION_TEST_CONSTANT =
|
||||
"This is a test description of the connect device action on TV, " +
|
||||
"when user is not logged in."
|
||||
private const val INITIAL_NOTIFICATION_DESCRIPTION_TEST_CONSTANT =
|
||||
"This is a test description for initial notification."
|
||||
private const val INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT_TEST_CONSTANT = "This is a " +
|
||||
"test description of initial notification description when account is not present."
|
||||
private const val INITIAL_PAIRING_DESCRIPTION_TEST_CONSTANT =
|
||||
"This is a test description for Fast Pair initial pairing."
|
||||
private const val COMPANION_APP_PACKAGE_TEST_CONSTANT = "android.nearby.companion"
|
||||
private const val COMPANION_APP_ACTIVITY_TEST_CONSTANT =
|
||||
"android.nearby.companion.MainActivity"
|
||||
private const val OPEN_COMPANION_APP_DESCRIPTION_TEST_CONSTANT =
|
||||
"This is a test description for opening companion app."
|
||||
private const val RETRO_ACTIVE_PAIRING_DESCRIPTION_TEST_CONSTANT =
|
||||
"This is a test description that reminds users opt in their device."
|
||||
private const val SUBSEQUENT_PAIRING_DESCRIPTION_TEST_CONSTANT =
|
||||
"This is a test description that reminds user there is a paired device nearby."
|
||||
private const val SYNC_CONTACT_DESCRIPTION_TEST_CONSTANT =
|
||||
"This is a test description of the UI to ask the user to confirm to sync contacts."
|
||||
private const val SYNC_CONTACTS_TITLE_TEST_CONSTANT = "Sync contacts to your device"
|
||||
private const val SYNC_SMS_DESCRIPTION_TEST_CONSTANT =
|
||||
"This is a test description of the UI to ask the user to confirm to sync SMS."
|
||||
private const val SYNC_SMS_TITLE_TEST_CONSTANT = "Sync SMS to your device"
|
||||
private const val UNABLE_TO_CONNECT_DESCRIPTION_TEST_CONSTANT =
|
||||
"This is a test description when Fast Pair device is unable to be connected to."
|
||||
private const val UNABLE_TO_CONNECT_TITLE_TEST_CONSTANT =
|
||||
"Unable to connect your Fast Pair device"
|
||||
private const val UPDATE_COMPANION_APP_DESCRIPTION_TEST_CONSTANT =
|
||||
"This is a test description for updating companion app."
|
||||
private const val WAIT_LAUNCH_COMPANION_APP_DESCRIPTION_TEST_CONSTANT =
|
||||
"This is a test description that indicates companion app is about to launch."
|
||||
|
||||
private fun ByteArray.bytesToStringLowerCase(): String =
|
||||
joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
|
||||
|
||||
// Primary image serving domain for Google Photos and most other clients of FIFE.
|
||||
private fun String.toFifeUrlString() = "https://lh3.googleusercontent.com/$this"
|
||||
}
|
||||
}
|
||||
|
|
@ -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.seeker
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Fast Pair Test Data Provider Service entry point for platform overlay.
|
||||
*/
|
||||
class FastPairTestDataProviderService : Service() {
|
||||
private var testDataProvider: FastPairTestDataProvider? = null
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
Log.d(TAG, "onBind(intent: $intent)")
|
||||
if (testDataProvider == null) {
|
||||
testDataProvider = FastPairTestDataProvider()
|
||||
}
|
||||
return testDataProvider!!.binder
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(TAG, "onDestroy()")
|
||||
if (testDataProvider != null) {
|
||||
testDataProvider = null
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FastPairTestDataProviderService"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 android.nearby.multidevices.common.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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.nearby.common.bluetooth.fastpair.testing;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/** Cryptography utilities for ephemeral IDs. */
|
||||
public final class Crypto {
|
||||
private static final int AES_BLOCK_SIZE = 16;
|
||||
private static final ImmutableSet<Integer> VALID_AES_KEY_SIZES = ImmutableSet.of(16, 24, 32);
|
||||
private static final String AES_ECB_NOPADDING_ENCRYPTION_ALGO = "AES/ECB/NoPadding";
|
||||
private static final String AES_ENCRYPTION_ALGO = "AES";
|
||||
|
||||
/** Encrypts the provided data with the provided key using the AES/ECB/NoPadding algorithm. */
|
||||
public static ByteString aesEcbNoPaddingEncrypt(ByteString key, ByteString data) {
|
||||
return aesEcbOperation(key, data, Cipher.ENCRYPT_MODE);
|
||||
}
|
||||
|
||||
/** Decrypts the provided data with the provided key using the AES/ECB/NoPadding algorithm. */
|
||||
public static ByteString aesEcbNoPaddingDecrypt(ByteString key, ByteString data) {
|
||||
return aesEcbOperation(key, data, Cipher.DECRYPT_MODE);
|
||||
}
|
||||
|
||||
@SuppressLint("GetInstance")
|
||||
private static ByteString aesEcbOperation(ByteString key, ByteString data, int operation) {
|
||||
checkArgument(VALID_AES_KEY_SIZES.contains(key.size()));
|
||||
checkArgument(data.size() % AES_BLOCK_SIZE == 0);
|
||||
try {
|
||||
Cipher aesCipher = Cipher.getInstance(AES_ECB_NOPADDING_ENCRYPTION_ALGO);
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(key.toByteArray(), AES_ENCRYPTION_ALGO);
|
||||
aesCipher.init(operation, secretKeySpec);
|
||||
ByteBuffer output = ByteBuffer.allocate(data.size());
|
||||
checkState(aesCipher.doFinal(data.asReadOnlyByteBuffer(), output) == data.size());
|
||||
output.rewind();
|
||||
return ByteString.copyFrom(output);
|
||||
} catch (GeneralSecurityException e) {
|
||||
// Should never happen.
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Crypto() {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.testing;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Verify;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.spec.ECFieldFp;
|
||||
import java.security.spec.ECParameterSpec;
|
||||
import java.security.spec.ECPoint;
|
||||
import java.security.spec.EllipticCurve;
|
||||
import java.util.Collections;
|
||||
|
||||
/** Provides methods for calculating E2EE EIDs and E2E encryption/decryption based on E2EE EIDs. */
|
||||
public final class E2eeCalculator {
|
||||
|
||||
private static final byte[] TEMP_KEY_PADDING_1 =
|
||||
Bytes.toArray(Collections.nCopies(11, (byte) 0xFF));
|
||||
private static final byte[] TEMP_KEY_PADDING_2 = new byte[11];
|
||||
private static final ECParameterSpec CURVE_SPEC = getCurveSpec();
|
||||
private static final BigInteger P = ((ECFieldFp) CURVE_SPEC.getCurve().getField()).getP();
|
||||
private static final BigInteger TWO = new BigInteger("2");
|
||||
private static final BigInteger THREE = new BigInteger("3");
|
||||
private static final int E2EE_EID_IDENTITY_KEY_SIZE = 32;
|
||||
private static final int E2EE_EID_SIZE = 20;
|
||||
|
||||
/**
|
||||
* Computes the E2EE EID value for the given device clock based time. Note that Eddystone beacons
|
||||
* start advertising the new EID at a random time within the window, therefore the currently
|
||||
* advertised EID for beacon time <em>t</em> may be either {@code computeE2eeEid(eik, k, t)} or
|
||||
* {@code computeE2eeEid(eik, k, t - (1 << k))}.
|
||||
*
|
||||
* <p>The E2EE EID computation is based on https://goto.google.com/e2ee-eid-computation.
|
||||
*
|
||||
* @param identityKey the beacon's 32-byte Eddystone E2EE identity key
|
||||
* @param exponent rotation period exponent as configured on the beacon, must be in range the [0,
|
||||
* 15]
|
||||
* @param deviceClockSeconds the value of the beacon's 32-bit seconds time counter (treated as an
|
||||
* unsigned value)
|
||||
* @return E2EE EID value.
|
||||
*/
|
||||
public static ByteString computeE2eeEid(
|
||||
ByteString identityKey, int exponent, int deviceClockSeconds) {
|
||||
return computePublicKey(computePrivateKey(identityKey, exponent, deviceClockSeconds));
|
||||
}
|
||||
|
||||
private static ByteString computePublicKey(BigInteger privateKey) {
|
||||
return getXCoordinateBytes(toPoint(privateKey));
|
||||
}
|
||||
|
||||
private static BigInteger computePrivateKey(
|
||||
ByteString identityKey, int exponent, int deviceClockSeconds) {
|
||||
Preconditions.checkArgument(
|
||||
Preconditions.checkNotNull(identityKey).size() == E2EE_EID_IDENTITY_KEY_SIZE);
|
||||
Preconditions.checkArgument(exponent >= 0 && exponent < 16);
|
||||
|
||||
byte[] exponentByte = new byte[]{(byte) exponent};
|
||||
byte[] paddedCounter = Ints.toByteArray((deviceClockSeconds >>> exponent) << exponent);
|
||||
byte[] data =
|
||||
Bytes.concat(
|
||||
TEMP_KEY_PADDING_1,
|
||||
exponentByte,
|
||||
paddedCounter,
|
||||
TEMP_KEY_PADDING_2,
|
||||
exponentByte,
|
||||
paddedCounter);
|
||||
|
||||
byte[] rTag =
|
||||
Crypto.aesEcbNoPaddingEncrypt(identityKey, ByteString.copyFrom(data)).toByteArray();
|
||||
return new BigInteger(1, rTag).mod(CURVE_SPEC.getOrder());
|
||||
}
|
||||
|
||||
private static ECPoint toPoint(BigInteger privateKey) {
|
||||
return multiplyPoint(CURVE_SPEC.getGenerator(), privateKey);
|
||||
}
|
||||
|
||||
private static ByteString getXCoordinateBytes(ECPoint point) {
|
||||
byte[] unalignedBytes = point.getAffineX().toByteArray();
|
||||
|
||||
// The unalignedBytes may have length < 32 if the leading E2EE EID bytes are zero, or
|
||||
// it may be E2EE_EID_SIZE + 1 if the leading bit is 1, in which case the first byte is always
|
||||
// zero.
|
||||
Verify.verify(
|
||||
unalignedBytes.length <= E2EE_EID_SIZE
|
||||
|| (unalignedBytes.length == E2EE_EID_SIZE + 1 && unalignedBytes[0] == 0));
|
||||
|
||||
byte[] bytes;
|
||||
if (unalignedBytes.length < E2EE_EID_SIZE) {
|
||||
bytes = new byte[E2EE_EID_SIZE];
|
||||
System.arraycopy(
|
||||
unalignedBytes, 0, bytes, bytes.length - unalignedBytes.length, unalignedBytes.length);
|
||||
} else if (unalignedBytes.length == E2EE_EID_SIZE + 1) {
|
||||
bytes = new byte[E2EE_EID_SIZE];
|
||||
System.arraycopy(unalignedBytes, 1, bytes, 0, E2EE_EID_SIZE);
|
||||
} else { // unalignedBytes.length == GattE2EE_EID_SIZE
|
||||
bytes = unalignedBytes;
|
||||
}
|
||||
return ByteString.copyFrom(bytes);
|
||||
}
|
||||
|
||||
/** Returns a secp160r1 curve spec. */
|
||||
private static ECParameterSpec getCurveSpec() {
|
||||
final BigInteger p = new BigInteger("ffffffffffffffffffffffffffffffff7fffffff", 16);
|
||||
final BigInteger n = new BigInteger("0100000000000000000001f4c8f927aed3ca752257", 16);
|
||||
final BigInteger a = new BigInteger("ffffffffffffffffffffffffffffffff7ffffffc", 16);
|
||||
final BigInteger b = new BigInteger("1c97befc54bd7a8b65acf89f81d4d4adc565fa45", 16);
|
||||
final BigInteger gx = new BigInteger("4a96b5688ef573284664698968c38bb913cbfc82", 16);
|
||||
final BigInteger gy = new BigInteger("23a628553168947d59dcc912042351377ac5fb32", 16);
|
||||
final int h = 1;
|
||||
ECFieldFp fp = new ECFieldFp(p);
|
||||
EllipticCurve spec = new EllipticCurve(fp, a, b);
|
||||
ECPoint g = new ECPoint(gx, gy);
|
||||
return new ECParameterSpec(spec, g, n, h);
|
||||
}
|
||||
|
||||
/** Returns the scalar multiplication result of k*p in Fp. */
|
||||
private static ECPoint multiplyPoint(ECPoint p, BigInteger k) {
|
||||
ECPoint r = ECPoint.POINT_INFINITY;
|
||||
ECPoint s = p;
|
||||
BigInteger kModP = k.mod(P);
|
||||
int length = kModP.bitLength();
|
||||
for (int i = 0; i <= length - 1; i++) {
|
||||
if (kModP.mod(TWO).byteValue() == 1) {
|
||||
r = addPoint(r, s);
|
||||
}
|
||||
s = doublePoint(s);
|
||||
kModP = kModP.divide(TWO);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/** Returns the point addition r+s in Fp. */
|
||||
private static ECPoint addPoint(ECPoint r, ECPoint s) {
|
||||
if (r.equals(s)) {
|
||||
return doublePoint(r);
|
||||
} else if (r.equals(ECPoint.POINT_INFINITY)) {
|
||||
return s;
|
||||
} else if (s.equals(ECPoint.POINT_INFINITY)) {
|
||||
return r;
|
||||
}
|
||||
BigInteger slope =
|
||||
r.getAffineY()
|
||||
.subtract(s.getAffineY())
|
||||
.multiply(r.getAffineX().subtract(s.getAffineX()).modInverse(P))
|
||||
.mod(P);
|
||||
BigInteger x = slope.modPow(TWO, P).subtract(r.getAffineX()).subtract(s.getAffineX()).mod(P);
|
||||
BigInteger y = s.getAffineY().negate().mod(P);
|
||||
y = y.add(slope.multiply(s.getAffineX().subtract(x))).mod(P);
|
||||
return new ECPoint(x, y);
|
||||
}
|
||||
|
||||
/** Returns the point doubling 2*r in Fp. */
|
||||
private static ECPoint doublePoint(ECPoint r) {
|
||||
if (r.equals(ECPoint.POINT_INFINITY)) {
|
||||
return r;
|
||||
}
|
||||
BigInteger slope = r.getAffineX().pow(2).multiply(THREE);
|
||||
slope = slope.add(CURVE_SPEC.getCurve().getA());
|
||||
slope = slope.multiply(r.getAffineY().multiply(TWO).modInverse(P));
|
||||
BigInteger x = slope.pow(2).subtract(r.getAffineX().multiply(TWO)).mod(P);
|
||||
BigInteger y = r.getAffineY().negate().add(slope.multiply(r.getAffineX().subtract(x))).mod(P);
|
||||
return new ECPoint(x, y);
|
||||
}
|
||||
|
||||
private E2eeCalculator() {
|
||||
}
|
||||
}
|
||||
|
|
@ -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 com.android.server.nearby.common.bluetooth.fastpair.testing;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/** Helper for advertising Fast Pair data. */
|
||||
public interface FastPairAdvertiser {
|
||||
|
||||
void startAdvertising(@Nullable byte[] serviceData);
|
||||
|
||||
void stopAdvertising();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue