| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.pandora |
| |
| import android.bluetooth.BluetoothAdapter |
| import android.bluetooth.BluetoothAssignedNumbers |
| import android.bluetooth.BluetoothDevice |
| import android.bluetooth.BluetoothDevice.ADDRESS_TYPE_PUBLIC |
| import android.bluetooth.BluetoothDevice.BOND_BONDED |
| import android.bluetooth.BluetoothDevice.TRANSPORT_BREDR |
| import android.bluetooth.BluetoothDevice.TRANSPORT_LE |
| import android.bluetooth.BluetoothManager |
| import android.bluetooth.BluetoothProfile |
| import android.bluetooth.BluetoothUuid |
| import android.bluetooth.le.AdvertiseCallback |
| import android.bluetooth.le.AdvertiseData |
| import android.bluetooth.le.AdvertiseSettings |
| import android.bluetooth.le.AdvertisingSetParameters |
| import android.bluetooth.le.ScanCallback |
| import android.bluetooth.le.ScanRecord |
| import android.bluetooth.le.ScanResult |
| import android.content.Context |
| import android.content.Intent |
| import android.content.IntentFilter |
| import android.net.MacAddress |
| import android.os.ParcelUuid |
| import android.util.Log |
| import com.google.protobuf.ByteString |
| import com.google.protobuf.Empty |
| import io.grpc.stub.StreamObserver |
| import java.io.Closeable |
| import java.lang.IllegalArgumentException |
| import java.nio.ByteBuffer |
| import java.time.Duration |
| import java.util.UUID |
| import kotlinx.coroutines.CoroutineScope |
| import kotlinx.coroutines.Dispatchers |
| import kotlinx.coroutines.awaitCancellation |
| import kotlinx.coroutines.cancel |
| import kotlinx.coroutines.channels.awaitClose |
| import kotlinx.coroutines.channels.trySendBlocking |
| import kotlinx.coroutines.delay |
| import kotlinx.coroutines.flow.Flow |
| import kotlinx.coroutines.flow.SharingStarted |
| import kotlinx.coroutines.flow.callbackFlow |
| import kotlinx.coroutines.flow.filter |
| import kotlinx.coroutines.flow.first |
| import kotlinx.coroutines.flow.map |
| import kotlinx.coroutines.flow.shareIn |
| import kotlinx.coroutines.launch |
| import kotlinx.coroutines.runBlocking |
| import pandora.HostGrpc.HostImplBase |
| import pandora.HostProto.* |
| |
| object ByteArrayOps { |
| public fun getUShortAt(input: ByteArray, index: Int): UShort { |
| return (((input[index + 1].toUInt() and 0xffU) shl 8) or (input[index].toUInt() and 0xffU)) |
| .toUShort() |
| } |
| |
| public fun getShortAt(input: ByteArray, index: Int): Short { |
| return getUShortAt(input, index).toShort() |
| } |
| |
| public fun getUIntAt(input: ByteArray, index: Int): UInt { |
| return (((input[index + 3].toUInt() and 0xffU) shl 24) or |
| ((input[index + 2].toUInt() and 0xffU) shl 16) or |
| ((input[index + 1].toUInt() and 0xffU) shl 8) or |
| (input[index].toUInt() and 0xffU)) |
| } |
| |
| public fun getIntAt(input: ByteArray, index: Int): Int { |
| return getUIntAt(input, index).toInt() |
| } |
| |
| public fun getUInt24At(input: ByteArray, index: Int): UInt { |
| return (((input[index + 2].toUInt() and 0xffU) shl 16) or |
| ((input[index + 1].toUInt() and 0xffU) shl 8) or |
| (input[index].toUInt() and 0xffU)) |
| } |
| |
| public fun getInt24At(input: ByteArray, index: Int): Int { |
| return getUInt24At(input, index).toInt() |
| } |
| } |
| |
| @kotlinx.coroutines.ExperimentalCoroutinesApi |
| class Host( |
| private val context: Context, |
| private val security: Security, |
| private val server: Server |
| ) : HostImplBase(), Closeable { |
| private val TAG = "PandoraHost" |
| |
| private val scope: CoroutineScope |
| private val flow: Flow<Intent> |
| |
| private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!! |
| private val bluetoothAdapter = bluetoothManager.adapter |
| |
| private var connectability = ConnectabilityMode.NOT_CONNECTABLE |
| private var discoverability = DiscoverabilityMode.NOT_DISCOVERABLE |
| |
| private val advertisers = mutableMapOf<UUID, AdvertiseCallback>() |
| |
| init { |
| scope = CoroutineScope(Dispatchers.Default.limitedParallelism(1)) |
| |
| // Add all intent actions to be listened. |
| val intentFilter = IntentFilter() |
| intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED) |
| intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED) |
| intentFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED) |
| intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST) |
| intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED) |
| intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED) |
| intentFilter.addAction(BluetoothDevice.ACTION_FOUND) |
| |
| // Creates a shared flow of intents that can be used in all methods in the coroutine scope. |
| // This flow is started eagerly to make sure that the broadcast receiver is registered |
| // before |
| // any function call. This flow is only cancelled when the corresponding scope is cancelled. |
| flow = intentFlow(context, intentFilter, scope).shareIn(scope, SharingStarted.Eagerly) |
| } |
| |
| override fun close() { |
| scope.cancel() |
| } |
| |
| private suspend fun rebootBluetooth() { |
| Log.i(TAG, "rebootBluetooth") |
| |
| val stateFlow = |
| flow |
| .filter { it.getAction() == BluetoothAdapter.ACTION_STATE_CHANGED } |
| .map { it.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR) } |
| |
| if (bluetoothAdapter.isEnabled) { |
| bluetoothAdapter.disable() |
| stateFlow.filter { it == BluetoothAdapter.STATE_OFF }.first() |
| } |
| |
| // TODO: b/234892968 |
| delay(3000L) |
| |
| bluetoothAdapter.enable() |
| stateFlow.filter { it == BluetoothAdapter.STATE_ON }.first() |
| } |
| |
| override fun factoryReset(request: Empty, responseObserver: StreamObserver<Empty>) { |
| grpcUnary<Empty>(scope, responseObserver, 30) { |
| Log.i(TAG, "factoryReset") |
| |
| val stateFlow = |
| flow |
| .filter { it.getAction() == BluetoothAdapter.ACTION_STATE_CHANGED } |
| .map { it.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR) } |
| |
| initiatedConnection.clear() |
| waitedAclConnection.clear() |
| waitedAclDisconnection.clear() |
| |
| bluetoothAdapter.clearBluetooth() |
| |
| stateFlow.filter { it == BluetoothAdapter.STATE_ON }.first() |
| // Delay to initialize the Bluetooth completely and to fix flakiness: b/266611263 |
| delay(1000L) |
| Log.i(TAG, "Shutdown the gRPC Server") |
| server.shutdown() |
| |
| // The last expression is the return value. |
| Empty.getDefaultInstance() |
| } |
| } |
| |
| override fun reset(request: Empty, responseObserver: StreamObserver<Empty>) { |
| grpcUnary<Empty>(scope, responseObserver) { |
| Log.i(TAG, "reset") |
| initiatedConnection.clear() |
| waitedAclConnection.clear() |
| waitedAclDisconnection.clear() |
| |
| rebootBluetooth() |
| |
| Empty.getDefaultInstance() |
| } |
| } |
| |
| override fun readLocalAddress( |
| request: Empty, |
| responseObserver: StreamObserver<ReadLocalAddressResponse> |
| ) { |
| grpcUnary<ReadLocalAddressResponse>(scope, responseObserver) { |
| Log.i(TAG, "readLocalAddress") |
| val localMacAddress = MacAddress.fromString(bluetoothAdapter.getAddress()) |
| ReadLocalAddressResponse.newBuilder() |
| .setAddress(ByteString.copyFrom(localMacAddress.toByteArray())) |
| .build() |
| } |
| } |
| |
| private suspend fun waitPairingRequestIntent(bluetoothDevice: BluetoothDevice) { |
| Log.i(TAG, "waitPairingRequestIntent: device=$bluetoothDevice") |
| var pairingVariant = |
| flow |
| .filter { it.getAction() == BluetoothDevice.ACTION_PAIRING_REQUEST } |
| .filter { it.getBluetoothDeviceExtra() == bluetoothDevice } |
| .first() |
| .getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR) |
| |
| val confirmationCases = |
| intArrayOf( |
| BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION, |
| BluetoothDevice.PAIRING_VARIANT_CONSENT, |
| BluetoothDevice.PAIRING_VARIANT_PIN, |
| ) |
| |
| if (pairingVariant in confirmationCases) { |
| bluetoothDevice.setPairingConfirmation(true) |
| } |
| } |
| |
| private suspend fun waitConnectionIntent(bluetoothDevice: BluetoothDevice) { |
| Log.i(TAG, "waitConnectionIntent: device=$bluetoothDevice") |
| flow |
| .filter { it.action == BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED } |
| .filter { it.getBluetoothDeviceExtra() == bluetoothDevice } |
| .map { it.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, BluetoothAdapter.ERROR) } |
| .filter { it == BluetoothAdapter.STATE_CONNECTED } |
| .first() |
| } |
| |
| suspend fun waitBondIntent(bluetoothDevice: BluetoothDevice) { |
| // We only wait for bonding to be completed since we only need the ACL connection to be |
| // established with the peer device (on Android state connected is sent when all profiles |
| // have been connected). |
| Log.i(TAG, "waitBondIntent: device=$bluetoothDevice") |
| flow |
| .filter { it.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED } |
| .filter { it.getBluetoothDeviceExtra() == bluetoothDevice } |
| .map { it.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothAdapter.ERROR) } |
| .filter { it == BOND_BONDED } |
| .first() |
| } |
| |
| suspend fun waitIncomingAclConnectedIntent(address: String?, transport: Int): Intent { |
| return flow |
| .filter { it.action == BluetoothDevice.ACTION_ACL_CONNECTED } |
| .filter { address == null || it.getBluetoothDeviceExtra().address == address } |
| .filter { !initiatedConnection.contains(it.getBluetoothDeviceExtra()) } |
| .filter { |
| it.getIntExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.ERROR) == transport |
| } |
| .first() |
| } |
| |
| private suspend fun acceptPairingAndAwaitBonded(bluetoothDevice: BluetoothDevice) { |
| val acceptPairingJob = scope.launch { waitPairingRequestIntent(bluetoothDevice) } |
| waitBondIntent(bluetoothDevice) |
| if (acceptPairingJob.isActive) { |
| acceptPairingJob.cancel() |
| } |
| } |
| |
| override fun waitConnection( |
| request: WaitConnectionRequest, |
| responseObserver: StreamObserver<WaitConnectionResponse> |
| ) { |
| grpcUnary(scope, responseObserver) { |
| if (request.address.isEmpty()) |
| throw IllegalArgumentException("Request address field must be set") |
| var bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter) |
| |
| Log.i(TAG, "waitConnection: device=$bluetoothDevice") |
| |
| if (!bluetoothAdapter.isEnabled) { |
| throw RuntimeException("Bluetooth is not enabled, cannot waitConnection") |
| } |
| |
| if (!bluetoothDevice.isConnected() || waitedAclConnection.contains(bluetoothDevice)) { |
| bluetoothDevice = |
| waitIncomingAclConnectedIntent(bluetoothDevice.address, TRANSPORT_BREDR) |
| .getBluetoothDeviceExtra() |
| } |
| |
| waitedAclConnection.add(bluetoothDevice) |
| |
| WaitConnectionResponse.newBuilder() |
| .setConnection(bluetoothDevice.toConnection(TRANSPORT_BREDR)) |
| .build() |
| } |
| } |
| |
| override fun waitDisconnection( |
| request: WaitDisconnectionRequest, |
| responseObserver: StreamObserver<Empty> |
| ) { |
| grpcUnary(scope, responseObserver) { |
| val bluetoothDevice = request.connection.toBluetoothDevice(bluetoothAdapter) |
| Log.i(TAG, "waitDisconnection: device=$bluetoothDevice") |
| if (!bluetoothAdapter.isEnabled) { |
| throw RuntimeException("Bluetooth is not enabled, cannot waitDisconnection") |
| } |
| if ( |
| bluetoothDevice.isConnected() && !waitedAclDisconnection.contains(bluetoothDevice) |
| ) { |
| flow |
| .filter { it.action == BluetoothDevice.ACTION_ACL_DISCONNECTED } |
| .filter { it.getBluetoothDeviceExtra() == bluetoothDevice } |
| .first() |
| } |
| |
| waitedAclDisconnection.add(bluetoothDevice) |
| |
| Empty.getDefaultInstance() |
| } |
| } |
| |
| override fun connect( |
| request: ConnectRequest, |
| responseObserver: StreamObserver<ConnectResponse> |
| ) { |
| grpcUnary(scope, responseObserver) { |
| if (request.address.isEmpty()) |
| throw IllegalArgumentException("Request address field must be set") |
| val bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter) |
| |
| Log.i(TAG, "connect: address=$bluetoothDevice") |
| |
| initiatedConnection.add(bluetoothDevice) |
| bluetoothAdapter.cancelDiscovery() |
| |
| if (!bluetoothDevice.isConnected()) { |
| if (bluetoothDevice.bondState == BOND_BONDED) { |
| // already bonded, just reconnect |
| bluetoothDevice.connect() |
| waitConnectionIntent(bluetoothDevice) |
| } else { |
| // need to bond |
| bluetoothDevice.createBond() |
| if (!security.manuallyConfirm) { |
| acceptPairingAndAwaitBonded(bluetoothDevice) |
| } |
| } |
| } |
| |
| ConnectResponse.newBuilder() |
| .setConnection(bluetoothDevice.toConnection(TRANSPORT_BREDR)) |
| .build() |
| } |
| } |
| |
| override fun disconnect(request: DisconnectRequest, responseObserver: StreamObserver<Empty>) { |
| grpcUnary<Empty>(scope, responseObserver) { |
| val bluetoothDevice = request.connection.toBluetoothDevice(bluetoothAdapter) |
| Log.i(TAG, "disconnect: device=$bluetoothDevice") |
| |
| if (!bluetoothDevice.isConnected()) { |
| throw RuntimeException("Device is not connected, cannot disconnect") |
| } |
| |
| when (request.connection.transport) { |
| TRANSPORT_BREDR -> { |
| Log.i(TAG, "disconnect BR_EDR") |
| bluetoothDevice.disconnect() |
| } |
| TRANSPORT_LE -> { |
| Log.i(TAG, "disconnect LE") |
| val gattInstance = |
| try { |
| GattInstance.get(bluetoothDevice.address) |
| } catch (e: Exception) { |
| Log.w(TAG, "Gatt instance doesn't exist. Android might be peripheral") |
| val instance = GattInstance(bluetoothDevice, TRANSPORT_LE, context) |
| instance.waitForState(BluetoothProfile.STATE_CONNECTED) |
| instance |
| } |
| if (gattInstance.isDisconnected()) { |
| throw RuntimeException("Device is not connected, cannot disconnect") |
| } |
| |
| bluetoothDevice.disconnect() |
| gattInstance.disconnectInstance() |
| } |
| else -> { |
| throw RuntimeException("Device type UNKNOWN") |
| } |
| } |
| flow |
| .filter { it.action == BluetoothDevice.ACTION_ACL_DISCONNECTED } |
| .filter { it.getBluetoothDeviceExtra() == bluetoothDevice } |
| .first() |
| |
| Empty.getDefaultInstance() |
| } |
| } |
| |
| override fun connectLE( |
| request: ConnectLERequest, |
| responseObserver: StreamObserver<ConnectLEResponse> |
| ) { |
| grpcUnary<ConnectLEResponse>(scope, responseObserver) { |
| val ownAddressType = request.ownAddressType |
| if ( |
| ownAddressType != OwnAddressType.RANDOM && |
| ownAddressType != OwnAddressType.RESOLVABLE_OR_RANDOM |
| ) { |
| throw RuntimeException("connectLE: Unsupported OwnAddressType: $ownAddressType") |
| } |
| val (address, type) = |
| when (request.getAddressCase()!!) { |
| ConnectLERequest.AddressCase.PUBLIC -> |
| Pair(request.public, BluetoothDevice.ADDRESS_TYPE_PUBLIC) |
| ConnectLERequest.AddressCase.RANDOM -> |
| Pair(request.random, BluetoothDevice.ADDRESS_TYPE_RANDOM) |
| ConnectLERequest.AddressCase.PUBLIC_IDENTITY -> |
| Pair(request.publicIdentity, BluetoothDevice.ADDRESS_TYPE_PUBLIC) |
| ConnectLERequest.AddressCase.RANDOM_STATIC_IDENTITY -> |
| Pair(request.randomStaticIdentity, BluetoothDevice.ADDRESS_TYPE_RANDOM) |
| ConnectLERequest.AddressCase.ADDRESS_NOT_SET -> |
| throw IllegalArgumentException("Request address field must be set") |
| } |
| Log.i(TAG, "connectLE: $address") |
| val bluetoothDevice = scanLeDevice(address.decodeAsMacAddressToString(), type)!! |
| initiatedConnection.add(bluetoothDevice) |
| GattInstance(bluetoothDevice, TRANSPORT_LE, context) |
| .waitForState(BluetoothProfile.STATE_CONNECTED) |
| ConnectLEResponse.newBuilder() |
| .setConnection(bluetoothDevice.toConnection(TRANSPORT_LE)) |
| .build() |
| } |
| } |
| |
| private fun scanLeDevice(address: String, addressType: Int): BluetoothDevice? { |
| Log.d(TAG, "scanLeDevice") |
| var bluetoothDevice: BluetoothDevice? = null |
| runBlocking { |
| val flow = callbackFlow { |
| val leScanCallback = |
| object : ScanCallback() { |
| override fun onScanFailed(errorCode: Int) { |
| super.onScanFailed(errorCode) |
| Log.d(TAG, "onScanFailed: errorCode: $errorCode") |
| trySendBlocking(null) |
| } |
| override fun onScanResult(callbackType: Int, result: ScanResult) { |
| super.onScanResult(callbackType, result) |
| val deviceAddress = result.device.address |
| val deviceAddressType = result.device.addressType |
| if (deviceAddress == address && deviceAddressType == addressType) { |
| Log.d(TAG, "found device address: $deviceAddress") |
| trySendBlocking(result.device) |
| } |
| } |
| } |
| val bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner |
| bluetoothLeScanner?.startScan(leScanCallback) ?: run { trySendBlocking(null) } |
| awaitClose { bluetoothLeScanner?.stopScan(leScanCallback) } |
| } |
| bluetoothDevice = flow.first() |
| } |
| return bluetoothDevice |
| } |
| |
| override fun advertise( |
| request: AdvertiseRequest, |
| responseObserver: StreamObserver<AdvertiseResponse> |
| ) { |
| Log.d(TAG, "advertise") |
| grpcServerStream(scope, responseObserver) { |
| callbackFlow { |
| val callback = |
| object : AdvertiseCallback() { |
| override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { |
| Log.d(TAG, "advertising started") |
| } |
| override fun onStartFailure(errorCode: Int) { |
| error("failed to start advertising: $errorCode") |
| } |
| } |
| val advertisingDataBuilder = AdvertiseData.Builder() |
| val dataTypesRequest = request.data |
| |
| if ( |
| !dataTypesRequest.getIncompleteServiceClassUuids16List().isEmpty() or |
| !dataTypesRequest.getIncompleteServiceClassUuids32List().isEmpty() or |
| !dataTypesRequest.getIncompleteServiceClassUuids128List().isEmpty() |
| ) { |
| throw RuntimeException("Incomplete Service Class Uuids not supported") |
| } |
| |
| // Handle service uuids |
| for (uuid16 in dataTypesRequest.getCompleteServiceClassUuids16List()) { |
| val parcel_uuid16 = |
| ParcelUuid.fromString("0000${uuid16}-0000-1000-8000-00805F9B34FB") |
| advertisingDataBuilder.addServiceUuid(parcel_uuid16) |
| } |
| for (uuid32 in dataTypesRequest.getCompleteServiceClassUuids32List()) { |
| val parcel_uuid32 = |
| ParcelUuid.fromString("${uuid32}-0000-1000-8000-00805F9B34FB") |
| advertisingDataBuilder.addServiceUuid(parcel_uuid32) |
| } |
| for (uuid128 in dataTypesRequest.getCompleteServiceClassUuids128List()) { |
| advertisingDataBuilder.addServiceUuid(ParcelUuid.fromString(uuid128)) |
| } |
| |
| // Handle Service solicitation uuids |
| for (uuid16 in dataTypesRequest.getServiceSolicitationUuids16List()) { |
| val parcel_uuid16 = |
| ParcelUuid.fromString("0000${uuid16}-0000-1000-8000-00805F9B34FB") |
| advertisingDataBuilder.addServiceSolicitationUuid(parcel_uuid16) |
| } |
| for (uuid32 in dataTypesRequest.getServiceSolicitationUuids32List()) { |
| val parcel_uuid32 = |
| ParcelUuid.fromString("${uuid32}-0000-1000-8000-00805F9B34FB") |
| advertisingDataBuilder.addServiceSolicitationUuid(parcel_uuid32) |
| } |
| for (uuid128 in dataTypesRequest.getServiceSolicitationUuids128List()) { |
| advertisingDataBuilder.addServiceSolicitationUuid( |
| ParcelUuid.fromString(uuid128) |
| ) |
| } |
| |
| // Handle service data uuids |
| for ((uuid16, data) in dataTypesRequest.getServiceDataUuid16()) { |
| val parcel_uuid16 = |
| ParcelUuid.fromString("0000${uuid16}-0000-1000-8000-00805F9B34FB") |
| advertisingDataBuilder.addServiceData(parcel_uuid16, data.toByteArray()) |
| } |
| for ((uuid32, data) in dataTypesRequest.getServiceDataUuid32()) { |
| val parcel_uuid32 = |
| ParcelUuid.fromString("${uuid32}-0000-1000-8000-00805F9B34FB") |
| advertisingDataBuilder.addServiceData(parcel_uuid32, data.toByteArray()) |
| } |
| for ((uuid128, data) in dataTypesRequest.getServiceDataUuid128()) { |
| advertisingDataBuilder.addServiceData( |
| ParcelUuid.fromString(uuid128), |
| data.toByteArray() |
| ) |
| } |
| |
| advertisingDataBuilder |
| .setIncludeDeviceName( |
| dataTypesRequest.includeCompleteLocalName || |
| dataTypesRequest.includeShortenedLocalName |
| ) |
| .setIncludeTxPowerLevel(dataTypesRequest.includeTxPowerLevel) |
| .addManufacturerData( |
| BluetoothAssignedNumbers.GOOGLE, |
| dataTypesRequest.manufacturerSpecificData.toByteArray() |
| ) |
| val advertisingData = advertisingDataBuilder.build() |
| |
| val ownAddressType = |
| when (request.ownAddressType) { |
| OwnAddressType.RESOLVABLE_OR_PUBLIC, |
| OwnAddressType.PUBLIC -> AdvertisingSetParameters.ADDRESS_TYPE_PUBLIC |
| OwnAddressType.RESOLVABLE_OR_RANDOM, |
| OwnAddressType.RANDOM -> AdvertisingSetParameters.ADDRESS_TYPE_RANDOM |
| else -> AdvertisingSetParameters.ADDRESS_TYPE_DEFAULT |
| } |
| val advertiseSettings = |
| AdvertiseSettings.Builder() |
| .setConnectable(request.connectable) |
| .setOwnAddressType(ownAddressType) |
| .build() |
| |
| bluetoothAdapter.bluetoothLeAdvertiser.startAdvertising( |
| advertiseSettings, |
| advertisingData, |
| callback, |
| ) |
| |
| if (request.connectable) { |
| while (true) { |
| Log.d(TAG, "Waiting for incoming connection") |
| val connection = |
| waitIncomingAclConnectedIntent(null, TRANSPORT_LE) |
| .getBluetoothDeviceExtra() |
| .toConnection(TRANSPORT_LE) |
| Log.d(TAG, "Receive connection") |
| trySendBlocking( |
| AdvertiseResponse.newBuilder().setConnection(connection).build() |
| ) |
| } |
| } |
| |
| awaitClose { bluetoothAdapter.bluetoothLeAdvertiser.stopAdvertising(callback) } |
| } |
| } |
| } |
| |
| // TODO: Handle request parameters |
| override fun scan(request: ScanRequest, responseObserver: StreamObserver<ScanningResponse>) { |
| Log.d(TAG, "scan") |
| grpcServerStream(scope, responseObserver) { |
| callbackFlow { |
| val callback = |
| object : ScanCallback() { |
| override fun onScanResult(callbackType: Int, result: ScanResult) { |
| val bluetoothDevice = result.device |
| val scanRecord = result.scanRecord |
| checkNotNull(scanRecord) |
| val scanData = scanRecord.getAdvertisingDataMap() |
| val serviceData = scanRecord.serviceData |
| checkNotNull(serviceData) |
| |
| var dataTypesBuilder = |
| DataTypes.newBuilder().setTxPowerLevel(scanRecord.getTxPowerLevel()) |
| |
| scanData[ScanRecord.DATA_TYPE_LOCAL_NAME_SHORT]?.let { |
| dataTypesBuilder.setShortenedLocalName(it.decodeToString()) |
| } |
| ?: run { dataTypesBuilder.setIncludeShortenedLocalName(false) } |
| |
| scanData[ScanRecord.DATA_TYPE_LOCAL_NAME_COMPLETE]?.let { |
| dataTypesBuilder.setCompleteLocalName(it.decodeToString()) |
| } |
| ?: run { dataTypesBuilder.setIncludeCompleteLocalName(false) } |
| |
| scanData[ScanRecord.DATA_TYPE_ADVERTISING_INTERVAL]?.let { |
| dataTypesBuilder.setAdvertisingInterval( |
| ByteArrayOps.getShortAt(it, 0).toInt() |
| ) |
| } |
| |
| scanData[ScanRecord.DATA_TYPE_ADVERTISING_INTERVAL_LONG]?.let { |
| dataTypesBuilder.setAdvertisingInterval( |
| ByteArrayOps.getIntAt(it, 0) |
| ) |
| } |
| |
| scanData[ScanRecord.DATA_TYPE_APPEARANCE]?.let { |
| dataTypesBuilder.setAppearance( |
| ByteArrayOps.getShortAt(it, 0).toInt() |
| ) |
| } |
| |
| scanData[ScanRecord.DATA_TYPE_CLASS_OF_DEVICE]?.let { |
| dataTypesBuilder.setClassOfDevice(ByteArrayOps.getInt24At(it, 0)) |
| } |
| |
| scanData[ScanRecord.DATA_TYPE_URI]?.let { |
| dataTypesBuilder.setUri(it.decodeToString()) |
| } |
| |
| scanData[ScanRecord.DATA_TYPE_LE_SUPPORTED_FEATURES]?.let { |
| dataTypesBuilder.setLeSupportedFeatures(ByteString.copyFrom(it)) |
| } |
| |
| scanData[ScanRecord.DATA_TYPE_SLAVE_CONNECTION_INTERVAL_RANGE]?.let { |
| dataTypesBuilder.setPeripheralConnectionIntervalMin( |
| ByteArrayOps.getShortAt(it, 0).toInt() |
| ) |
| dataTypesBuilder.setPeripheralConnectionIntervalMax( |
| ByteArrayOps.getShortAt(it, 2).toInt() |
| ) |
| } |
| |
| for (serviceDataEntry in serviceData) { |
| val parcelUuid = serviceDataEntry.key |
| Log.d(TAG, parcelUuid.uuid.toString().uppercase()) |
| |
| // use upper case uuid as the key |
| if (BluetoothUuid.is16BitUuid(parcelUuid)) { |
| val uuid16 = |
| parcelUuid.uuid.toString().substring(4, 8).uppercase() |
| dataTypesBuilder.putServiceDataUuid16( |
| uuid16, |
| ByteString.copyFrom(serviceDataEntry.value) |
| ) |
| } else if (BluetoothUuid.is32BitUuid(parcelUuid)) { |
| val uuid32 = |
| parcelUuid.uuid.toString().substring(0, 8).uppercase() |
| dataTypesBuilder.putServiceDataUuid32( |
| uuid32, |
| ByteString.copyFrom(serviceDataEntry.value) |
| ) |
| } else { |
| val uuid128 = parcelUuid.uuid.toString().uppercase() |
| dataTypesBuilder.putServiceDataUuid128( |
| uuid128, |
| ByteString.copyFrom(serviceDataEntry.value) |
| ) |
| } |
| } |
| |
| for (serviceUuid in |
| scanRecord.serviceSolicitationUuids ?: listOf<ParcelUuid>()) { |
| Log.d(TAG, serviceUuid.uuid.toString().uppercase()) |
| if (BluetoothUuid.is16BitUuid(serviceUuid)) { |
| val uuid16 = |
| serviceUuid.uuid.toString().substring(4, 8).uppercase() |
| dataTypesBuilder.addServiceSolicitationUuids16(uuid16) |
| } else if (BluetoothUuid.is32BitUuid(serviceUuid)) { |
| val uuid32 = |
| serviceUuid.uuid.toString().substring(0, 8).uppercase() |
| dataTypesBuilder.addServiceSolicitationUuids32(uuid32) |
| } else { |
| val uuid128 = serviceUuid.uuid.toString().uppercase() |
| dataTypesBuilder.addServiceSolicitationUuids128(uuid128) |
| } |
| } |
| |
| for (serviceUuid in scanRecord.serviceUuids ?: listOf<ParcelUuid>()) { |
| Log.d(TAG, serviceUuid.uuid.toString().uppercase()) |
| if (BluetoothUuid.is16BitUuid(serviceUuid)) { |
| val uuid16 = |
| serviceUuid.uuid.toString().substring(4, 8).uppercase() |
| dataTypesBuilder.addIncompleteServiceClassUuids16(uuid16) |
| } else if (BluetoothUuid.is32BitUuid(serviceUuid)) { |
| val uuid32 = |
| serviceUuid.uuid.toString().substring(0, 8).uppercase() |
| dataTypesBuilder.addIncompleteServiceClassUuids32(uuid32) |
| } else { |
| val uuid128 = serviceUuid.uuid.toString().uppercase() |
| dataTypesBuilder.addIncompleteServiceClassUuids128(uuid128) |
| } |
| } |
| |
| // Flags DataTypes CSSv10 1.3 Flags |
| val mode: DiscoverabilityMode = |
| when (scanRecord.advertiseFlags and 0b11) { |
| 0b01 -> DiscoverabilityMode.DISCOVERABLE_LIMITED |
| 0b10 -> DiscoverabilityMode.DISCOVERABLE_GENERAL |
| else -> DiscoverabilityMode.NOT_DISCOVERABLE |
| } |
| dataTypesBuilder.setLeDiscoverabilityMode(mode) |
| var manufacturerData = ByteBuffer.allocate(512) |
| val manufacturerSpecificDatas = scanRecord.getManufacturerSpecificData() |
| for (i in 0..manufacturerSpecificDatas.size() - 1) { |
| val id = manufacturerSpecificDatas.keyAt(i) |
| manufacturerData |
| .put(id.toByte()) |
| .put(id.shr(8).toByte()) |
| .put(manufacturerSpecificDatas.get(id)) |
| } |
| dataTypesBuilder.setManufacturerSpecificData( |
| ByteString.copyFrom( |
| manufacturerData.array(), |
| 0, |
| manufacturerData.position() |
| ) |
| ) |
| val primaryPhy = |
| when (result.getPrimaryPhy()) { |
| BluetoothDevice.PHY_LE_1M -> PrimaryPhy.PRIMARY_1M |
| BluetoothDevice.PHY_LE_CODED -> PrimaryPhy.PRIMARY_CODED |
| else -> PrimaryPhy.UNRECOGNIZED |
| } |
| val secondaryPhy = |
| when (result.getSecondaryPhy()) { |
| ScanResult.PHY_UNUSED -> SecondaryPhy.SECONDARY_NONE |
| BluetoothDevice.PHY_LE_1M -> SecondaryPhy.SECONDARY_1M |
| BluetoothDevice.PHY_LE_2M -> SecondaryPhy.SECONDARY_2M |
| BluetoothDevice.PHY_LE_CODED -> SecondaryPhy.SECONDARY_CODED |
| else -> SecondaryPhy.UNRECOGNIZED |
| } |
| var scanningResponseBuilder = |
| ScanningResponse.newBuilder() |
| .setLegacy(result.isLegacy()) |
| .setConnectable(result.isConnectable()) |
| .setTruncated( |
| result.getDataStatus() == ScanResult.DATA_TRUNCATED |
| ) |
| .setSid(result.getAdvertisingSid()) |
| .setPrimaryPhy(primaryPhy) |
| .setSecondaryPhy(secondaryPhy) |
| .setTxPower(result.getTxPower()) |
| .setRssi(result.getRssi()) |
| .setPeriodicAdvertisingInterval( |
| result.getPeriodicAdvertisingInterval().toFloat() |
| ) |
| .setData(dataTypesBuilder.build()) |
| when (bluetoothDevice.addressType) { |
| BluetoothDevice.ADDRESS_TYPE_PUBLIC -> |
| scanningResponseBuilder.setPublic( |
| bluetoothDevice.toByteString() |
| ) |
| BluetoothDevice.ADDRESS_TYPE_RANDOM -> |
| scanningResponseBuilder.setRandom( |
| bluetoothDevice.toByteString() |
| ) |
| else -> |
| Log.w( |
| TAG, |
| "Address type UNKNOWN: ${bluetoothDevice.type} addr: $bluetoothDevice" |
| ) |
| } |
| // TODO: Complete the missing field as needed, all the examples are here |
| trySendBlocking(scanningResponseBuilder.build()) |
| } |
| |
| override fun onScanFailed(errorCode: Int) { |
| error("scan failed") |
| } |
| } |
| bluetoothAdapter.bluetoothLeScanner.startScan(callback) |
| |
| awaitClose { bluetoothAdapter.bluetoothLeScanner.stopScan(callback) } |
| } |
| } |
| } |
| |
| override fun inquiry(request: Empty, responseObserver: StreamObserver<InquiryResponse>) { |
| Log.d(TAG, "Inquiry") |
| grpcServerStream(scope, responseObserver) { |
| launch { |
| try { |
| bluetoothAdapter.startDiscovery() |
| awaitCancellation() |
| } finally { |
| bluetoothAdapter.cancelDiscovery() |
| } |
| } |
| flow |
| .filter { it.action == BluetoothDevice.ACTION_FOUND } |
| .map { |
| val bluetoothDevice = it.getBluetoothDeviceExtra() |
| Log.i(TAG, "Device found: $bluetoothDevice") |
| InquiryResponse.newBuilder().setAddress(bluetoothDevice.toByteString()).build() |
| } |
| } |
| } |
| |
| override fun setDiscoverabilityMode( |
| request: SetDiscoverabilityModeRequest, |
| responseObserver: StreamObserver<Empty> |
| ) { |
| Log.d(TAG, "setDiscoverabilityMode") |
| grpcUnary(scope, responseObserver) { |
| discoverability = request.mode!! |
| |
| val scanMode = |
| when (discoverability) { |
| DiscoverabilityMode.UNRECOGNIZED -> null |
| DiscoverabilityMode.NOT_DISCOVERABLE -> |
| if (connectability == ConnectabilityMode.CONNECTABLE) { |
| BluetoothAdapter.SCAN_MODE_CONNECTABLE |
| } else { |
| BluetoothAdapter.SCAN_MODE_NONE |
| } |
| DiscoverabilityMode.DISCOVERABLE_LIMITED, |
| DiscoverabilityMode.DISCOVERABLE_GENERAL -> |
| BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE |
| } |
| |
| if (scanMode != null) { |
| bluetoothAdapter.setScanMode(scanMode) |
| } |
| |
| if (discoverability == DiscoverabilityMode.DISCOVERABLE_LIMITED) { |
| bluetoothAdapter.setDiscoverableTimeout( |
| Duration.ofSeconds(120) |
| ) // limited discoverability needs a timeout, 120s is Android default |
| } |
| Empty.getDefaultInstance() |
| } |
| } |
| |
| override fun setConnectabilityMode( |
| request: SetConnectabilityModeRequest, |
| responseObserver: StreamObserver<Empty> |
| ) { |
| grpcUnary(scope, responseObserver) { |
| Log.d(TAG, "setConnectabilityMode") |
| connectability = request.mode!! |
| |
| val scanMode = |
| when (connectability) { |
| ConnectabilityMode.UNRECOGNIZED -> null |
| ConnectabilityMode.NOT_CONNECTABLE -> { |
| BluetoothAdapter.SCAN_MODE_NONE |
| } |
| ConnectabilityMode.CONNECTABLE -> { |
| if ( |
| discoverability == DiscoverabilityMode.DISCOVERABLE_LIMITED || |
| discoverability == DiscoverabilityMode.DISCOVERABLE_GENERAL |
| ) { |
| BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE |
| } else { |
| BluetoothAdapter.SCAN_MODE_CONNECTABLE |
| } |
| } |
| } |
| if (scanMode != null) { |
| bluetoothAdapter.setScanMode(scanMode) |
| } |
| Empty.getDefaultInstance() |
| } |
| } |
| } |