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:
Eric Lin 2022-01-11 11:56:45 +00:00
commit c7a4d1bb74
39 changed files with 6457 additions and 1 deletions

View file

@ -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

View file

@ -0,0 +1,4 @@
# Bug component: 1092133
ericth@google.com
ryancllin@google.com

View 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"],
},
}

View 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>

View 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

View 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"],
}

View file

@ -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;
}

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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"
}
}

View file

@ -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()
)
}
}

View file

@ -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)
}
}
}

View file

@ -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"
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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())
}
}
}

View file

@ -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() {
}
}

View file

@ -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() {
}
}

View file

@ -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