diff --git a/CHANGELOG.md b/CHANGELOG.md index 61846c7..9ee05db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.0.2 +* Added RSSI value, alias name and device type added to BluetoothDevice +* Scan results und bonded devices are now filtered correctly to only return BL Classic devices +* Example app updated: Tap on a scan result to connect to it and send and receive messages. + ## 0.0.1 * Initial release: Scan for and connect to BL Classic devices. diff --git a/README.md b/README.md index 4417f52..433631f 100644 --- a/README.md +++ b/README.md @@ -50,19 +50,16 @@ In the **android/app/src/main/AndroidManifest.xml** add: ```xml - + + - + + + - + ``` #### With location access @@ -71,18 +68,16 @@ In the **android/app/src/main/AndroidManifest.xml** add: ```xml - + + + - + + - + ``` Then pass the `accessFineLocation` parameter when initializing the plugin: diff --git a/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/AdapterStateReceiver.kt b/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/AdapterStateReceiver.kt index 4ac8eb1..2b1f5be 100644 --- a/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/AdapterStateReceiver.kt +++ b/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/AdapterStateReceiver.kt @@ -6,8 +6,8 @@ import android.content.Context import android.content.Intent import io.flutter.plugin.common.EventChannel -class AdapterStateReceiver : EventChannel.StreamHandler{ - companion object{ +class AdapterStateReceiver : EventChannel.StreamHandler { + companion object { const val CHANNEL_NAME: String = "${BlueClassicHelper.NAMESPACE}/adapterState" } @@ -30,11 +30,13 @@ class AdapterStateReceiver : EventChannel.StreamHandler{ val adapterState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR) - adapterStateEventSink.let { it?.success( - BlueClassicHelper.adapterStateString( - adapterState + adapterStateEventSink.let { + it?.success( + BlueClassicHelper.adapterStateString( + adapterState + ) ) - ) } + } } } } \ No newline at end of file diff --git a/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/BlueClassicHelper.kt b/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/BlueClassicHelper.kt index 1424ddb..481c1ba 100644 --- a/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/BlueClassicHelper.kt +++ b/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/BlueClassicHelper.kt @@ -1,13 +1,16 @@ package dev.lenhart.flutter_blue_classic +import android.Manifest +import android.annotation.TargetApi import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import androidx.annotation.RequiresPermission class BlueClassicHelper { companion object { const val NAMESPACE: String = "blue_classic" const val METHOD_CHANNEL_NAME: String = "$NAMESPACE/methods" - const val ERROR_ADDRESS_INVALID : String= "addressInvalid" + const val ERROR_ADDRESS_INVALID: String = "addressInvalid" fun adapterStateString(state: Int): String { return when (state) { @@ -29,11 +32,30 @@ class BlueClassicHelper { } } - fun bluetoothDeviceToMap(device: BluetoothDevice): MutableMap { - val entry: MutableMap = HashMap() + fun deviceTypeString(type: Int): String { + return when (type) { + BluetoothDevice.DEVICE_TYPE_LE -> "le" + BluetoothDevice.DEVICE_TYPE_CLASSIC -> "classic" + BluetoothDevice.DEVICE_TYPE_DUAL -> "dual" + else -> "unknown" + } + } + + @TargetApi(34) + @RequiresPermission(value = Manifest.permission.BLUETOOTH_CONNECT) + fun bluetoothDeviceToMap( + device: BluetoothDevice, + rssi: Short? = null + ): MutableMap { + val entry: MutableMap = HashMap() entry["address"] = device.address - entry["name"] = device.name ?: "" + entry["name"] = device.name + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + entry["alias"] = device.alias + } entry["bondState"] = bondStateString(device.bondState) + entry["deviceType"] = deviceTypeString(device.type) + entry["rssi"] = rssi return entry } } diff --git a/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/FlutterBlueClassicPlugin.kt b/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/FlutterBlueClassicPlugin.kt index 5a8b9ee..40d1b4b 100644 --- a/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/FlutterBlueClassicPlugin.kt +++ b/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/FlutterBlueClassicPlugin.kt @@ -29,474 +29,473 @@ import io.flutter.plugin.common.MethodChannel.Result import java.util.concurrent.Executors /** FlutterBlueClassicPlugin */ -class FlutterBlueClassicPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { - - companion object { - const val TAG: String = "FlutterBlueClassic" - } - - /// The MethodChannel that will the communication between Flutter and native Android - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity - private lateinit var methodChannel : MethodChannel - - private var activityPluginBinding: ActivityPluginBinding? = null - private var binaryMessenger: BinaryMessenger? = null - - private lateinit var adapterStateChannel: EventChannel - private lateinit var adapterStateReceiver: AdapterStateReceiver - - private lateinit var scanResultChannel: EventChannel - private lateinit var scanResultReceiver: ScanResultReceiver - - private lateinit var discoveryStateChannel: EventChannel - private lateinit var discoveryStateReceiver: DiscoveryStateReceiver - - private lateinit var permissionManager: PermissionManager - private var bluetoothAdapter: BluetoothAdapter? = null - - private val connections = SparseArray(2) - private var lastConnectionId = 0 - - private var context: Application? = null - - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - methodChannel = - MethodChannel( - flutterPluginBinding.binaryMessenger, - BlueClassicHelper.METHOD_CHANNEL_NAME - ) - methodChannel.setMethodCallHandler(this) - binaryMessenger = flutterPluginBinding.binaryMessenger - - // Adapter State Stream - adapterStateReceiver = AdapterStateReceiver() - adapterStateChannel = - EventChannel(flutterPluginBinding.binaryMessenger, AdapterStateReceiver.CHANNEL_NAME) - adapterStateChannel.setStreamHandler(adapterStateReceiver) - val filterAdapter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) - flutterPluginBinding.applicationContext.registerReceiver( - adapterStateReceiver.mBluetoothAdapterStateReceiver, - filterAdapter - ) - - // Scan Result Stream - scanResultReceiver = ScanResultReceiver() - scanResultChannel = - EventChannel(flutterPluginBinding.binaryMessenger, ScanResultReceiver.CHANNEL_NAME) - scanResultChannel.setStreamHandler(scanResultReceiver) - val filterScanResults = IntentFilter(BluetoothDevice.ACTION_FOUND) - flutterPluginBinding.applicationContext.registerReceiver( - scanResultReceiver.scanResultReceiver, - filterScanResults - ) - - // Discovery State Stream - discoveryStateReceiver = DiscoveryStateReceiver() - discoveryStateChannel = - EventChannel(flutterPluginBinding.binaryMessenger, DiscoveryStateReceiver.CHANNEL_NAME) - discoveryStateChannel.setStreamHandler(discoveryStateReceiver) - val filterDiscoveryState = IntentFilter() - filterDiscoveryState.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED) - filterDiscoveryState.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) - flutterPluginBinding.applicationContext.registerReceiver( - discoveryStateReceiver.discoveryStateReceiver, - filterDiscoveryState - ) - - this.context = flutterPluginBinding.applicationContext as Application - - val bluetoothManager = - getSystemService(flutterPluginBinding.applicationContext, BluetoothManager::class.java) - bluetoothAdapter = bluetoothManager?.adapter - - } - - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activityPluginBinding = binding - permissionManager = PermissionManager(context!!.applicationContext, binding.activity) - binding.addRequestPermissionsResultListener(permissionManager) - } - - override fun onDetachedFromActivity() { - activityPluginBinding?.removeRequestPermissionsResultListener(permissionManager) - activityPluginBinding = null - permissionManager.setActivity(null) - } - - override fun onDetachedFromActivityForConfigChanges() { - onDetachedFromActivity() - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - onAttachedToActivity(binding) - } - - override fun onMethodCall(call: MethodCall, result: Result) { - when (call.method) { - "isSupported" -> result.success(checkBluetoothSupport()) - "isEnabled" -> result.success(isBluetoothEnabled()) - "turnOn" -> turnOn(result) - "getAdapterState" -> result.success(getAdapterState()) - "bondedDevices" -> getBondedDevices(result) - "startScan" -> startScan(result, call.argument("usesFineLocation") ?: false) - "stopScan" -> stopScan(result) - "isScanningNow" -> isScanningNow(result) - "bondDevice" -> bondDevice(result, call.argument("address") ?: "") - "connect" -> connect(result, call.argument("address") ?: "") - "write" -> { - val id = call.argument("id") - val bytes = call.argument("bytes") - if (id != null && bytes != null) { - write(result, id, bytes) - } else { - result.error("argumentError", "Not all required arguments were specified", null) - } - } +class FlutterBlueClassicPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { - else -> { - result.notImplemented() - } + companion object { + const val TAG: String = "FlutterBlueClassic" } - } - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - methodChannel.setMethodCallHandler(null) - adapterStateChannel.setStreamHandler(null) - binaryMessenger = null + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var methodChannel: MethodChannel - binding.applicationContext.unregisterReceiver(adapterStateReceiver.mBluetoothAdapterStateReceiver) - binding.applicationContext.unregisterReceiver(scanResultReceiver.scanResultReceiver) - binding.applicationContext.unregisterReceiver(discoveryStateReceiver.discoveryStateReceiver) - } + private var activityPluginBinding: ActivityPluginBinding? = null + private var binaryMessenger: BinaryMessenger? = null + private lateinit var adapterStateChannel: EventChannel + private lateinit var adapterStateReceiver: AdapterStateReceiver + private lateinit var scanResultChannel: EventChannel + private lateinit var scanResultReceiver: ScanResultReceiver + private lateinit var discoveryStateChannel: EventChannel + private lateinit var discoveryStateReceiver: DiscoveryStateReceiver - private fun checkBluetoothSupport(): Boolean { - return context?.packageManager?.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) ?: false - } + private lateinit var permissionManager: PermissionManager + private var bluetoothAdapter: BluetoothAdapter? = null - private fun isBluetoothEnabled(): Boolean { - return bluetoothAdapter?.isEnabled ?: false - } + private val connections = SparseArray(2) + private var lastConnectionId = 0 - private fun turnOn(result: Result) { - if (isBluetoothEnabled()) { - result.success(null) - return - } - val permissions = ArrayList() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - permissions.add(Manifest.permission.BLUETOOTH_CONNECT) - } - permissionManager.ensurePermissions(permissions.toTypedArray()) { success: Boolean, deniedPermissions: List? -> - run { - try { - if (success) { - val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) - startActivityForResult( - activityPluginBinding!!.activity, - enableBtIntent, - PermissionManager.REQUEST_ENABLE_BT, - null + private var context: Application? = null + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + methodChannel = + MethodChannel( + flutterPluginBinding.binaryMessenger, + BlueClassicHelper.METHOD_CHANNEL_NAME ) - result.success(null) - return@run - } - } catch (_: Exception) { - } - result.error( - PermissionManager.ERROR_PERMISSION_DENIED, - String.format( - "Required permission(s) %s denied", - deniedPermissions?.joinToString() ?: "" - ), null + methodChannel.setMethodCallHandler(this) + binaryMessenger = flutterPluginBinding.binaryMessenger + + // Adapter State Stream + adapterStateReceiver = AdapterStateReceiver() + adapterStateChannel = + EventChannel(flutterPluginBinding.binaryMessenger, AdapterStateReceiver.CHANNEL_NAME) + adapterStateChannel.setStreamHandler(adapterStateReceiver) + val filterAdapter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) + flutterPluginBinding.applicationContext.registerReceiver( + adapterStateReceiver.mBluetoothAdapterStateReceiver, + filterAdapter + ) + + // Scan Result Stream + scanResultReceiver = ScanResultReceiver() + scanResultChannel = + EventChannel(flutterPluginBinding.binaryMessenger, ScanResultReceiver.CHANNEL_NAME) + scanResultChannel.setStreamHandler(scanResultReceiver) + val filterScanResults = IntentFilter(BluetoothDevice.ACTION_FOUND) + flutterPluginBinding.applicationContext.registerReceiver( + scanResultReceiver.scanResultReceiver, + filterScanResults + ) + + // Discovery State Stream + discoveryStateReceiver = DiscoveryStateReceiver() + discoveryStateChannel = + EventChannel(flutterPluginBinding.binaryMessenger, DiscoveryStateReceiver.CHANNEL_NAME) + discoveryStateChannel.setStreamHandler(discoveryStateReceiver) + val filterDiscoveryState = IntentFilter() + filterDiscoveryState.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED) + filterDiscoveryState.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) + flutterPluginBinding.applicationContext.registerReceiver( + discoveryStateReceiver.discoveryStateReceiver, + filterDiscoveryState ) - } + this.context = flutterPluginBinding.applicationContext as Application + + val bluetoothManager = + getSystemService(flutterPluginBinding.applicationContext, BluetoothManager::class.java) + bluetoothAdapter = bluetoothManager?.adapter + } - } - - private fun getAdapterState(): String { - return bluetoothAdapter?.state?.let { BlueClassicHelper.adapterStateString(it) } - ?: "unavailable" - } - - @SuppressLint("MissingPermission") - private fun getBondedDevices(result: Result) { - val permissions = ArrayList() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - permissions.add(Manifest.permission.BLUETOOTH_CONNECT) + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activityPluginBinding = binding + permissionManager = PermissionManager(context!!.applicationContext, binding.activity) + binding.addRequestPermissionsResultListener(permissionManager) } - permissionManager.ensurePermissions(permissions.toTypedArray()) { success: Boolean, deniedPermissions: List? -> - run { - if (success) { - val devices: List>? = - bluetoothAdapter?.bondedDevices?.map { - BlueClassicHelper.bluetoothDeviceToMap(it) - } - result.success(devices) - } else { - result.error( - PermissionManager.ERROR_PERMISSION_DENIED, - String.format( - "Required permission(s) %s denied", - deniedPermissions?.joinToString() ?: "" - ), null - ) - } - } + override fun onDetachedFromActivity() { + activityPluginBinding?.removeRequestPermissionsResultListener(permissionManager) + activityPluginBinding = null + permissionManager.setActivity(null) } - } - - @SuppressLint("MissingPermission") - private fun startScan(result: Result, usesFineLocation: Boolean) { - val permissions = ArrayList() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - permissions.add(Manifest.permission.BLUETOOTH_SCAN) - permissions.add(Manifest.permission.BLUETOOTH_CONNECT) + + override fun onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity() } - if (usesFineLocation) { - permissions.add(Manifest.permission.ACCESS_FINE_LOCATION) + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + onAttachedToActivity(binding) } - permissionManager.ensurePermissions(permissions.toTypedArray()) { success: Boolean, deniedPermissions: List? -> - run { - if (success) { - val discoveryStartState = bluetoothAdapter?.startDiscovery() - result.success(discoveryStartState) - } else { - result.error( - PermissionManager.ERROR_PERMISSION_DENIED, - String.format( - "Required permission(s) %s denied", - deniedPermissions?.joinToString() ?: "" - ), null - ) + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "isSupported" -> result.success(checkBluetoothSupport()) + "isEnabled" -> result.success(isBluetoothEnabled()) + "turnOn" -> turnOn(result) + "getAdapterState" -> result.success(getAdapterState()) + "bondedDevices" -> getBondedDevices(result) + "startScan" -> startScan(result, call.argument("usesFineLocation") ?: false) + "stopScan" -> stopScan(result) + "isScanningNow" -> isScanningNow(result) + "bondDevice" -> bondDevice(result, call.argument("address") ?: "") + "connect" -> connect(result, call.argument("address") ?: "") + "write" -> { + val id = call.argument("id") + val bytes = call.argument("bytes") + if (id != null && bytes != null) { + write(result, id, bytes) + } else { + result.error("argumentError", "Not all required arguments were specified", null) + } + } + + else -> { + result.notImplemented() + } } - } } - } - @SuppressLint("MissingPermission") - private fun stopScan(result: Result) { - val permissions = ArrayList() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - permissions.add(Manifest.permission.BLUETOOTH_SCAN) + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + methodChannel.setMethodCallHandler(null) + adapterStateChannel.setStreamHandler(null) + binaryMessenger = null + + binding.applicationContext.unregisterReceiver(adapterStateReceiver.mBluetoothAdapterStateReceiver) + binding.applicationContext.unregisterReceiver(scanResultReceiver.scanResultReceiver) + binding.applicationContext.unregisterReceiver(discoveryStateReceiver.discoveryStateReceiver) } - permissionManager.ensurePermissions(permissions.toTypedArray()) { success: Boolean, deniedPermissions: List? -> - run { - if (success) { - result.success(bluetoothAdapter?.cancelDiscovery()) - } else { - result.error( - PermissionManager.ERROR_PERMISSION_DENIED, - String.format( - "Required permission(s) %s denied", - deniedPermissions?.joinToString() ?: "" - ), null - ) - } - } + + private fun checkBluetoothSupport(): Boolean { + return context?.packageManager?.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) ?: false } - } - @SuppressLint("MissingPermission") - private fun isScanningNow(result: Result) { - val permissions = ArrayList() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - permissions.add(Manifest.permission.BLUETOOTH_SCAN) + private fun isBluetoothEnabled(): Boolean { + return bluetoothAdapter?.isEnabled ?: false } - permissionManager.ensurePermissions(permissions.toTypedArray()) { success: Boolean, deniedPermissions: List? -> - run { - if (success) { - result.success(bluetoothAdapter?.isDiscovering ?: false) - } else { - result.error( - PermissionManager.ERROR_PERMISSION_DENIED, - String.format( - "Required permission(s) %s denied", - deniedPermissions?.joinToString() ?: "" - ), null - ) + private fun turnOn(result: Result) { + if (isBluetoothEnabled()) { + result.success(null) + return + } + val permissions = ArrayList() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions.add(Manifest.permission.BLUETOOTH_CONNECT) + } + permissionManager.ensurePermissions(permissions.toTypedArray()) { success: Boolean, deniedPermissions: List? -> + run { + try { + if (success) { + val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) + startActivityForResult( + activityPluginBinding!!.activity, + enableBtIntent, + PermissionManager.REQUEST_ENABLE_BT, + null + ) + result.success(null) + return@run + } + } catch (_: Exception) { + } + result.error( + PermissionManager.ERROR_PERMISSION_DENIED, + String.format( + "Required permission(s) %s denied", + deniedPermissions?.joinToString() ?: "" + ), null + ) + + } } - } - } - } - - @SuppressLint("MissingPermission") - private fun bondDevice(result: Result, address: String) { - if (!BluetoothAdapter.checkBluetoothAddress(address)) { - result.error( - BlueClassicHelper.ERROR_ADDRESS_INVALID, - "The bluetooth address $address is invalid", - null - ) - return } - val permissions = ArrayList() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - permissions.add(Manifest.permission.BLUETOOTH_CONNECT) + private fun getAdapterState(): String { + return bluetoothAdapter?.state?.let { BlueClassicHelper.adapterStateString(it) } + ?: "unavailable" } - permissionManager.ensurePermissions(permissions.toTypedArray()) { success: Boolean, deniedPermissions: List? -> - run { - if (success) { - val device = bluetoothAdapter?.getRemoteDevice(address) - result.success(device?.createBond() ?: false) - } else { - result.error( - PermissionManager.ERROR_PERMISSION_DENIED, - String.format( - "Required permission(s) %s denied", - deniedPermissions?.joinToString() ?: "" - ), null - ) + @SuppressLint("MissingPermission") + private fun getBondedDevices(result: Result) { + val permissions = ArrayList() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions.add(Manifest.permission.BLUETOOTH_CONNECT) + } + permissionManager.ensurePermissions(permissions.toTypedArray()) { success: Boolean, deniedPermissions: List? -> + run { + if (success) { + val devices: List>? = + bluetoothAdapter?.bondedDevices?.filter { it.type != BluetoothDevice.DEVICE_TYPE_LE } + ?.map { + BlueClassicHelper.bluetoothDeviceToMap(it) + } + + result.success(devices) + } else { + result.error( + PermissionManager.ERROR_PERMISSION_DENIED, + String.format( + "Required permission(s) %s denied", + deniedPermissions?.joinToString() ?: "" + ), null + ) + } + } } - } } - } + @SuppressLint("MissingPermission") + private fun startScan(result: Result, usesFineLocation: Boolean) { + val permissions = ArrayList() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions.add(Manifest.permission.BLUETOOTH_SCAN) + permissions.add(Manifest.permission.BLUETOOTH_CONNECT) + } + if (usesFineLocation) { + permissions.add(Manifest.permission.ACCESS_FINE_LOCATION) + } - @SuppressLint("MissingPermission") - private fun connect(result: Result, address: String) { - var permissionSuccess = true - val permissions = ArrayList() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - permissions.add(Manifest.permission.BLUETOOTH_CONNECT) - permissions.add(Manifest.permission.BLUETOOTH_SCAN) + permissionManager.ensurePermissions(permissions.toTypedArray()) { success: Boolean, deniedPermissions: List? -> + run { + if (success) { + val discoveryStartState = bluetoothAdapter?.startDiscovery() + result.success(discoveryStartState) + } else { + result.error( + PermissionManager.ERROR_PERMISSION_DENIED, + String.format( + "Required permission(s) %s denied", + deniedPermissions?.joinToString() ?: "" + ), null + ) + } + } + } } - permissionManager.ensurePermissions(permissions.toTypedArray()){ success: Boolean, deniedPermissions: List? -> - if(!success){ - result.error( - PermissionManager.ERROR_PERMISSION_DENIED, - String.format( - "Required permission(s) %s denied", - deniedPermissions?.joinToString() ?: "" - ), null - ) - } - permissionSuccess = success + @SuppressLint("MissingPermission") + private fun stopScan(result: Result) { + val permissions = ArrayList() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions.add(Manifest.permission.BLUETOOTH_SCAN) + } + + permissionManager.ensurePermissions(permissions.toTypedArray()) { success: Boolean, deniedPermissions: List? -> + run { + if (success) { + result.success(bluetoothAdapter?.cancelDiscovery()) + } else { + result.error( + PermissionManager.ERROR_PERMISSION_DENIED, + String.format( + "Required permission(s) %s denied", + deniedPermissions?.joinToString() ?: "" + ), null + ) + } + } + } } - if(!permissionSuccess) return + @SuppressLint("MissingPermission") + private fun isScanningNow(result: Result) { + val permissions = ArrayList() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions.add(Manifest.permission.BLUETOOTH_SCAN) + } - if (!BluetoothAdapter.checkBluetoothAddress(address)) { - result.error( - BlueClassicHelper.ERROR_ADDRESS_INVALID, - "The bluetooth address $address is invalid", - null - ) - return + permissionManager.ensurePermissions(permissions.toTypedArray()) { success: Boolean, deniedPermissions: List? -> + run { + if (success) { + result.success(bluetoothAdapter?.isDiscovering ?: false) + } else { + result.error( + PermissionManager.ERROR_PERMISSION_DENIED, + String.format( + "Required permission(s) %s denied", + deniedPermissions?.joinToString() ?: "" + ), null + ) + } + } + } } - val id = ++lastConnectionId - val connection = BluetoothConnectionWrapper(id, bluetoothAdapter!!) - connections.put(id, connection) - Log.d( - TAG, - "Connecting to $address (id: $id)" - ) - - Executors.newSingleThreadExecutor().execute { - Handler(Looper.getMainLooper()).post { - try { - connection.connect(address) - activityPluginBinding!!.activity.runOnUiThread { - result.success(id) - } - } catch (ex: java.lang.Exception) { - activityPluginBinding!!.activity.runOnUiThread { + @SuppressLint("MissingPermission") + private fun bondDevice(result: Result, address: String) { + if (!BluetoothAdapter.checkBluetoothAddress(address)) { result.error( - "connect_error", - ex.message, null + BlueClassicHelper.ERROR_ADDRESS_INVALID, + "The bluetooth address $address is invalid", + null ) - } - connections.remove(id) + return } - } - } - } - - private fun write(result: Result, id: Int, bytes: ByteArray) { - val connection: BluetoothConnection = connections[id] - Executors.newSingleThreadExecutor().execute { - Handler(Looper.getMainLooper()).post { - connection.write(bytes) - activityPluginBinding!!.activity.runOnUiThread { - result.success(null) + + val permissions = ArrayList() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions.add(Manifest.permission.BLUETOOTH_CONNECT) + } + + permissionManager.ensurePermissions(permissions.toTypedArray()) { success: Boolean, deniedPermissions: List? -> + run { + if (success) { + val device = bluetoothAdapter?.getRemoteDevice(address) + result.success(device?.createBond() ?: false) + } else { + result.error( + PermissionManager.ERROR_PERMISSION_DENIED, + String.format( + "Required permission(s) %s denied", + deniedPermissions?.joinToString() ?: "" + ), null + ) + } + } } - } } - } - // ------ INNER CLASS ------ + @SuppressLint("MissingPermission") + private fun connect(result: Result, address: String) { + var permissionSuccess = true + val permissions = ArrayList() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions.add(Manifest.permission.BLUETOOTH_CONNECT) + permissions.add(Manifest.permission.BLUETOOTH_SCAN) + } - private inner class BluetoothConnectionWrapper(private val id: Int, adapter: BluetoothAdapter) : - BluetoothConnection(adapter), EventChannel.StreamHandler { - private var readSink: EventSink? = null - private var readChannel: EventChannel = EventChannel( - binaryMessenger, - BlueClassicHelper.NAMESPACE + "/connection/" + id - ) + permissionManager.ensurePermissions(permissions.toTypedArray()) { success: Boolean, deniedPermissions: List? -> + if (!success) { + result.error( + PermissionManager.ERROR_PERMISSION_DENIED, + String.format( + "Required permission(s) %s denied", + deniedPermissions?.joinToString() ?: "" + ), null + ) + } + permissionSuccess = success + } - init { - readChannel.setStreamHandler(this) - } + if (!permissionSuccess) return - override fun onRead(data: ByteArray?) { - activityPluginBinding?.activity?.runOnUiThread { - if (readSink != null) { - readSink?.success(data) + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + result.error( + BlueClassicHelper.ERROR_ADDRESS_INVALID, + "The bluetooth address $address is invalid", + null + ) + return } - } - } - override fun onDisconnected(byRemote: Boolean) { - activityPluginBinding?.activity?.runOnUiThread { - if (byRemote) { - Log.d( + val id = ++lastConnectionId + val connection = BluetoothConnectionWrapper(id, bluetoothAdapter!!) + connections.put(id, connection) + Log.d( TAG, - "onDisconnected by remote (id: $id)" - ) - if (readSink != null) { - readSink?.endOfStream() - readSink = null - } - } else { - Log.d( - TAG, - "onDisconnected by local (id: $id)" - ) + "Connecting to $address (id: $id)" + ) + + Executors.newSingleThreadExecutor().execute { + Handler(Looper.getMainLooper()).post { + try { + connection.connect(address) + activityPluginBinding!!.activity.runOnUiThread { + result.success(id) + } + } catch (ex: java.lang.Exception) { + activityPluginBinding!!.activity.runOnUiThread { + result.error( + "connect_error", + ex.message, null + ) + } + connections.remove(id) + } + } } - } } - override fun onListen(obj: Any?, eventSink: EventSink) { - readSink = eventSink + private fun write(result: Result, id: Int, bytes: ByteArray) { + val connection: BluetoothConnection = connections[id] + Executors.newSingleThreadExecutor().execute { + Handler(Looper.getMainLooper()).post { + connection.write(bytes) + activityPluginBinding!!.activity.runOnUiThread { + result.success(null) + } + } + } + } - override fun onCancel(obj: Any?) { - this.disconnect() + // ------ INNER CLASS ------ - Executors.newSingleThreadExecutor().execute { - Handler(Looper.getMainLooper()).post { - readChannel.setStreamHandler(null) - connections.remove(id) - Log.d( - TAG, - "Disconnected (id: $id)" - ) + private inner class BluetoothConnectionWrapper(private val id: Int, adapter: BluetoothAdapter) : + BluetoothConnection(adapter), EventChannel.StreamHandler { + private var readSink: EventSink? = null + private var readChannel: EventChannel = EventChannel( + binaryMessenger, + BlueClassicHelper.NAMESPACE + "/connection/" + id + ) + + init { + readChannel.setStreamHandler(this) + } + + override fun onRead(data: ByteArray?) { + activityPluginBinding?.activity?.runOnUiThread { + if (readSink != null) { + readSink?.success(data) + } + } + } + + override fun onDisconnected(byRemote: Boolean) { + activityPluginBinding?.activity?.runOnUiThread { + if (byRemote) { + Log.d( + TAG, + "onDisconnected by remote (id: $id)" + ) + if (readSink != null) { + readSink?.endOfStream() + readSink = null + } + } else { + Log.d( + TAG, + "onDisconnected by local (id: $id)" + ) + } + } + } + + override fun onListen(obj: Any?, eventSink: EventSink) { + readSink = eventSink + } + + override fun onCancel(obj: Any?) { + this.disconnect() + + Executors.newSingleThreadExecutor().execute { + Handler(Looper.getMainLooper()).post { + readChannel.setStreamHandler(null) + connections.remove(id) + Log.d( + TAG, + "Disconnected (id: $id)" + ) + } + } } - } } - } } diff --git a/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/PermissionManager.kt b/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/PermissionManager.kt index a114f24..4301794 100644 --- a/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/PermissionManager.kt +++ b/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/PermissionManager.kt @@ -11,10 +11,10 @@ import io.flutter.plugin.common.PluginRegistry class PermissionManager(private var context: Context, private var activity: Activity?) : PluginRegistry.RequestPermissionsResultListener { - companion object{ - const val ERROR_PERMISSION_DENIED = "permissionDenied" - const val REQUEST_ENABLE_BT = 1337 - } + companion object { + const val ERROR_PERMISSION_DENIED = "permissionDenied" + const val REQUEST_ENABLE_BT = 1337 + } private var lastPermissionRequestCode = 1 private val callbackForRequestCode = HashMap) -> Unit)>() @@ -32,8 +32,10 @@ class PermissionManager(private var context: Context, private var activity: Acti ): Boolean { if (requestCode < lastPermissionRequestCode) { - val success = grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED } - val deniedPermissions = permissions.zip(grantResults.asIterable()).filter { it.second == PackageManager.PERMISSION_DENIED }.map { it.first } + val success = + grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED } + val deniedPermissions = permissions.zip(grantResults.asIterable()) + .filter { it.second == PackageManager.PERMISSION_DENIED }.map { it.first } val callback = callbackForRequestCode.remove(requestCode) callback?.invoke(success, deniedPermissions) @@ -42,7 +44,10 @@ class PermissionManager(private var context: Context, private var activity: Acti return false } - fun ensurePermissions(permissions: Array, callback: ((Boolean, List?) -> Unit)){ + fun ensurePermissions( + permissions: Array, + callback: ((Boolean, List?) -> Unit) + ) { val requestCode: Int = lastPermissionRequestCode lastPermissionRequestCode++ callbackForRequestCode[requestCode] = callback @@ -55,7 +60,7 @@ class PermissionManager(private var context: Context, private var activity: Acti } } - if(permissionsNeeded.isEmpty()){ + if (permissionsNeeded.isEmpty()) { callback.invoke(true, null) return } @@ -63,14 +68,15 @@ class PermissionManager(private var context: Context, private var activity: Acti requestPermissions(requestCode, permissionsNeeded.toTypedArray()) } - private fun requestPermissions(requestCode: Int, permissions: Array){ + private fun requestPermissions(requestCode: Int, permissions: Array) { pendingRequestCount = permissions.size activity?.let { ActivityCompat.requestPermissions( it, permissions, - requestCode) + requestCode + ) } } diff --git a/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/ScanResultReceiver.kt b/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/ScanResultReceiver.kt index 56ed513..592338c 100644 --- a/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/ScanResultReceiver.kt +++ b/android/src/main/kotlin/dev/lenhart/flutter_blue_classic/ScanResultReceiver.kt @@ -1,5 +1,6 @@ package dev.lenhart.flutter_blue_classic +import android.annotation.SuppressLint import android.bluetooth.BluetoothDevice import android.content.BroadcastReceiver import android.content.Context @@ -7,40 +8,46 @@ import android.content.Intent import android.os.Build import io.flutter.plugin.common.EventChannel -class ScanResultReceiver : EventChannel.StreamHandler{ +class ScanResultReceiver : EventChannel.StreamHandler { - companion object{ - const val CHANNEL_NAME: String = "${BlueClassicHelper.NAMESPACE}/scanResults" - } + companion object { + const val CHANNEL_NAME: String = "${BlueClassicHelper.NAMESPACE}/scanResults" + } - private var adapterStateEventSink: EventChannel.EventSink? = null + private var adapterStateEventSink: EventChannel.EventSink? = null - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - adapterStateEventSink = events - } + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + adapterStateEventSink = events + } - override fun onCancel(arguments: Any?) { - adapterStateEventSink = null - } + override fun onCancel(arguments: Any?) { + adapterStateEventSink = null + } - val scanResultReceiver: BroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val device: BluetoothDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) + val scanResultReceiver: BroadcastReceiver = object : BroadcastReceiver() { + @SuppressLint("MissingPermission") + override fun onReceive(context: Context, intent: Intent) { + val device: BluetoothDevice? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra( + BluetoothDevice.EXTRA_DEVICE, + BluetoothDevice::class.java + ) } else { intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) } - if (device != null) { - adapterStateEventSink.let { it?.success( + val rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE) + + if (device != null && device.type != BluetoothDevice.DEVICE_TYPE_LE) { + adapterStateEventSink.let { + it?.success( BlueClassicHelper.bluetoothDeviceToMap( - device + device, rssi ) - ) } - + ) } - - } } + } } \ No newline at end of file diff --git a/example/lib/device_screen.dart b/example/lib/device_screen.dart new file mode 100644 index 0000000..d76e97d --- /dev/null +++ b/example/lib/device_screen.dart @@ -0,0 +1,69 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_blue_classic/flutter_blue_classic.dart'; + +class DeviceScreen extends StatefulWidget { + const DeviceScreen({super.key, required this.connection}); + + final BluetoothConnection connection; + + @override + State createState() => _DeviceScreenState(); +} + +class _DeviceScreenState extends State { + StreamSubscription? _readSubscription; + final List _receivedInput = []; + + @override + void initState() { + _readSubscription = widget.connection.input?.listen((event) { + if (mounted) { + setState(() => _receivedInput.add(utf8.decode(event))); + } + }); + super.initState(); + } + + @override + void dispose() { + widget.connection.dispose(); + _readSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Connection to ${widget.connection.address}"), + ), + body: ListView( + children: [ + ElevatedButton( + onPressed: () { + try { + widget.connection.writeString("hello world"); + } catch (e) { + if (kDebugMode) print(e); + ScaffoldMessenger.maybeOf(context)?.showSnackBar(SnackBar( + content: Text( + "Error sending to device. Device is ${widget.connection.isConnected ? "connected" : "not connected"}"))); + } + }, + child: const Text("Send hello world to remote device")), + const Divider(), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text("Received data", + style: Theme.of(context).textTheme.titleLarge), + ), + for (String input in _receivedInput) Text(input), + ], + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index c98450b..4c7731d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,19 +3,29 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blue_classic/flutter_blue_classic.dart'; +import 'package:flutter_blue_classic_example/device_screen.dart'; void main() { runApp(const MyApp()); } -class MyApp extends StatefulWidget { +class MyApp extends StatelessWidget { const MyApp({super.key}); @override - State createState() => _MyAppState(); + Widget build(BuildContext context) { + return const MaterialApp(home: MainScreen()); + } +} + +class MainScreen extends StatefulWidget { + const MainScreen({super.key}); + + @override + State createState() => _MainScreenState(); } -class _MyAppState extends State { +class _MainScreenState extends State { final _flutterBlueClassicPlugin = FlutterBlueClassic(); BluetoothAdapterState _adapterState = BluetoothAdapterState.unknown; @@ -38,13 +48,16 @@ class _MyAppState extends State { try { adapterState = await _flutterBlueClassicPlugin.adapterStateNow; - _adapterStateSubscription = _flutterBlueClassicPlugin.adapterState.listen((current) { + _adapterStateSubscription = + _flutterBlueClassicPlugin.adapterState.listen((current) { if (mounted) setState(() => _adapterState = current); }); - _scanSubscription = _flutterBlueClassicPlugin.scanResults.listen((device) { + _scanSubscription = + _flutterBlueClassicPlugin.scanResults.listen((device) { if (mounted) setState(() => _scanResults.add(device)); }); - _scanningStateSubscription = _flutterBlueClassicPlugin.isScanning.listen((isScanning) { + _scanningStateSubscription = + _flutterBlueClassicPlugin.isScanning.listen((isScanning) { if (mounted) setState(() => _isScanning = isScanning); }); } catch (e) { @@ -70,41 +83,64 @@ class _MyAppState extends State { Widget build(BuildContext context) { List scanResults = _scanResults.toList(); - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('FlutterBluePlus example app'), - ), - body: ListView( - children: [ - ListTile( - title: const Text("Bluetooth Adapter state"), - subtitle: const Text("Tap to enable"), - trailing: Text(_adapterState.name), - leading: const Icon(Icons.settings_bluetooth), - onTap: () => _flutterBlueClassicPlugin.turnOn(), - ), - const Divider(), - if (scanResults.isEmpty) - const Center(child: Text("No devices found yet")) - else - for (var result in scanResults) - ListTile( - title: Text("${result.name ?? "???"} (${result.address})"), - ) - ], - ), - floatingActionButton: FloatingActionButton.extended( - onPressed: () { - if (_isScanning) { - _flutterBlueClassicPlugin.stopScan(); - } else { - _flutterBlueClassicPlugin.startScan(); - } - }, - label: Text(_isScanning ? "Scanning..." : "Start device scan"), - icon: Icon(_isScanning ? Icons.bluetooth_searching : Icons.bluetooth), - ), + return Scaffold( + appBar: AppBar( + title: const Text('FlutterBluePlus example app'), + ), + body: ListView( + children: [ + ListTile( + title: const Text("Bluetooth Adapter state"), + subtitle: const Text("Tap to enable"), + trailing: Text(_adapterState.name), + leading: const Icon(Icons.settings_bluetooth), + onTap: () => _flutterBlueClassicPlugin.turnOn(), + ), + const Divider(), + if (scanResults.isEmpty) + const Center(child: Text("No devices found yet")) + else + for (var result in scanResults) + ListTile( + title: Text("${result.name ?? "???"} (${result.address})"), + subtitle: Text( + "Bondstate: ${result.bondState.name}, Device type: ${result.type.name}"), + trailing: Text("${result.rssi} dBm"), + onTap: () async { + BluetoothConnection? connection; + try { + connection = + await _flutterBlueClassicPlugin.connect(result.address); + if (!this.context.mounted) return; + if (connection != null && connection.isConnected) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + DeviceScreen(connection: connection!))); + } + } catch (e) { + if (kDebugMode) print(e); + connection?.dispose(); + ScaffoldMessenger.maybeOf(context)?.showSnackBar( + const SnackBar( + content: Text("Error connecting to device"))); + } + }, + ) + ], + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () { + if (_isScanning) { + _flutterBlueClassicPlugin.stopScan(); + } else { + _scanResults.clear(); + _flutterBlueClassicPlugin.startScan(); + } + }, + label: Text(_isScanning ? "Scanning..." : "Start device scan"), + icon: Icon(_isScanning ? Icons.bluetooth_searching : Icons.bluetooth), ), ); } diff --git a/lib/src/flutter_blue_classic.dart b/lib/src/flutter_blue_classic.dart index 9790300..3bdd3b6 100644 --- a/lib/src/flutter_blue_classic.dart +++ b/lib/src/flutter_blue_classic.dart @@ -5,7 +5,7 @@ import 'model/bluetooth_device.dart'; class FlutterBlueClassic { final _instance = FlutterBlueClassicPlatform.instance; - /// Indicates whether your app is using the fine location feature on android. For iOS apps this can be ignored. + /// Indicates whether your app is using the fine location feature. /// /// Note that you have to either define the ACCESS_FINE_LOCATION permission and set this [_usesFineLocation] to true or the /// usesPermissionFlags="neverForLocation" on the relevant manifest tag and set this to false @@ -20,13 +20,13 @@ class FlutterBlueClassic { Future get isEnabled => _instance.isEnabled(); /// Returns the current adapter state - Future get adapterStateNow => _instance.adapterStateNow(); + Future get adapterStateNow => + _instance.adapterStateNow(); /// Returns an event stream for all adapter state changes Stream get adapterState => _instance.adapterState(); - /// On Android this returns the list of bonded devices. - /// On iOS/macOS this will always return null + /// Returns the list of bonded devices. Future?> get bondedDevices => _instance.bondedDevices(); /// This will attempt to start scanning for Bluetooth devices. @@ -44,21 +44,21 @@ class FlutterBlueClassic { /// Returns an event stream for every bluetooth device found during scan Stream get scanResults => _instance.scanResults(); - /// On Android this will attempt to enable Bluetooth. On iOS this is a no-op. + /// Requests to turns the bluetooth adapter on. void turnOn() => _instance.turnOn(); - /// On Android this creates a bond to the device with the given address. - /// On iOS/macOS this is a no-op and will always return false + /// Tries to create a bond to the device with the given address. /// /// Returns whether the bond was successfully created. Future bondDevice(String address) => _instance.bondDevice(address); - /// Creates a connection to the device with the given address. - Future connect(String address) => _instance.connect(address); + /// Tries to create a connection to the device with the given address. + Future connect(String address) => + _instance.connect(address); } /// State of the Bluetooth adapter -enum BluetoothAdapterState { unknown, unavailable, unauthorized, turningOn, on, turningOff, off } +enum BluetoothAdapterState { unknown, turningOn, on, turningOff, off } /// Bonding state on a device enum BluetoothBondState { none, bonding, bonded } diff --git a/lib/src/flutter_blue_classic_method_channel.dart b/lib/src/flutter_blue_classic_method_channel.dart index 95edc3c..97d1ffc 100644 --- a/lib/src/flutter_blue_classic_method_channel.dart +++ b/lib/src/flutter_blue_classic_method_channel.dart @@ -18,32 +18,39 @@ class MethodChannelFlutterBlueClassic extends FlutterBlueClassicPlatform { /// The event channel used to get updates for the adapter state @visibleForTesting - final EventChannel adapterStateEventChannel = const EventChannel("$namespace/adapterState"); + final EventChannel adapterStateEventChannel = + const EventChannel("$namespace/adapterState"); /// The event channel used to get updates for new discovered devices @visibleForTesting - final EventChannel scanStateEventChannel = const EventChannel("$namespace/discoveryState"); + final EventChannel scanStateEventChannel = + const EventChannel("$namespace/discoveryState"); /// The event channel used to get updates for new discovered devices @visibleForTesting - final EventChannel scanResultEventChannel = const EventChannel("$namespace/scanResults"); + final EventChannel scanResultEventChannel = + const EventChannel("$namespace/scanResults"); @override - Future isSupported() async => await methodChannel.invokeMethod("isSupported") ?? false; + Future isSupported() async => + await methodChannel.invokeMethod("isSupported") ?? false; @override - Future isEnabled() async => await methodChannel.invokeMethod("isEnabled") ?? false; + Future isEnabled() async => + await methodChannel.invokeMethod("isEnabled") ?? false; @override Future adapterStateNow() async { String? state = await methodChannel.invokeMethod("getAdapterState"); - return BluetoothAdapterState.values.firstWhere((e) => e.name == state, orElse: () => BluetoothAdapterState.unknown); + return BluetoothAdapterState.values.firstWhere((e) => e.name == state, + orElse: () => BluetoothAdapterState.unknown); } @override Stream adapterState() { return adapterStateEventChannel.receiveBroadcastStream().map((event) => - BluetoothAdapterState.values.firstWhere((e) => e.name == event, orElse: () => BluetoothAdapterState.unknown)); + BluetoothAdapterState.values.firstWhere((e) => e.name == event, + orElse: () => BluetoothAdapterState.unknown)); } @override @@ -57,15 +64,20 @@ class MethodChannelFlutterBlueClassic extends FlutterBlueClassicPlatform { @override Stream isScanning() { - return scanStateEventChannel.receiveBroadcastStream().map((event) => event == true ? true : false); + return scanStateEventChannel + .receiveBroadcastStream() + .map((event) => event == true ? true : false); } @override - Future isScanningNow() async => await methodChannel.invokeMethod("isScanningNow") ?? false; + Future isScanningNow() async => + await methodChannel.invokeMethod("isScanningNow") ?? false; @override Stream scanResults() { - return scanResultEventChannel.receiveBroadcastStream().map((event) => BluetoothDevice.fromMap(event)); + return scanResultEventChannel + .receiveBroadcastStream() + .map((event) => BluetoothDevice.fromMap(event)); } @override @@ -75,7 +87,8 @@ class MethodChannelFlutterBlueClassic extends FlutterBlueClassicPlatform { @override void startScan(bool usesFineLocation) { - methodChannel.invokeMethod("startScan", {"usesFineLocation": usesFineLocation}); + methodChannel + .invokeMethod("startScan", {"usesFineLocation": usesFineLocation}); } @override @@ -84,13 +97,19 @@ class MethodChannelFlutterBlueClassic extends FlutterBlueClassicPlatform { } @override - Future bondDevice(String address) async => - Platform.isAndroid ? await methodChannel.invokeMethod("bondDevice", {"address": address}) ?? false : false; + Future bondDevice(String address) async => Platform.isAndroid + ? await methodChannel + .invokeMethod("bondDevice", {"address": address}) ?? + false + : false; @override Future connect(String address) async { - int? id = await methodChannel.invokeMethod("connect", {"address": address}); - return id != null ? BluetoothConnection.fromConnectionId(id, address) : null; + int? id = + await methodChannel.invokeMethod("connect", {"address": address}); + return id != null + ? BluetoothConnection.fromConnectionId(id, address) + : null; } @override diff --git a/lib/src/flutter_blue_classic_platform_interface.dart b/lib/src/flutter_blue_classic_platform_interface.dart index ab8c358..433751a 100644 --- a/lib/src/flutter_blue_classic_platform_interface.dart +++ b/lib/src/flutter_blue_classic_platform_interface.dart @@ -13,7 +13,8 @@ abstract class FlutterBlueClassicPlatform extends PlatformInterface { static final Object _token = Object(); - static FlutterBlueClassicPlatform _instance = MethodChannelFlutterBlueClassic(); + static FlutterBlueClassicPlatform _instance = + MethodChannelFlutterBlueClassic(); /// The default instance of [BlueClassicPlatform] to use. /// diff --git a/lib/src/model/bluetooth_connection.dart b/lib/src/model/bluetooth_connection.dart index b1c0513..e0701a4 100644 --- a/lib/src/model/bluetooth_connection.dart +++ b/lib/src/model/bluetooth_connection.dart @@ -10,14 +10,16 @@ import '../flutter_blue_classic_platform_interface.dart'; /// Represents an ongoing Bluetooth connection to a remote device. class BluetoothConnection { BluetoothConnection.fromConnectionId(this._id, this.address) - : _readChannel = EventChannel("${MethodChannelFlutterBlueClassic.namespace}/connection/$_id") { + : _readChannel = EventChannel( + "${MethodChannelFlutterBlueClassic.namespace}/connection/$_id") { _readStreamController = StreamController(); - _readStreamSubscription = _readChannel.receiveBroadcastStream().cast().listen( - _readStreamController.add, - onError: _readStreamController.addError, - onDone: close, - ); + _readStreamSubscription = + _readChannel.receiveBroadcastStream().cast().listen( + _readStreamController.add, + onError: _readStreamController.addError, + onDone: close, + ); input = _readStreamController.stream; output = BluetoothStreamSink(_id); @@ -25,6 +27,8 @@ class BluetoothConnection { /// This ID identifies the real BluetoothConenction object on platform side code. final int _id; + + /// The Bluetooth-Adress of the remote device. final String address; final EventChannel _readChannel; @@ -59,7 +63,9 @@ class BluetoothConnection { return Future.wait([ output.close(), _readStreamSubscription.cancel(), - (!_readStreamController.isClosed) ? _readStreamController.close() : Future.value() + (!_readStreamController.isClosed) + ? _readStreamController.close() + : Future.value() ], eagerError: true); } @@ -71,7 +77,8 @@ class BluetoothConnection { } /// Helper class for sending data. -class BluetoothStreamSink implements EventSink, StreamConsumer { +class BluetoothStreamSink + implements EventSink, StreamConsumer { final int _id; final _instance = FlutterBlueClassicPlatform.instance; @@ -115,7 +122,8 @@ class BluetoothStreamSink implements EventSink, StreamConsumer BluetoothDevice( + BluetoothDevice._( + {required this.address, + this.name = "", + this.alias, + this.type = BluetoothDeviceType.unknown, + this.rssi, + this.bondState = BluetoothBondState.none}); + factory BluetoothDevice.fromMap(Map map) => BluetoothDevice._( name: map["name"], + alias: map["alias"], address: map["address"]!, - bondState: BluetoothBondState.values - .firstWhere((e) => e.name == map["bondState"], orElse: () => BluetoothBondState.none)); + type: BluetoothDeviceType.values.firstWhere( + (e) => e.name == map["deviceType"], + orElse: () => BluetoothDeviceType.unknown), + rssi: map["rssi"], + bondState: BluetoothBondState.values.firstWhere( + (e) => e.name == map["bondState"], + orElse: () => BluetoothBondState.none)); @override operator ==(Object other) { @@ -24,3 +48,5 @@ class BluetoothDevice { @override int get hashCode => address.hashCode; } + +enum BluetoothDeviceType { classic, dual, unknown }