diff --git a/README.md b/README.md index b7882de..b304dc3 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE | isPaired | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | onPairingStateChange | ✔️ | ⏺ | ⏺ | ✔️ | ✔️ | ⏺ | | getBluetoothAvailabilityState | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | -| enableBluetooth | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ | +| enable/disable Bluetooth | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ | | onAvailabilityChange | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | requestMtu | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | @@ -229,6 +229,9 @@ UniversalBle.onAvailabilityChange = (state) { // Enable Bluetooth programmatically UniversalBle.enableBluetooth(); + +// Disable Bluetooth programmatically +UniversalBle.disableBluetooth(); ``` ## Command Queue diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt index dfcf243..6d6a34f 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v22.4.0), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -270,6 +270,7 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() { interface UniversalBlePlatformChannel { fun getBluetoothAvailabilityState(callback: (Result) -> Unit) fun enableBluetooth(callback: (Result) -> Unit) + fun disableBluetooth(callback: (Result) -> Unit) fun startScan(filter: UniversalScanFilter?) fun stopScan() fun connect(deviceId: String) @@ -330,6 +331,24 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disableBluetooth$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.disableBluetooth{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan$separatedMessageChannelSuffix", codec) if (api != null) { diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt index 1244fd2..322ce95 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -2,9 +2,16 @@ package com.navideck.universal_ble import android.annotation.SuppressLint import android.app.Activity -import android.bluetooth.* +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice.BOND_BONDED -import android.bluetooth.le.BluetoothLeScanner +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothManager +import android.bluetooth.BluetoothProfile +import android.bluetooth.BluetoothStatusCodes import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanFilter import android.bluetooth.le.ScanResult @@ -21,7 +28,7 @@ import android.util.Log import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.flutter.plugin.common.* +import io.flutter.plugin.common.PluginRegistry import java.util.UUID import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -33,6 +40,7 @@ private const val TAG = "UniversalBlePlugin" class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), FlutterPlugin, ActivityAware, PluginRegistry.ActivityResultListener { private val bluetoothEnableRequestCode = 2342313 + private val bluetoothDisableRequestCode = 2342414 private var callbackChannel: UniversalBleCallbackChannel? = null private var mainThreadHandler: Handler? = null private lateinit var context: Context @@ -45,6 +53,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), // Flutter Futures private var bluetoothEnableRequestFuture: ((Result) -> Unit)? = null + private var bluetoothDisableRequestFuture: ((Result) -> Unit)? = null private val discoverServicesFutureList = mutableListOf() private val mtuResultFutureList = mutableListOf() private val readResultFutureList = mutableListOf() @@ -104,6 +113,24 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), bluetoothEnableRequestFuture = callback } + override fun disableBluetooth(callback: (Result) -> Unit) { + if (!bluetoothManager.adapter.isEnabled) { + callback(Result.success(true)) + return + } + if (bluetoothDisableRequestFuture != null) { + callback( + Result.failure( + FlutterError("Failed", "Bluetooth disable request in progress", null) + ) + ) + return + } + val disableBtIntent = Intent("android.bluetooth.adapter.action.REQUEST_DISABLE") + activity?.startActivityForResult(disableBtIntent, bluetoothDisableRequestCode) + bluetoothDisableRequestFuture = callback + } + override fun startScan(filter: UniversalScanFilter?) { if (!isBluetoothAvailable()) throw FlutterError( "BluetoothNotEnabled", @@ -995,6 +1022,11 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), future(Result.success(resultCode == Activity.RESULT_OK)) bluetoothEnableRequestFuture = null return true + } else if (requestCode == bluetoothDisableRequestCode) { + val future = bluetoothDisableRequestFuture ?: return false + future(Result.success(resultCode == Activity.RESULT_OK)) + bluetoothDisableRequestFuture = null + return true } return false } diff --git a/darwin/Classes/UniversalBle.g.swift b/darwin/Classes/UniversalBle.g.swift index 3bacd2f..eb5424e 100644 --- a/darwin/Classes/UniversalBle.g.swift +++ b/darwin/Classes/UniversalBle.g.swift @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v22.4.0), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -312,6 +312,7 @@ class UniversalBlePigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable protocol UniversalBlePlatformChannel { func getBluetoothAvailabilityState(completion: @escaping (Result) -> Void) func enableBluetooth(completion: @escaping (Result) -> Void) + func disableBluetooth(completion: @escaping (Result) -> Void) func startScan(filter: UniversalScanFilter?) throws func stopScan() throws func connect(deviceId: String) throws @@ -364,6 +365,21 @@ class UniversalBlePlatformChannelSetup { } else { enableBluetoothChannel.setMessageHandler(nil) } + let disableBluetoothChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disableBluetooth\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + disableBluetoothChannel.setMessageHandler { _, reply in + api.disableBluetooth { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + disableBluetoothChannel.setMessageHandler(nil) + } let startScanChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { startScanChannel.setMessageHandler { message, reply in diff --git a/darwin/Classes/UniversalBlePlugin.swift b/darwin/Classes/UniversalBlePlugin.swift index f9d80da..eb85975 100644 --- a/darwin/Classes/UniversalBlePlugin.swift +++ b/darwin/Classes/UniversalBlePlugin.swift @@ -52,7 +52,11 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral func enableBluetooth(completion: @escaping (Result) -> Void) { completion(Result.failure(PigeonError(code: "NotSupported", message: nil, details: nil))) } - + + func disableBluetooth(completion: @escaping (Result) -> Void) { + completion(Result.failure(PigeonError(code: "NotSupported", message: nil, details: nil))) + } + func startScan(filter: UniversalScanFilter?) throws { // If filter has any other filter other than official one let usesCustomFilters = filter?.usesCustomFilters ?? false diff --git a/example/lib/data/mock_universal_ble.dart b/example/lib/data/mock_universal_ble.dart index b08b493..233e8c6 100644 --- a/example/lib/data/mock_universal_ble.dart +++ b/example/lib/data/mock_universal_ble.dart @@ -116,4 +116,9 @@ class MockUniversalBle extends UniversalBlePlatform { Future getConnectionState(String deviceId) { throw UnimplementedError(); } + + @override + Future disableBluetooth() { + throw UnimplementedError(); + } } diff --git a/example/lib/home/home.dart b/example/lib/home/home.dart index 95c1410..16496e5 100644 --- a/example/lib/home/home.dart +++ b/example/lib/home/home.dart @@ -147,15 +147,24 @@ class _MyAppState extends State { }); }, ), - if (BleCapabilities.supportsBluetoothEnableApi && - bleAvailabilityState == AvailabilityState.poweredOff) - PlatformButton( - text: 'Enable Bluetooth', - onPressed: () async { - bool isEnabled = await UniversalBle.enableBluetooth(); - showSnackbar("BluetoothEnabled: $isEnabled"); - }, - ), + if (BleCapabilities.supportsBluetoothEnableApi) + bleAvailabilityState != AvailabilityState.poweredOn + ? PlatformButton( + text: 'Enable Bluetooth', + onPressed: () async { + bool isEnabled = + await UniversalBle.enableBluetooth(); + showSnackbar("BluetoothEnabled: $isEnabled"); + }, + ) + : PlatformButton( + text: 'Disable Bluetooth', + onPressed: () async { + bool isDisabled = + await UniversalBle.disableBluetooth(); + showSnackbar("BluetoothDisabled: $isDisabled"); + }, + ), if (BleCapabilities.requiresRuntimePermission) PlatformButton( text: 'Check Permissions', diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart index a8a75da..f4aff22 100644 --- a/lib/src/universal_ble.dart +++ b/lib/src/universal_ble.dart @@ -321,6 +321,15 @@ class UniversalBle { ); } + /// Disable Bluetooth. + /// It might throw errors if Bluetooth is not available. + /// Not supported on `Web` and `Apple`. + static Future disableBluetooth() async { + return await _bleCommandQueue.queueCommand( + () => _platform.disableBluetooth(), + ); + } + /// [receivesAdvertisements] returns true on web if the browser supports receiving advertisements from a certain `deviceId`. /// The rest of the platforms will always return true. /// If true, then you will be getting scanResult updates for this device. diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index a194aac..8e6c44e 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -53,6 +53,23 @@ class UniversalBleLinux extends UniversalBlePlatform { } } + @override + Future disableBluetooth() async { + await _ensureInitialized(); + var adapter = _activeAdapter; + if (adapter == null) { + throw "Adapter not available"; + } + if (!adapter.powered) return true; + try { + await adapter.setPowered(false); + return !adapter.powered; + } catch (e) { + UniversalLogger.logError('Error disabling bluetooth: $e'); + return false; + } + } + @override Future startScan({ ScanFilter? scanFilter, diff --git a/lib/src/universal_ble_pigeon/universal_ble.g.dart b/lib/src/universal_ble_pigeon/universal_ble.g.dart index b50e57e..ae3484f 100644 --- a/lib/src/universal_ble_pigeon/universal_ble.g.dart +++ b/lib/src/universal_ble_pigeon/universal_ble.g.dart @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v22.4.0), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -332,6 +332,33 @@ class UniversalBlePlatformChannel { } } + Future disableBluetooth() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disableBluetooth$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + Future startScan(UniversalScanFilter? filter) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( diff --git a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart index 0d7f964..6d82573 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -28,6 +28,14 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { return _channel.enableBluetooth(); } + @override + Future disableBluetooth() { + if (!BleCapabilities.supportsBluetoothEnableApi) { + throw UnsupportedError("Not supported"); + } + return _channel.disableBluetooth(); + } + @override Future startScan({ ScanFilter? scanFilter, diff --git a/lib/src/universal_ble_platform_interface.dart b/lib/src/universal_ble_platform_interface.dart index cf55bab..30ace50 100644 --- a/lib/src/universal_ble_platform_interface.dart +++ b/lib/src/universal_ble_platform_interface.dart @@ -12,6 +12,8 @@ abstract class UniversalBlePlatform { Future enableBluetooth(); + Future disableBluetooth(); + Future startScan({ ScanFilter? scanFilter, PlatformConfig? platformConfig, diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index b4fd3ae..42a5733 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -335,6 +335,11 @@ class UniversalBleWeb extends UniversalBlePlatform { throw UnimplementedError(); } + @override + Future disableBluetooth() { + throw UnimplementedError(); + } + RequestOptionsBuilder _getRequestOptionBuilder( ScanFilter? scanFilter, WebOptions? webOptions, diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart index 224865d..f6f8d8b 100644 --- a/pigeon/universal_ble.dart +++ b/pigeon/universal_ble.dart @@ -27,6 +27,9 @@ abstract class UniversalBlePlatformChannel { @async bool enableBluetooth(); + @async + bool disableBluetooth(); + void startScan(UniversalScanFilter? filter); void stopScan(); diff --git a/windows/src/generated/universal_ble.g.cpp b/windows/src/generated/universal_ble.g.cpp index 810b7af..46ed503 100644 --- a/windows/src/generated/universal_ble.g.cpp +++ b/windows/src/generated/universal_ble.g.cpp @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v22.4.0), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon #undef _HAS_EXCEPTIONS @@ -546,6 +546,28 @@ void UniversalBlePlatformChannel::SetUp( channel.SetMessageHandler(nullptr); } } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disableBluetooth" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->DisableBluetooth([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } { BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan" + prepended_suffix, &GetCodec()); if (api != nullptr) { diff --git a/windows/src/generated/universal_ble.g.h b/windows/src/generated/universal_ble.g.h index 1fe32e0..2a22a7c 100644 --- a/windows/src/generated/universal_ble.g.h +++ b/windows/src/generated/universal_ble.g.h @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v22.4.0), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon #ifndef PIGEON_UNIVERSAL_BLE_G_H_ @@ -298,6 +298,7 @@ class UniversalBlePlatformChannel { virtual ~UniversalBlePlatformChannel() {} virtual void GetBluetoothAvailabilityState(std::function reply)> result) = 0; virtual void EnableBluetooth(std::function reply)> result) = 0; + virtual void DisableBluetooth(std::function reply)> result) = 0; virtual std::optional StartScan(const UniversalScanFilter* filter) = 0; virtual std::optional StopScan() = 0; virtual std::optional Connect(const std::string& device_id) = 0; diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index 12fc011..c7ad695 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -79,6 +79,7 @@ namespace universal_ble if (!bluetoothRadio) { result(FlutterError("Bluetooth is not available")); + result(false); return; } if (bluetoothRadio.State() == RadioState::On) @@ -100,6 +101,33 @@ namespace universal_ble } }); } + void UniversalBlePlugin::DisableBluetooth(std::function reply)> result) + { + if (!bluetoothRadio) + { + result(FlutterError("Bluetooth is not available")); + result(false); + return; + } + if (bluetoothRadio.State() == RadioState::Off) + { + result(true); + return; + } + auto async_c = bluetoothRadio.SetStateAsync(RadioState::Off); + async_c.Completed([&, result](IAsyncOperation const &sender, AsyncStatus const args) + { + auto radioAccessStatus = sender.GetResults(); + if (radioAccessStatus == RadioAccessStatus::Allowed) + { + result(true); + } + else + { + result(FlutterError("Failed to disable bluetooth")); + } }); + } + std::optional UniversalBlePlugin::StartScan(const UniversalScanFilter *filter) { if (bluetoothRadio && bluetoothRadio.State() == RadioState::On) diff --git a/windows/src/universal_ble_plugin.h b/windows/src/universal_ble_plugin.h index b0f9251..3295948 100644 --- a/windows/src/universal_ble_plugin.h +++ b/windows/src/universal_ble_plugin.h @@ -131,6 +131,7 @@ namespace universal_ble // UniversalBlePlatformChannel implementation. void GetBluetoothAvailabilityState(std::function reply)> result) override; void EnableBluetooth(std::function reply)> result) override; + void DisableBluetooth(std::function reply)> result) override; ErrorOr GetConnectionState(const std::string &device_id) override; std::optional StartScan(const UniversalScanFilter *filter) override; std::optional StopScan() override;