XYOracleNetwork/sdk-ble-android

View on GitHub
ble-android-library/src/main/kotlin/network/xyo/ble/generic/gatt/peripheral/XYBluetoothGatt.kt

Summary

Maintainability
D
2 days
Test Coverage
package network.xyo.ble.generic.gatt.peripheral

import android.bluetooth.*
import android.content.Context
import android.os.Handler
import java.util.UUID
import java.util.concurrent.Executors
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.*
import network.xyo.ble.generic.XYBluetoothBase
import network.xyo.ble.generic.gatt.peripheral.actions.XYBluetoothGattConnect
import network.xyo.ble.generic.gatt.peripheral.impl.*
import network.xyo.ble.generic.scanner.XYScanResult

// XYBluetoothGatt is a pure wrapper that does not add any functionality
// other than the ability to call the BluetoothGatt functions using coroutines

@Suppress("unused")
open class XYBluetoothGatt protected constructor(
    context: Context,
    protected var device: BluetoothDevice?,
    private var autoConnect: Boolean,
    private val callback: XYBluetoothGattCallback?,
    private val transport: Int?,
    private val phy: Int?,
    private val handler: Handler?
) : XYBluetoothBase(context) {

    open val bluetoothQueue = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
    protected var state = BluetoothGatt.STATE_DISCONNECTED

    protected val centralCallback = object : XYBluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
            onConnectionStateChange(newState)
            super.onConnectionStateChange(gatt, status, newState)
            when (newState) {
                BluetoothGatt.STATE_DISCONNECTED -> close()
            }
        }

        override fun onReadRemoteRssi(gatt: BluetoothGatt?, rssi: Int, status: Int) {
            super.onReadRemoteRssi(gatt, rssi, status)
            this@XYBluetoothGatt.rssi = rssi
            onDetect(null)
        }
    }

    private var _references = 0
    protected open var references: Int
        get() {
            log.info("References Get: $_references")
            return _references
        }
        set(value) {
            if (value <= 0) {
                _references = 0
                close()
            } else {
                _references = value
            }
            log.info("References Set: $_references")
        }

    // last time this device was accessed (connected to)
    protected var lastAccessTime: Long? = null

    // last time we heard a ad from this device
    protected var lastAdTime: Long? = null

    // last time we heard a ad from this device
    protected var enterTime: Long? = null

    var rssi: Int? = null

    protected var connection: XYBluetoothGattConnect? = null

    fun services(): List<BluetoothGattService> {
        return connection?.services ?: emptyList()
    }

    open val defaultTimeout = 15000L

    // force ble functions for this gatt to run in order
    @Suppress("BlockingMethodInNonBlockingContext")
    open fun <T> queueBleAsync(
        timeout: Long? = null,
        action: String = "Unknown",
        context: CoroutineContext = bluetoothQueue,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> XYBluetoothResult<T>
    ) = ble.async(context, start) {
        val timeoutToUse = timeout ?: defaultTimeout
        return@async runBlocking {
            lastAccessTime = now
            try {
                return@runBlocking withTimeout(timeoutToUse) {
                    lastAccessTime = now
                    return@withTimeout block()
                }
            } catch (ex: TimeoutCancellationException) {
                log.error(ex)
                close()
                return@runBlocking XYBluetoothResult<T>(XYBluetoothResultErrorCode.Timeout)
            }
        }
    }

    private var stayConnectedValue = false

    var stayConnected: Boolean
        get() {
            return stayConnectedValue
        }
        set(value) {
            synchronized(value) {
                if (stayConnectedValue != value) {
                    stayConnectedValue = value
                    if (!stayConnectedValue) {
                        references--
                    } else {
                        references++
                    }
                }
            }
        }

    val closed: Boolean
        get() = (connection == null)

    open fun onDetect(scanResult: XYScanResult?) {
    }

    open fun onConnectionStateChange(newState: Int) {
        state = newState
    }

    suspend fun requestMtu(mtu: Int, timeout: Long = 2900) = queueBleAsync(timeout, "requestMtu") {
        connection.let { connection ->
            if (connection != null) {
                return@queueBleAsync requestMtuImpl(connection, mtu, centralCallback)
            } else {
                return@queueBleAsync XYBluetoothResult(XYBluetoothResultErrorCode.Disconnected)
            }
        }
    }.await()

    suspend fun waitForNotification(characteristicToWaitFor: UUID): XYBluetoothResult<Any?> {
        return waitForNotificationImpl(characteristicToWaitFor, centralCallback)
    }

    private fun refreshGatt(): XYBluetoothResult<Boolean> {
        val gatt = connection?.gatt
        return if (gatt == null) {
            XYBluetoothResult(false, XYBluetoothResultErrorCode.NoGatt)
        } else {
            refreshGattImpl(gatt)
        }
    }

    suspend fun connect(timeout: Long = 60000) = queueBleAsync(timeout, "connect") {
        log.info("connect: start")

        this@XYBluetoothGatt.device?.let { device ->
            log.info("connect: has device")
            var connection = this@XYBluetoothGatt.connection
            lastAccessTime = now
            if (connection == null) {
                log.info("connect: creating connection object")
                connection = XYBluetoothGattConnect(device)
                connection.callback.addListener("Gatt", object : BluetoothGattCallback() {
                    override fun onConnectionStateChange(
                        gatt: BluetoothGatt?,
                        status: Int,
                        newState: Int
                    ) {
                        log.info("connect: onConnectionStateChange [$newState]")
                        super.onConnectionStateChange(gatt, status, newState)
                        this@XYBluetoothGatt.onConnectionStateChange(newState)
                    }
                })
            }
            val connectionResult = connection.start(context, transport)
            if (connectionResult.error != XYBluetoothResultErrorCode.None) {
                log.info("connect: error[${connectionResult.error}], closing...")
                close()
                return@queueBleAsync XYBluetoothResult(false, connectionResult.error)
            }
            connection.callback.addListener("XYBluetoothGatt", centralCallback)
            this@XYBluetoothGatt.connection = connection
            log.info("connect: success")
            return@queueBleAsync XYBluetoothResult(true)
        } ?: run {
            log.info("connect: nodevice")
            return@queueBleAsync XYBluetoothResult(false, XYBluetoothResultErrorCode.NoDevice)
        }
    }.await()

    fun disconnect() {
        ble.launch {
            disconnectAsync().await()
        }
    }

    suspend fun disconnectAsync(timeout: Long = 2900) = queueBleAsync(timeout, "disconnect") {
        connection?.disconnect()
        return@queueBleAsync XYBluetoothResult(true)
    }

    protected fun close() {
        ble.launch {
            closeAsync().await()
        }
    }

    private suspend fun closeAsync(timeout: Long = 2900) = queueBleAsync(timeout, "close") {
        connection?.close()
        connection = null
        return@queueBleAsync XYBluetoothResult(true)
    }

    // this can only be called after a successful discover
    protected fun findCharacteristicAsync(
        service: UUID,
        characteristic: UUID,
        timeout: Long = 1500
    ) = queueBleAsync(timeout, "findCharacteristic") {
        connection.let { connection ->
            if (connection != null) {
                return@queueBleAsync findCharacteristicImpl(connection, service, characteristic)
            } else {
                return@queueBleAsync XYBluetoothResult(XYBluetoothResultErrorCode.Disconnected)
            }
        }
    }

    protected suspend fun readCharacteristic(
        characteristicToRead: BluetoothGattCharacteristic,
        timeout: Long = 10000
    ) = queueBleAsync(timeout, "readCharacteristic") {
        connection.let { connection ->
            return@queueBleAsync if (connection != null) {
                readCharacteristicImpl(connection, characteristicToRead, centralCallback)
            } else {
                XYBluetoothResult(XYBluetoothResultErrorCode.Disconnected)
            }
        }
    }.await()

    protected suspend fun writeCharacteristicAsync(
        characteristicToWrite: BluetoothGattCharacteristic,
        timeout: Long = 10000,
        writeType: Int = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
    ) = queueBleAsync(timeout, "writeCharacteristic") {
        connection.let { connection ->
            return@queueBleAsync if (connection != null) {
                writeCharacteristicImpl(
                    connection,
                    characteristicToWrite,
                    writeType,
                    centralCallback
                )
            } else {
                XYBluetoothResult(XYBluetoothResultErrorCode.Disconnected)
            }
        }
    }

    protected suspend fun setCharacteristicNotifyAsync(
        characteristic: BluetoothGattCharacteristic,
        notify: Boolean,
        timeout: Long = 10000
    ) = queueBleAsync(timeout, "setCharacteristicNotify") {
        connection.let { connection ->
            return@queueBleAsync if (connection != null) {
                setCharacteristicNotifyImpl(connection, characteristic, notify)
            } else {
                XYBluetoothResult(XYBluetoothResultErrorCode.Disconnected)
            }
        }
    }

    protected suspend fun writeDescriptorAsync(
        descriptor: BluetoothGattDescriptor,
        timeout: Long = 1100
    ) = queueBleAsync(timeout, "writeDescriptor") {
        connection.let { connection ->
            return@queueBleAsync if (connection != null) {
                writeDescriptorImpl(connection, descriptor, centralCallback)
            } else {
                XYBluetoothResult(XYBluetoothResultErrorCode.Disconnected)
            }
        }
    }

    suspend fun <T> connection(closure: suspend () -> XYBluetoothResult<T>): XYBluetoothResult<T> {
        return connectionAsync(closure).await()
    }

    // make a safe session to interact with the device
    fun <T> connectionAsync(closure: suspend () -> XYBluetoothResult<T>) = ble.async {
        var value: T? = null
        val error: XYBluetoothResultErrorCode
        references++

        try {
            if (connect().error == XYBluetoothResultErrorCode.None) {
                val result = closure()
                error = result.error
                value = result.value
            } else {
                error = XYBluetoothResultErrorCode.FailedToConnect
            }
        } finally {
            references--
        }

        return@async XYBluetoothResult(value, error)
    }
}