Loading android/pandora/server/proto/pandora_experimental/gatt.proto +22 −0 Original line number Diff line number Diff line Loading @@ -34,6 +34,9 @@ service GATT { // Reads characteristic with given descriptor handle. rpc ReadCharacteristicDescriptorFromHandle(ReadCharacteristicDescriptorRequest) returns (ReadCharacteristicDescriptorResponse); // Register a GATT service rpc RegisterService(RegisterServiceRequest) returns (RegisterServiceResponse); } enum AttStatusCode { Loading Loading @@ -172,3 +175,22 @@ message ReadCharacteristicDescriptorResponse { AttValue value = 1; AttStatusCode status = 2; } message GattServiceParams { string uuid = 1; repeated GattCharacteristicParams characteristics = 2; } message GattCharacteristicParams { uint32 properties = 1; uint32 permissions = 2; string uuid = 3; } message RegisterServiceRequest { GattServiceParams service = 1; } message RegisterServiceResponse { GattService service = 1; } android/pandora/server/proto/pandora_experimental/host.proto +24 −0 Original line number Diff line number Diff line Loading @@ -43,6 +43,10 @@ service Host { rpc DisconnectLE(DisconnectLERequest) returns (google.protobuf.Empty); // Start LE advertisement rpc SetLEConnectable(google.protobuf.Empty) returns (google.protobuf.Empty); // Run BR/EDR inquiry and returns each device found rpc RunInquiry(RunInquiryRequest) returns (stream RunInquiryResponse); // Run LE discovery (scanning) and return each device found rpc RunDiscovery(RunDiscoveryRequest) returns (stream RunDiscoveryResponse); } // Response of the `ReadLocalAddress` method. Loading Loading @@ -141,3 +145,23 @@ message GetLEConnectionResponse { message DisconnectLERequest { Connection connection = 1; } message RunInquiryRequest { } message RunInquiryResponse { repeated Device device = 1; } message RunDiscoveryRequest { } message RunDiscoveryResponse { Device device = 1; uint32 flags = 2; } message Device { string name = 1; bytes address = 2; } android/pandora/server/src/com/android/pandora/Gatt.kt +120 −100 Original line number Diff line number Diff line Loading @@ -20,18 +20,20 @@ import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothGattService.SERVICE_TYPE_PRIMARY import android.bluetooth.BluetoothManager import android.content.Context import android.content.Intent import android.content.IntentFilter import android.util.Log import com.google.protobuf.Empty import io.grpc.Status import io.grpc.stub.StreamObserver import java.util.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted Loading @@ -45,15 +47,15 @@ import pandora.GattProto.* class Gatt(private val context: Context) : GATTImplBase() { private val TAG = "PandoraGatt" private val mScope: CoroutineScope private val flow: Flow<Intent> private val mScope: CoroutineScope = CoroutineScope(Dispatchers.Default) private val mBluetoothManager = context.getSystemService(BluetoothManager::class.java)!! private val mBluetoothAdapter = mBluetoothManager.adapter init { mScope = CoroutineScope(Dispatchers.Default) private val serverManager by lazy { GattServerManager(mBluetoothManager, context, mScope) } private val flow: Flow<Intent> init { val intentFilter = IntentFilter() intentFilter.addAction(BluetoothDevice.ACTION_UUID) Loading @@ -61,10 +63,14 @@ class Gatt(private val context: Context) : GATTImplBase() { } fun deinit() { serverManager.server.close() mScope.cancel() } override fun exchangeMTU(request: ExchangeMTURequest, responseObserver: StreamObserver<ExchangeMTUResponse>) { override fun exchangeMTU( request: ExchangeMTURequest, responseObserver: StreamObserver<ExchangeMTUResponse> ) { grpcUnary<ExchangeMTUResponse>(mScope, responseObserver) { val mtu = request.mtu Log.i(TAG, "exchangeMTU MTU=$mtu") Loading @@ -88,24 +94,16 @@ class Gatt(private val context: Context) : GATTImplBase() { if (characteristic == null) { val descriptor: BluetoothGattDescriptor? = getDescriptorWithHandle(request.handle, gattInstance) checkNotNull(descriptor) { "Found no characteristic or descriptor with handle ${request.handle}" } val valueWrote = gattInstance.writeDescriptorBlocking( descriptor, request.value.toByteArray() ) WriteResponse.newBuilder() .setHandle(valueWrote.handle) .setStatus(valueWrote.status) .build() checkNotNull(descriptor) { "Found no characteristic or descriptor with handle ${request.handle}" } val valueWrote = gattInstance.writeDescriptorBlocking(descriptor, request.value.toByteArray()) WriteResponse.newBuilder().setHandle(valueWrote.handle).setStatus(valueWrote.status).build() } else { val valueWrote = gattInstance.writeCharacteristicBlocking( characteristic, request.value.toByteArray() ) WriteResponse.newBuilder() .setHandle(valueWrote.handle) .setStatus(valueWrote.status) .build() val valueWrote = gattInstance.writeCharacteristicBlocking(characteristic, request.value.toByteArray()) WriteResponse.newBuilder().setHandle(valueWrote.handle).setStatus(valueWrote.status).build() } } } Loading Loading @@ -164,7 +162,10 @@ class Gatt(private val context: Context) : GATTImplBase() { } } override fun clearCache(request: ClearCacheRequest, responseObserver: StreamObserver<ClearCacheResponse>) { override fun clearCache( request: ClearCacheRequest, responseObserver: StreamObserver<ClearCacheResponse> ) { grpcUnary<ClearCacheResponse>(mScope, responseObserver) { Log.i(TAG, "clearCache") val gattInstance = GattInstance.get(request.connection.cookie) Loading @@ -185,11 +186,7 @@ class Gatt(private val context: Context) : GATTImplBase() { checkNotNull(characteristic) { "Characteristic handle ${request.handle} not found." } val readValue = gattInstance.readCharacteristicBlocking(characteristic) ReadCharacteristicResponse.newBuilder() .setValue( AttValue.newBuilder() .setHandle(readValue.handle) .setValue(readValue.value) ) .setValue(AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)) .setStatus(readValue.status) .build() } Loading @@ -205,10 +202,7 @@ class Gatt(private val context: Context) : GATTImplBase() { tryDiscoverServices(gattInstance) val readValues = gattInstance.readCharacteristicUuidBlocking( UUID.fromString(request.uuid), request.startHandle, request.endHandle ) UUID.fromString(request.uuid), request.startHandle, request.endHandle) ReadCharacteristicsFromUuidResponse.newBuilder() .addAllCharacteristicsRead(generateReadValuesList(readValues)) .build() Loading @@ -227,16 +221,46 @@ class Gatt(private val context: Context) : GATTImplBase() { checkNotNull(descriptor) { "Descriptor handle ${request.handle} not found." } val readValue = gattInstance.readDescriptorBlocking(descriptor) ReadCharacteristicDescriptorResponse.newBuilder() .setValue( AttValue.newBuilder() .setHandle(readValue.handle) .setValue(readValue.value) ) .setValue(AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)) .setStatus(readValue.status) .build() } } override fun registerService( request: RegisterServiceRequest, responseObserver: StreamObserver<RegisterServiceResponse> ) { grpcUnary(mScope, responseObserver) { val service = BluetoothGattService(UUID.fromString(request.service.uuid), SERVICE_TYPE_PRIMARY) for (characteristic in request.service.characteristicsList) { service.addCharacteristic( BluetoothGattCharacteristic( UUID.fromString(characteristic.uuid), characteristic.properties, characteristic.permissions)) } val fullService = coroutineScope { val firstService = mScope.async { serverManager.newServiceFlow.first() } serverManager.server.addService(service) firstService.await() } RegisterServiceResponse.newBuilder() .setService( GattService.newBuilder() .setHandle(fullService.instanceId) .setType(fullService.type) .setUuid(fullService.uuid.toString()) .addAllIncludedServices(generateServicesList(service.includedServices, 1)) .addAllCharacteristics(generateCharacteristicsList(service.characteristics)) .build()) .build() } } /** * Discovers services, then returns characteristic with given handle. BluetoothGatt API is * package-private so we have to redefine it here. Loading Loading @@ -338,11 +362,7 @@ class Gatt(private val context: Context) : GATTImplBase() { for (readValue in readValuesList) { val readValueBuilder = ReadCharacteristicResponse.newBuilder() .setValue( AttValue.newBuilder() .setHandle(readValue.handle) .setValue(readValue.value) ) .setValue(AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)) .setStatus(readValue.status) newReadValuesList.add(readValueBuilder.build()) } Loading android/pandora/server/src/com/android/pandora/GattServerManager.kt 0 → 100644 +53 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS 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.BluetoothGatt import android.bluetooth.BluetoothGattServer import android.bluetooth.BluetoothGattServerCallback import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothManager import android.content.Context import java.util.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map class GattServerManager(bluetoothManager: BluetoothManager, context: Context, globalScope: CoroutineScope) { val services = mutableMapOf<UUID, BluetoothGattService>() val server: BluetoothGattServer val newServiceFlow = MutableSharedFlow<BluetoothGattService>(extraBufferCapacity = 8) init { newServiceFlow.map { services[it.uuid] = it }.launchIn(globalScope) } init { val callback = object : BluetoothGattServerCallback() { override fun onServiceAdded(status: Int, service: BluetoothGattService) { check(status == BluetoothGatt.GATT_SUCCESS) check(newServiceFlow.tryEmit(service)) } } server = bluetoothManager.openGattServer(context, callback) } } android/pandora/server/src/com/android/pandora/Host.kt +72 −0 Original line number Diff line number Diff line Loading @@ -42,8 +42,10 @@ import kotlin.Result.Companion.success import kotlin.coroutines.suspendCoroutine 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.sendBlocking import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow Loading Loading @@ -77,6 +79,7 @@ class Host(private val context: Context, private val server: Server) : HostImplB intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED) intentFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED) intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST) 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 Loading Loading @@ -405,4 +408,73 @@ class Host(private val context: Context, private val server: Server) : HostImplB } return bluetoothDevice } override fun runInquiry( request: RunInquiryRequest, responseObserver: StreamObserver<RunInquiryResponse> ) { Log.d(TAG, "runInquiry") grpcServerStream(scope, responseObserver) { launch { try { bluetoothAdapter.startDiscovery() awaitCancellation() } finally { bluetoothAdapter.cancelDiscovery() } } flow .filter { it.action == BluetoothDevice.ACTION_FOUND } .map { val device = it.getBluetoothDeviceExtra() Log.i(TAG, "Device found: $device") RunInquiryResponse.newBuilder() .addDevice( Device.newBuilder() .setName(device.name) .setAddress( ByteString.copyFrom(MacAddress.fromString(device.address).toByteArray()) ) ) .build() } } } override fun runDiscovery( request: RunDiscoveryRequest, responseObserver: StreamObserver<RunDiscoveryResponse> ) { Log.d(TAG, "runDiscovery") grpcServerStream(scope, responseObserver) { callbackFlow { val callback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { sendBlocking( RunDiscoveryResponse.newBuilder() .setDevice( Device.newBuilder() .setAddress( ByteString.copyFrom( MacAddress.fromString(result.device.address).toByteArray() ) ) .setName(result.device.name ?: "") ) .setFlags(result.scanRecord?.advertiseFlags ?: 0) .build() ) } override fun onScanFailed(errorCode: Int) { error("scan failed") } } bluetoothAdapter.bluetoothLeScanner.startScan(callback) awaitClose { bluetoothAdapter.bluetoothLeScanner.stopScan(callback) } } } } } Loading
android/pandora/server/proto/pandora_experimental/gatt.proto +22 −0 Original line number Diff line number Diff line Loading @@ -34,6 +34,9 @@ service GATT { // Reads characteristic with given descriptor handle. rpc ReadCharacteristicDescriptorFromHandle(ReadCharacteristicDescriptorRequest) returns (ReadCharacteristicDescriptorResponse); // Register a GATT service rpc RegisterService(RegisterServiceRequest) returns (RegisterServiceResponse); } enum AttStatusCode { Loading Loading @@ -172,3 +175,22 @@ message ReadCharacteristicDescriptorResponse { AttValue value = 1; AttStatusCode status = 2; } message GattServiceParams { string uuid = 1; repeated GattCharacteristicParams characteristics = 2; } message GattCharacteristicParams { uint32 properties = 1; uint32 permissions = 2; string uuid = 3; } message RegisterServiceRequest { GattServiceParams service = 1; } message RegisterServiceResponse { GattService service = 1; }
android/pandora/server/proto/pandora_experimental/host.proto +24 −0 Original line number Diff line number Diff line Loading @@ -43,6 +43,10 @@ service Host { rpc DisconnectLE(DisconnectLERequest) returns (google.protobuf.Empty); // Start LE advertisement rpc SetLEConnectable(google.protobuf.Empty) returns (google.protobuf.Empty); // Run BR/EDR inquiry and returns each device found rpc RunInquiry(RunInquiryRequest) returns (stream RunInquiryResponse); // Run LE discovery (scanning) and return each device found rpc RunDiscovery(RunDiscoveryRequest) returns (stream RunDiscoveryResponse); } // Response of the `ReadLocalAddress` method. Loading Loading @@ -141,3 +145,23 @@ message GetLEConnectionResponse { message DisconnectLERequest { Connection connection = 1; } message RunInquiryRequest { } message RunInquiryResponse { repeated Device device = 1; } message RunDiscoveryRequest { } message RunDiscoveryResponse { Device device = 1; uint32 flags = 2; } message Device { string name = 1; bytes address = 2; }
android/pandora/server/src/com/android/pandora/Gatt.kt +120 −100 Original line number Diff line number Diff line Loading @@ -20,18 +20,20 @@ import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothGattService.SERVICE_TYPE_PRIMARY import android.bluetooth.BluetoothManager import android.content.Context import android.content.Intent import android.content.IntentFilter import android.util.Log import com.google.protobuf.Empty import io.grpc.Status import io.grpc.stub.StreamObserver import java.util.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted Loading @@ -45,15 +47,15 @@ import pandora.GattProto.* class Gatt(private val context: Context) : GATTImplBase() { private val TAG = "PandoraGatt" private val mScope: CoroutineScope private val flow: Flow<Intent> private val mScope: CoroutineScope = CoroutineScope(Dispatchers.Default) private val mBluetoothManager = context.getSystemService(BluetoothManager::class.java)!! private val mBluetoothAdapter = mBluetoothManager.adapter init { mScope = CoroutineScope(Dispatchers.Default) private val serverManager by lazy { GattServerManager(mBluetoothManager, context, mScope) } private val flow: Flow<Intent> init { val intentFilter = IntentFilter() intentFilter.addAction(BluetoothDevice.ACTION_UUID) Loading @@ -61,10 +63,14 @@ class Gatt(private val context: Context) : GATTImplBase() { } fun deinit() { serverManager.server.close() mScope.cancel() } override fun exchangeMTU(request: ExchangeMTURequest, responseObserver: StreamObserver<ExchangeMTUResponse>) { override fun exchangeMTU( request: ExchangeMTURequest, responseObserver: StreamObserver<ExchangeMTUResponse> ) { grpcUnary<ExchangeMTUResponse>(mScope, responseObserver) { val mtu = request.mtu Log.i(TAG, "exchangeMTU MTU=$mtu") Loading @@ -88,24 +94,16 @@ class Gatt(private val context: Context) : GATTImplBase() { if (characteristic == null) { val descriptor: BluetoothGattDescriptor? = getDescriptorWithHandle(request.handle, gattInstance) checkNotNull(descriptor) { "Found no characteristic or descriptor with handle ${request.handle}" } val valueWrote = gattInstance.writeDescriptorBlocking( descriptor, request.value.toByteArray() ) WriteResponse.newBuilder() .setHandle(valueWrote.handle) .setStatus(valueWrote.status) .build() checkNotNull(descriptor) { "Found no characteristic or descriptor with handle ${request.handle}" } val valueWrote = gattInstance.writeDescriptorBlocking(descriptor, request.value.toByteArray()) WriteResponse.newBuilder().setHandle(valueWrote.handle).setStatus(valueWrote.status).build() } else { val valueWrote = gattInstance.writeCharacteristicBlocking( characteristic, request.value.toByteArray() ) WriteResponse.newBuilder() .setHandle(valueWrote.handle) .setStatus(valueWrote.status) .build() val valueWrote = gattInstance.writeCharacteristicBlocking(characteristic, request.value.toByteArray()) WriteResponse.newBuilder().setHandle(valueWrote.handle).setStatus(valueWrote.status).build() } } } Loading Loading @@ -164,7 +162,10 @@ class Gatt(private val context: Context) : GATTImplBase() { } } override fun clearCache(request: ClearCacheRequest, responseObserver: StreamObserver<ClearCacheResponse>) { override fun clearCache( request: ClearCacheRequest, responseObserver: StreamObserver<ClearCacheResponse> ) { grpcUnary<ClearCacheResponse>(mScope, responseObserver) { Log.i(TAG, "clearCache") val gattInstance = GattInstance.get(request.connection.cookie) Loading @@ -185,11 +186,7 @@ class Gatt(private val context: Context) : GATTImplBase() { checkNotNull(characteristic) { "Characteristic handle ${request.handle} not found." } val readValue = gattInstance.readCharacteristicBlocking(characteristic) ReadCharacteristicResponse.newBuilder() .setValue( AttValue.newBuilder() .setHandle(readValue.handle) .setValue(readValue.value) ) .setValue(AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)) .setStatus(readValue.status) .build() } Loading @@ -205,10 +202,7 @@ class Gatt(private val context: Context) : GATTImplBase() { tryDiscoverServices(gattInstance) val readValues = gattInstance.readCharacteristicUuidBlocking( UUID.fromString(request.uuid), request.startHandle, request.endHandle ) UUID.fromString(request.uuid), request.startHandle, request.endHandle) ReadCharacteristicsFromUuidResponse.newBuilder() .addAllCharacteristicsRead(generateReadValuesList(readValues)) .build() Loading @@ -227,16 +221,46 @@ class Gatt(private val context: Context) : GATTImplBase() { checkNotNull(descriptor) { "Descriptor handle ${request.handle} not found." } val readValue = gattInstance.readDescriptorBlocking(descriptor) ReadCharacteristicDescriptorResponse.newBuilder() .setValue( AttValue.newBuilder() .setHandle(readValue.handle) .setValue(readValue.value) ) .setValue(AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)) .setStatus(readValue.status) .build() } } override fun registerService( request: RegisterServiceRequest, responseObserver: StreamObserver<RegisterServiceResponse> ) { grpcUnary(mScope, responseObserver) { val service = BluetoothGattService(UUID.fromString(request.service.uuid), SERVICE_TYPE_PRIMARY) for (characteristic in request.service.characteristicsList) { service.addCharacteristic( BluetoothGattCharacteristic( UUID.fromString(characteristic.uuid), characteristic.properties, characteristic.permissions)) } val fullService = coroutineScope { val firstService = mScope.async { serverManager.newServiceFlow.first() } serverManager.server.addService(service) firstService.await() } RegisterServiceResponse.newBuilder() .setService( GattService.newBuilder() .setHandle(fullService.instanceId) .setType(fullService.type) .setUuid(fullService.uuid.toString()) .addAllIncludedServices(generateServicesList(service.includedServices, 1)) .addAllCharacteristics(generateCharacteristicsList(service.characteristics)) .build()) .build() } } /** * Discovers services, then returns characteristic with given handle. BluetoothGatt API is * package-private so we have to redefine it here. Loading Loading @@ -338,11 +362,7 @@ class Gatt(private val context: Context) : GATTImplBase() { for (readValue in readValuesList) { val readValueBuilder = ReadCharacteristicResponse.newBuilder() .setValue( AttValue.newBuilder() .setHandle(readValue.handle) .setValue(readValue.value) ) .setValue(AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)) .setStatus(readValue.status) newReadValuesList.add(readValueBuilder.build()) } Loading
android/pandora/server/src/com/android/pandora/GattServerManager.kt 0 → 100644 +53 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS 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.BluetoothGatt import android.bluetooth.BluetoothGattServer import android.bluetooth.BluetoothGattServerCallback import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothManager import android.content.Context import java.util.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map class GattServerManager(bluetoothManager: BluetoothManager, context: Context, globalScope: CoroutineScope) { val services = mutableMapOf<UUID, BluetoothGattService>() val server: BluetoothGattServer val newServiceFlow = MutableSharedFlow<BluetoothGattService>(extraBufferCapacity = 8) init { newServiceFlow.map { services[it.uuid] = it }.launchIn(globalScope) } init { val callback = object : BluetoothGattServerCallback() { override fun onServiceAdded(status: Int, service: BluetoothGattService) { check(status == BluetoothGatt.GATT_SUCCESS) check(newServiceFlow.tryEmit(service)) } } server = bluetoothManager.openGattServer(context, callback) } }
android/pandora/server/src/com/android/pandora/Host.kt +72 −0 Original line number Diff line number Diff line Loading @@ -42,8 +42,10 @@ import kotlin.Result.Companion.success import kotlin.coroutines.suspendCoroutine 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.sendBlocking import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow Loading Loading @@ -77,6 +79,7 @@ class Host(private val context: Context, private val server: Server) : HostImplB intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED) intentFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED) intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST) 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 Loading Loading @@ -405,4 +408,73 @@ class Host(private val context: Context, private val server: Server) : HostImplB } return bluetoothDevice } override fun runInquiry( request: RunInquiryRequest, responseObserver: StreamObserver<RunInquiryResponse> ) { Log.d(TAG, "runInquiry") grpcServerStream(scope, responseObserver) { launch { try { bluetoothAdapter.startDiscovery() awaitCancellation() } finally { bluetoothAdapter.cancelDiscovery() } } flow .filter { it.action == BluetoothDevice.ACTION_FOUND } .map { val device = it.getBluetoothDeviceExtra() Log.i(TAG, "Device found: $device") RunInquiryResponse.newBuilder() .addDevice( Device.newBuilder() .setName(device.name) .setAddress( ByteString.copyFrom(MacAddress.fromString(device.address).toByteArray()) ) ) .build() } } } override fun runDiscovery( request: RunDiscoveryRequest, responseObserver: StreamObserver<RunDiscoveryResponse> ) { Log.d(TAG, "runDiscovery") grpcServerStream(scope, responseObserver) { callbackFlow { val callback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { sendBlocking( RunDiscoveryResponse.newBuilder() .setDevice( Device.newBuilder() .setAddress( ByteString.copyFrom( MacAddress.fromString(result.device.address).toByteArray() ) ) .setName(result.device.name ?: "") ) .setFlags(result.scanRecord?.advertiseFlags ?: 0) .build() ) } override fun onScanFailed(errorCode: Int) { error("scan failed") } } bluetoothAdapter.bluetoothLeScanner.startScan(callback) awaitClose { bluetoothAdapter.bluetoothLeScanner.stopScan(callback) } } } } }