diff --git a/app/build.gradle b/app/build.gradle index 9cd3df9..cb73a65 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,12 +2,12 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { - compileSdkVersion 31 + compileSdkVersion 32 defaultConfig { applicationId "com.welie.blessedexample" minSdkVersion 26 - targetSdkVersion 31 + targetSdkVersion 32 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -31,13 +31,13 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'androidx.appcompat:appcompat:1.4.1' - implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation 'androidx.appcompat:appcompat:1.5.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.jakewharton.timber:timber:5.0.1' - implementation "androidx.core:core-ktx:1.7.0" + implementation "androidx.core:core-ktx:1.8.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1" implementation project(':blessed') diff --git a/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt b/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt index c366822..eb6ae9d 100644 --- a/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt +++ b/app/src/main/java/com/welie/blessedexample/BluetoothHandler.kt @@ -297,7 +297,7 @@ internal class BluetoothHandler private constructor(context: Context) { central = BluetoothCentralManager(context) central.observeConnectionState { peripheral, state -> - Timber.i("Peripheral ${peripheral.name} has $state") + Timber.i("Peripheral '${peripheral.name}' is $state") when (state) { ConnectionState.CONNECTED -> handlePeripheral(peripheral) ConnectionState.DISCONNECTED -> scope.launch { diff --git a/blessed/build.gradle b/blessed/build.gradle index eac56b4..8f9ab13 100644 --- a/blessed/build.gradle +++ b/blessed/build.gradle @@ -3,11 +3,11 @@ apply plugin: 'kotlin-android' apply plugin: 'maven-publish' android { - compileSdkVersion 31 + compileSdkVersion 32 defaultConfig { minSdkVersion 26 - targetSdkVersion 31 + targetSdkVersion 32 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -30,9 +30,9 @@ android { } dependencies { - implementation "androidx.core:core-ktx:1.7.0" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1-native-mt" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1" + implementation "androidx.core:core-ktx:1.8.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1" implementation 'com.jakewharton.timber:timber:5.0.1' testImplementation 'junit:junit:4.13.2' diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt b/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt index c52d293..9065d7e 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothCentralManager.kt @@ -156,6 +156,10 @@ class BluetoothCentralManager(private val context: Context) { @JvmField val internalCallback: InternalCallback = object : InternalCallback { + override fun connecting(peripheral: BluetoothPeripheral) { + scope.launch { connectionStateCallback.invoke(peripheral, ConnectionState.CONNECTING)} + } + override fun connected(peripheral: BluetoothPeripheral) { connectionRetries.remove(peripheral.address) unconnectedPeripherals.remove(peripheral.address) @@ -195,6 +199,10 @@ class BluetoothCentralManager(private val context: Context) { } } + override fun disconnecting(peripheral: BluetoothPeripheral) { + scope.launch { connectionStateCallback.invoke(peripheral, ConnectionState.DISCONNECTING)} + } + override fun disconnected(peripheral: BluetoothPeripheral, status: HciStatus) { connectedPeripherals.remove(peripheral.address) unconnectedPeripherals.remove(peripheral.address) diff --git a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt index a4d9570..9cb7075 100644 --- a/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt +++ b/blessed/src/main/java/com/welie/blessed/BluetoothPeripheral.kt @@ -94,7 +94,7 @@ class BluetoothPeripheral internal constructor( */ private val bluetoothGattCallback: BluetoothGattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { - cancelConnectionTimer() + if (newState != BluetoothProfile.STATE_CONNECTING) cancelConnectionTimer() val previousState = state state = newState @@ -103,8 +103,14 @@ class BluetoothPeripheral internal constructor( when (newState) { BluetoothProfile.STATE_CONNECTED -> successfullyConnected() BluetoothProfile.STATE_DISCONNECTED -> successfullyDisconnected(previousState) - BluetoothProfile.STATE_DISCONNECTING -> Logger.d(TAG, "peripheral is disconnecting") - BluetoothProfile.STATE_CONNECTING -> Logger.d(TAG, "peripheral is connecting") + BluetoothProfile.STATE_DISCONNECTING -> { + Logger.d(TAG, "peripheral is disconnecting") + listener.disconnecting(this@BluetoothPeripheral) + } + BluetoothProfile.STATE_CONNECTING -> { + Logger.d(TAG, "peripheral is connecting") + listener.connecting(this@BluetoothPeripheral) + } else -> Logger.e(TAG, "unknown state received") } } else { @@ -142,9 +148,11 @@ class BluetoothPeripheral internal constructor( ) } + val value = currentWriteBytes + currentWriteBytes = ByteArray(0) + if (descriptor.uuid == CCC_DESCRIPTOR_UUID) { if (gattStatus == GattStatus.SUCCESS) { - val value = nonnullOf(descriptor.value) if (value.contentEquals(ENABLE_NOTIFICATION_VALUE) || value.contentEquals(ENABLE_INDICATION_VALUE) ) { @@ -155,7 +163,7 @@ class BluetoothPeripheral internal constructor( } callbackScope.launch { resultCallback.onNotificationStateUpdate(this@BluetoothPeripheral, parentCharacteristic, gattStatus) } } else { - callbackScope.launch { resultCallback.onDescriptorWrite(this@BluetoothPeripheral, currentWriteBytes, descriptor, gattStatus) } + callbackScope.launch { resultCallback.onDescriptorWrite(this@BluetoothPeripheral, value, descriptor, gattStatus) } } completedCommand() } @@ -440,7 +448,6 @@ class BluetoothPeripheral internal constructor( delay(DIRECT_CONNECTION_DELAY_IN_MS) Logger.d(TAG, "connect to '%s' (%s) using TRANSPORT_LE", name, address) registerBondingBroadcastReceivers() - state = BluetoothProfile.STATE_CONNECTING discoveryStarted = false bluetoothGatt = try { device.connectGatt(context, false, bluetoothGattCallback, BluetoothDevice.TRANSPORT_LE) @@ -449,6 +456,7 @@ class BluetoothPeripheral internal constructor( null } bluetoothGatt?.let { + bluetoothGattCallback.onConnectionStateChange(it, HciStatus.SUCCESS.value, BluetoothProfile.STATE_CONNECTING) connectTimestamp = SystemClock.elapsedRealtime() startConnectionTimer(this@BluetoothPeripheral) } @@ -469,15 +477,15 @@ class BluetoothPeripheral internal constructor( scope.launch { Logger.d(TAG, "autoConnect to '%s' (%s) using TRANSPORT_LE", name, address) registerBondingBroadcastReceivers() - state = BluetoothProfile.STATE_CONNECTING discoveryStarted = false bluetoothGatt = try { device.connectGatt(context, true, bluetoothGattCallback, BluetoothDevice.TRANSPORT_LE) } catch (e: SecurityException) { - Logger.d(TAG, "exception") + Logger.e(TAG, "connectGatt exception") null } bluetoothGatt?.let { + bluetoothGattCallback.onConnectionStateChange(it, HciStatus.SUCCESS.value, BluetoothProfile.STATE_CONNECTING) connectTimestamp = SystemClock.elapsedRealtime() } } @@ -506,11 +514,12 @@ class BluetoothPeripheral internal constructor( // Check if we have a Gatt object if (bluetoothGatt == null) { // No gatt object so no connection issued, do create bond immediately + registerBondingBroadcastReceivers() return device.createBond() } // Enqueue the bond command because a connection has been issued or we are already connected - val result = commandQueue.add(Runnable { + return enqueue { manuallyBonding = true if (!device.createBond()) { Logger.e(TAG, "bonding failed for %s", address) @@ -519,13 +528,7 @@ class BluetoothPeripheral internal constructor( Logger.d(TAG, "manually bonding %s", address) nrTries++ } - }) - if (result) { - nextCommand() - } else { - Logger.e(TAG, "could not enqueue bonding command") } - return result } /** @@ -557,7 +560,9 @@ class BluetoothPeripheral internal constructor( // Since we will not get a callback on onConnectionStateChange for this, we issue the disconnect ourselves scope.launch { delay(50) - bluetoothGattCallback.onConnectionStateChange(bluetoothGatt, HciStatus.SUCCESS.value, BluetoothProfile.STATE_DISCONNECTED) + bluetoothGatt?.let { + bluetoothGattCallback.onConnectionStateChange(bluetoothGatt, HciStatus.SUCCESS.value, BluetoothProfile.STATE_DISCONNECTED) + } } } else { // Cancel active connection and onConnectionStateChange will be called by Android @@ -573,10 +578,14 @@ class BluetoothPeripheral internal constructor( */ private fun disconnect() { if (state == BluetoothProfile.STATE_CONNECTED || state == BluetoothProfile.STATE_CONNECTING) { - state = BluetoothProfile.STATE_DISCONNECTING + bluetoothGatt?.let { + bluetoothGattCallback.onConnectionStateChange(it, HciStatus.SUCCESS.value, BluetoothProfile.STATE_DISCONNECTING) + } + scope.launch { if (state == BluetoothProfile.STATE_DISCONNECTING && bluetoothGatt != null) { bluetoothGatt?.disconnect() + Logger.i(TAG, "force disconnect '%s' (%s)", name, address) } } } else { @@ -585,7 +594,6 @@ class BluetoothPeripheral internal constructor( } fun disconnectWhenBluetoothOff() { - bluetoothGatt = null completeDisconnect(true, HciStatus.SUCCESS) } @@ -600,6 +608,10 @@ class BluetoothPeripheral internal constructor( commandQueue.clear() commandQueueBusy = false notifyingCharacteristics.clear() + currentMtu = DEFAULT_MTU + currentCommand = IDLE + manuallyBonding = false + discoveryStarted = false try { context.unregisterReceiver(bondStateReceiver) context.unregisterReceiver(pairingRequestBroadcastReceiver) @@ -1482,6 +1494,13 @@ class BluetoothPeripheral internal constructor( } interface InternalCallback { + /** + * Trying to connect to [BluetoothPeripheral] + * + * @param peripheral [BluetoothPeripheral] the peripheral. + */ + fun connecting(peripheral: BluetoothPeripheral) + /** * [BluetoothPeripheral] has successfully connected. * @@ -1496,6 +1515,13 @@ class BluetoothPeripheral internal constructor( */ fun connectFailed(peripheral: BluetoothPeripheral, status: HciStatus) + /** + * Trying to disconnect to [BluetoothPeripheral] + * + * @param peripheral [BluetoothPeripheral] the peripheral. + */ + fun disconnecting(peripheral: BluetoothPeripheral) + /** * [BluetoothPeripheral] has disconnected. * @@ -1515,7 +1541,9 @@ class BluetoothPeripheral internal constructor( disconnect() scope.launch { delay(50) - bluetoothGattCallback.onConnectionStateChange(bluetoothGatt, HciStatus.CONNECTION_FAILED_ESTABLISHMENT.value, BluetoothProfile.STATE_DISCONNECTED) + bluetoothGatt?.let { + bluetoothGattCallback.onConnectionStateChange(bluetoothGatt, HciStatus.CONNECTION_FAILED_ESTABLISHMENT.value, BluetoothProfile.STATE_DISCONNECTED) + } } } } diff --git a/build.gradle b/build.gradle index 1021293..2b64d4a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.1' + classpath 'com.android.tools.build:gradle:7.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong