blob: 587c1c4e8b64519cfdce57eeb691e3c5faa453af [file] [log] [blame]
/*
* 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.BluetoothDevice
import android.bluetooth.BluetoothDevice.TRANSPORT_LE
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.MacAddress
import android.util.Log
import com.google.protobuf.ByteString
import com.google.protobuf.Empty
import io.grpc.Status
import io.grpc.stub.StreamObserver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
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.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.runBlocking
import pandora.HostGrpc.HostImplBase
import pandora.HostProto.*
@kotlinx.coroutines.ExperimentalCoroutinesApi
class Host(private val context: Context, private val server: Server) : HostImplBase() {
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
init {
scope = CoroutineScope(Dispatchers.Default)
// 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)
// 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).shareIn(scope, SharingStarted.Eagerly)
}
fun deinit() {
scope.cancel()
}
override fun reset(request: Empty, responseObserver: StreamObserver<Empty>) {
grpcUnary<Empty>(scope, responseObserver) {
Log.i(TAG, "reset")
bluetoothAdapter.clearBluetooth()
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(2000L)
bluetoothAdapter.enable()
stateFlow.filter { it == BluetoothAdapter.STATE_ON }.first()
// The last expression is the return value.
Empty.getDefaultInstance()
}
.invokeOnCompletion {
Log.i(TAG, "Shutdown the gRPC Server")
server.shutdownNow()
}
}
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 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.getAction() == BluetoothDevice.ACTION_BOND_STATE_CHANGED }
.filter { it.getBluetoothDeviceExtra() == bluetoothDevice }
.map { it.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothAdapter.ERROR) }
.filter { it == BluetoothDevice.BOND_BONDED }
.first()
}
private suspend fun waitConnectionIntent(bluetoothDevice: BluetoothDevice) {
val acceptPairingJob = scope.launch { waitPairingRequestIntent(bluetoothDevice) }
waitBondIntent(bluetoothDevice)
if (acceptPairingJob.isActive) {
acceptPairingJob.cancel()
}
}
override fun waitConnection(
request: WaitConnectionRequest,
responseObserver: StreamObserver<WaitConnectionResponse>
) {
grpcUnary<WaitConnectionResponse>(scope, responseObserver) {
val bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter)
Log.i(TAG, "waitConnection: device=$bluetoothDevice")
if (!bluetoothAdapter.isEnabled) {
Log.e(TAG, "Bluetooth is not enabled, cannot waitConnection")
throw Status.UNKNOWN.asException()
}
waitConnectionIntent(bluetoothDevice)
WaitConnectionResponse.newBuilder()
.setConnection(
Connection.newBuilder()
.setCookie(ByteString.copyFromUtf8(bluetoothDevice.address))
.build()
)
.build()
}
}
override fun connect(request: ConnectRequest, responseObserver: StreamObserver<ConnectResponse>) {
grpcUnary<ConnectResponse>(scope, responseObserver) {
val bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter)
Log.i(TAG, "connect: address=$bluetoothDevice")
if (!bluetoothDevice.isConnected()) {
bluetoothDevice.createBond()
waitConnectionIntent(bluetoothDevice)
}
ConnectResponse.newBuilder()
.setConnection(
Connection.newBuilder()
.setCookie(ByteString.copyFromUtf8(bluetoothDevice.address))
.build()
)
.build()
}
}
override fun deletePairing(
request: DeletePairingRequest,
responseObserver: StreamObserver<DeletePairingResponse>
) {
grpcUnary<DeletePairingResponse>(scope, responseObserver) {
val bluetoothDevice = request.address.toBluetoothDevice(bluetoothAdapter)
Log.i(TAG, "DeletePairing: device=$bluetoothDevice")
if (bluetoothDevice.removeBond()) {
Log.i(TAG, "DeletePairing: device=$bluetoothDevice - wait BOND_NONE intent")
flow
.filter { it.getAction() == BluetoothDevice.ACTION_BOND_STATE_CHANGED }
.filter { it.getBluetoothDeviceExtra() == bluetoothDevice }
.filter {
it.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothAdapter.ERROR) ==
BluetoothDevice.BOND_NONE
}
.filter {
it.getIntExtra(BluetoothDevice.EXTRA_REASON, BluetoothAdapter.ERROR) ==
BluetoothDevice.BOND_SUCCESS
}
.first()
} else {
Log.i(TAG, "DeletePairing: device=$bluetoothDevice - Already unpaired")
}
DeletePairingResponse.getDefaultInstance()
}
}
override fun disconnect(
request: DisconnectRequest,
responseObserver: StreamObserver<DisconnectResponse>
) {
grpcUnary<DisconnectResponse>(scope, responseObserver) {
val bluetoothDevice = request.connection.toBluetoothDevice(bluetoothAdapter)
Log.i(TAG, "disconnect: device=$bluetoothDevice")
if (!bluetoothDevice.isConnected()) {
Log.e(TAG, "Device is not connected, cannot disconnect")
throw Status.UNKNOWN.asException()
}
val connectionStateChangedFlow =
flow
.filter { it.getAction() == BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED }
.filter { it.getBluetoothDeviceExtra() == bluetoothDevice }
.map { it.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, BluetoothAdapter.ERROR) }
bluetoothDevice.disconnect()
connectionStateChangedFlow.filter { it == BluetoothAdapter.STATE_DISCONNECTED }.first()
DisconnectResponse.getDefaultInstance()
}
}
override fun connectLE(
request: ConnectLERequest,
responseObserver: StreamObserver<ConnectLEResponse>
) {
grpcUnary<ConnectLEResponse>(scope, responseObserver) {
val ptsAddress = request.address.decodeToString()
Log.i(TAG, "connect: $ptsAddress")
val device = scanLeDevice(ptsAddress)
GattInstance(device!!, TRANSPORT_LE, context).waitForState(BluetoothProfile.STATE_CONNECTED)
ConnectLEResponse.newBuilder()
.setConnection(
Connection.newBuilder()
.setCookie(ByteString.copyFromUtf8(device.address))
.build()
)
.build()
}
}
override fun disconnectLE(request: DisconnectLERequest, responseObserver: StreamObserver<Empty>) {
grpcUnary<Empty>(scope, responseObserver) {
val ptsAddress = request.connection.cookie.toByteArray().decodeToString()
Log.i(TAG, "disconnect: $ptsAddress")
GattInstance.get(ptsAddress).disconnectInstance()
Empty.getDefaultInstance()
}
}
private fun scanLeDevice(ptsAddress: String): 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
if (deviceAddress == ptsAddress) {
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
}
}