Add API to disable Bluetooth (#113)

* Add api to disable Bluetooth

* Implement Android support

* Fix apple build

* Update Readme

* Combine rows

---------

Co-authored-by: Foti Dim <fdimanidis@gmail.com>
This commit is contained in:
Rohit Sangwan
2024-12-09 21:18:01 +05:30
committed by GitHub
parent 7187768bd0
commit 1292199426
18 changed files with 230 additions and 19 deletions
+4 -1
View File
@@ -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
@@ -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<Long>) -> Unit)
fun enableBluetooth(callback: (Result<Boolean>) -> Unit)
fun disableBluetooth(callback: (Result<Boolean>) -> Unit)
fun startScan(filter: UniversalScanFilter?)
fun stopScan()
fun connect(deviceId: String)
@@ -330,6 +331,24 @@ interface UniversalBlePlatformChannel {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disableBluetooth$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.disableBluetooth{ result: Result<Boolean> ->
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<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan$separatedMessageChannelSuffix", codec)
if (api != null) {
@@ -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<Boolean>) -> Unit)? = null
private var bluetoothDisableRequestFuture: ((Result<Boolean>) -> Unit)? = null
private val discoverServicesFutureList = mutableListOf<DiscoverServicesFuture>()
private val mtuResultFutureList = mutableListOf<MtuResultFuture>()
private val readResultFutureList = mutableListOf<ReadResultFuture>()
@@ -104,6 +113,24 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
bluetoothEnableRequestFuture = callback
}
override fun disableBluetooth(callback: (Result<Boolean>) -> 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
}
+17 -1
View File
@@ -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<Int64, Error>) -> Void)
func enableBluetooth(completion: @escaping (Result<Bool, Error>) -> Void)
func disableBluetooth(completion: @escaping (Result<Bool, Error>) -> 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
+5 -1
View File
@@ -52,7 +52,11 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
func enableBluetooth(completion: @escaping (Result<Bool, Error>) -> Void) {
completion(Result.failure(PigeonError(code: "NotSupported", message: nil, details: nil)))
}
func disableBluetooth(completion: @escaping (Result<Bool, any Error>) -> 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
+5
View File
@@ -116,4 +116,9 @@ class MockUniversalBle extends UniversalBlePlatform {
Future<BleConnectionState> getConnectionState(String deviceId) {
throw UnimplementedError();
}
@override
Future<bool> disableBluetooth() {
throw UnimplementedError();
}
}
+18 -9
View File
@@ -147,15 +147,24 @@ class _MyAppState extends State<MyApp> {
});
},
),
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',
+9
View File
@@ -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<bool> 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.
@@ -53,6 +53,23 @@ class UniversalBleLinux extends UniversalBlePlatform {
}
}
@override
Future<bool> 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<void> startScan({
ScanFilter? scanFilter,
@@ -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<bool> disableBluetooth() async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disableBluetooth$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_channel.send(null) as List<Object?>?;
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<void> startScan(UniversalScanFilter? filter) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
@@ -28,6 +28,14 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform {
return _channel.enableBluetooth();
}
@override
Future<bool> disableBluetooth() {
if (!BleCapabilities.supportsBluetoothEnableApi) {
throw UnsupportedError("Not supported");
}
return _channel.disableBluetooth();
}
@override
Future<void> startScan({
ScanFilter? scanFilter,
@@ -12,6 +12,8 @@ abstract class UniversalBlePlatform {
Future<bool> enableBluetooth();
Future<bool> disableBluetooth();
Future<void> startScan({
ScanFilter? scanFilter,
PlatformConfig? platformConfig,
@@ -335,6 +335,11 @@ class UniversalBleWeb extends UniversalBlePlatform {
throw UnimplementedError();
}
@override
Future<bool> disableBluetooth() {
throw UnimplementedError();
}
RequestOptionsBuilder _getRequestOptionBuilder(
ScanFilter? scanFilter,
WebOptions? webOptions,
+3
View File
@@ -27,6 +27,9 @@ abstract class UniversalBlePlatformChannel {
@async
bool enableBluetooth();
@async
bool disableBluetooth();
void startScan(UniversalScanFilter? filter);
void stopScan();
+23 -1
View File
@@ -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<EncodableValue>& reply) {
try {
api->DisableBluetooth([reply](ErrorOr<bool>&& 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) {
+2 -1
View File
@@ -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<void(ErrorOr<int64_t> reply)> result) = 0;
virtual void EnableBluetooth(std::function<void(ErrorOr<bool> reply)> result) = 0;
virtual void DisableBluetooth(std::function<void(ErrorOr<bool> reply)> result) = 0;
virtual std::optional<FlutterError> StartScan(const UniversalScanFilter* filter) = 0;
virtual std::optional<FlutterError> StopScan() = 0;
virtual std::optional<FlutterError> Connect(const std::string& device_id) = 0;
+28
View File
@@ -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<void(ErrorOr<bool> 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<RadioAccessStatus> const &sender, AsyncStatus const args)
{
auto radioAccessStatus = sender.GetResults();
if (radioAccessStatus == RadioAccessStatus::Allowed)
{
result(true);
}
else
{
result(FlutterError("Failed to disable bluetooth"));
} });
}
std::optional<FlutterError> UniversalBlePlugin::StartScan(const UniversalScanFilter *filter)
{
if (bluetoothRadio && bluetoothRadio.State() == RadioState::On)
+1
View File
@@ -131,6 +131,7 @@ namespace universal_ble
// UniversalBlePlatformChannel implementation.
void GetBluetoothAvailabilityState(std::function<void(ErrorOr<int64_t> reply)> result) override;
void EnableBluetooth(std::function<void(ErrorOr<bool> reply)> result) override;
void DisableBluetooth(std::function<void(ErrorOr<bool> reply)> result) override;
ErrorOr<int64_t> GetConnectionState(const std::string &device_id) override;
std::optional<FlutterError> StartScan(const UniversalScanFilter *filter) override;
std::optional<FlutterError> StopScan() override;