Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
Expand Down Expand Up @@ -48,6 +49,11 @@ class FlutterClassicBluetoothPlugin :
// (platform) thread; Bluetooth I/O runs on background threads.
private val mainHandler = Handler(Looper.getMainLooper())

// Detects when a remote Bluetooth device physically disconnects (e.g.
// printer turned off) and emits "disconnected" on the matching connection's
// state stream.
private var aclDisconnectReceiver: AclDisconnectReceiver? = null

// Event channels
private lateinit var adapterStateChannel: EventChannel
private lateinit var discoveryStateChannel: EventChannel
Expand Down Expand Up @@ -75,10 +81,17 @@ class FlutterClassicBluetoothPlugin :

bondStateChannel = EventChannel(messenger, BluetoothHelper.BOND_STATE_CHANNEL)
bondStateChannel.setStreamHandler(BondStateReceiver(context))

aclDisconnectReceiver = AclDisconnectReceiver(connections)
BluetoothHelper.registerExportedReceiver(
context, aclDisconnectReceiver!!, IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED)
)
}

override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
aclDisconnectReceiver?.let { context.unregisterReceiver(it) }
aclDisconnectReceiver = null
closeAll()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.flutter_classic_bluetooth.flutter_classic_bluetooth.receivers

import android.bluetooth.BluetoothDevice
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.SparseArray
import com.flutter_classic_bluetooth.flutter_classic_bluetooth.BluetoothConnectionWrapper

// Listens for system ACL disconnection events so we can immediately close the
// matching connection when a remote device goes away (e.g. printer turned off
// or moves out of range). The wrapper's close() emits "disconnected" on its
// state stream, so Dart is notified without extra plumbing here.
class AclDisconnectReceiver(
private val connections: SparseArray<BluetoothConnectionWrapper>
) : BroadcastReceiver() {

@Suppress("MissingPermission")
override fun onReceive(ctx: Context, intent: Intent) {
if (intent.action != BluetoothDevice.ACTION_ACL_DISCONNECTED) return

val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
} ?: return

val address = device.address
synchronized(connections) {
for (i in 0 until connections.size()) {
val conn = connections.valueAt(i)
if (conn.address == address) {
conn.close()
connections.removeAt(i)
break
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,28 @@ public class FlutterClassicBluetoothPlugin: NSObject, FlutterPlugin {
adapterChannel.setStreamHandler(instance.adapterStateHandler)

registrar.addMethodCallDelegate(instance, channel: channel)

EAAccessoryManager.shared().registerForLocalNotifications()
NotificationCenter.default.addObserver(
instance,
selector: #selector(accessoryDidDisconnect(_:)),
name: .EAAccessoryDidDisconnect,
object: nil
)
}

@objc private func accessoryDidDisconnect(_ notification: Notification) {
guard let accessory = notification.userInfo?[EAAccessoryKey] as? EAAccessory else {
return
}
for (id, wrapper) in connections {
if wrapper.accessoryConnectionID == accessory.connectionID {
wrapper.close()
connections.removeValue(forKey: id)

break
}
}
}

public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
Expand Down Expand Up @@ -195,7 +217,7 @@ public class FlutterClassicBluetoothPlugin: NSObject, FlutterPlugin {
let connId = nextConnectionId
nextConnectionId += 1

let wrapper = EASessionWrapper(id: connId, session: session, messenger: messenger)
let wrapper = EASessionWrapper(id: connId, session: session, accessoryConnectionID: accessory.connectionID, messenger: messenger)
wrapper.startReading()
connections[connId] = wrapper

Expand Down Expand Up @@ -249,13 +271,15 @@ public class FlutterClassicBluetoothPlugin: NSObject, FlutterPlugin {

class EASessionWrapper: NSObject, StreamDelegate {
let id: Int
let accessoryConnectionID: Int
private let session: EASession
private var messenger: FlutterBinaryMessenger
private var dataStreamHandler: IOSStreamHandler?
private var stateStreamHandler: IOSStreamHandler?

init(id: Int, session: EASession, messenger: FlutterBinaryMessenger) {
init(id: Int, session: EASession, accessoryConnectionID: Int, messenger: FlutterBinaryMessenger) {
self.id = id
self.accessoryConnectionID = accessoryConnectionID
self.session = session
self.messenger = messenger
super.init()
Expand Down