diff --git a/packages/quick_blue/android/src/main/kotlin/com/example/quick_blue/QuickBluePlugin.kt b/packages/quick_blue/android/src/main/kotlin/com/example/quick_blue/QuickBluePlugin.kt index db2f868..b08b642 100644 --- a/packages/quick_blue/android/src/main/kotlin/com/example/quick_blue/QuickBluePlugin.kt +++ b/packages/quick_blue/android/src/main/kotlin/com/example/quick_blue/QuickBluePlugin.kt @@ -17,6 +17,10 @@ import io.flutter.plugin.common.MethodChannel.Result import java.nio.ByteBuffer import java.nio.ByteOrder import java.util.* +import android.content.Intent +import android.content.IntentFilter +import android.content.BroadcastReceiver +import android.bluetooth.BluetoothDevice.* private const val TAG = "QuickBluePlugin" @@ -42,6 +46,7 @@ class QuickBluePlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHand context = flutterPluginBinding.applicationContext mainThreadHandler = Handler(Looper.getMainLooper()) bluetoothManager = flutterPluginBinding.applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + listenToBondStateChanges(context) } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { @@ -74,6 +79,15 @@ class QuickBluePlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHand bluetoothManager.adapter.bluetoothLeScanner?.stopScan(scanCallback) result.success(null) } + "pair" -> { + val deviceId = call.argument("deviceId")!! + val remoteDevice : BluetoothDevice = bluetoothManager.adapter.getRemoteDevice(deviceId) + var requiresPairing = remoteDevice.bondState == BOND_NONE + if(requiresPairing){ + remoteDevice.createBond() + } + result.success(null) + } "connect" -> { val deviceId = call.argument("deviceId")!! if (knownGatts.find { it.device.address == deviceId } != null) { @@ -159,6 +173,35 @@ class QuickBluePlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHand } } + fun listenToBondStateChanges(context: Context) { + context.applicationContext.registerReceiver( + broadcastReceiver, + IntentFilter(ACTION_BOND_STATE_CHANGED) + ) + } + + private val broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + with(intent) { + if (action == ACTION_BOND_STATE_CHANGED) { + val device = getParcelableExtra(EXTRA_DEVICE) + val previousBondState = getIntExtra(EXTRA_PREVIOUS_BOND_STATE, -1) + val bondState = getIntExtra(EXTRA_BOND_STATE, -1) + val bondTransition = "${previousBondState.toBondStateDescription()} to " + + bondState.toBondStateDescription() + Log.v(TAG, "${device?.address} Pair state changed | $bondTransition") + } + } + } + + private fun Int.toBondStateDescription() = when(this) { + BOND_BONDED -> "Paired" + BOND_BONDING -> "Pairing" + BOND_NONE -> "Not Paired" + else -> "ERROR: $this" + } + } + private fun cleanConnection(gatt: BluetoothGatt) { knownGatts.remove(gatt) gatt.disconnect() diff --git a/packages/quick_blue/example/lib/peripheral_detail_page.dart b/packages/quick_blue/example/lib/peripheral_detail_page.dart index 091c441..71d3339 100644 --- a/packages/quick_blue/example/lib/peripheral_detail_page.dart +++ b/packages/quick_blue/example/lib/peripheral_detail_page.dart @@ -76,6 +76,7 @@ class _PeripheralDetailPageState extends State { ), body: Column( children: [ + const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ @@ -85,6 +86,12 @@ class _PeripheralDetailPageState extends State { QuickBlue.connect(widget.deviceId); }, ), + ElevatedButton( + child: const Text('pair'), + onPressed: () { + QuickBlue.pair(widget.deviceId); + }, + ), ElevatedButton( child: const Text('disconnect'), onPressed: () { @@ -93,6 +100,7 @@ class _PeripheralDetailPageState extends State { ), ], ), + const Divider(), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ @@ -102,9 +110,7 @@ class _PeripheralDetailPageState extends State { QuickBlue.discoverServices(widget.deviceId); }, ), - ], - ), - ElevatedButton( + ElevatedButton( child: const Text('setNotifiable'), onPressed: () { QuickBlue.setNotifiable( @@ -112,6 +118,8 @@ class _PeripheralDetailPageState extends State { BleInputProperty.indication); }, ), + ], + ), TextField( controller: serviceUUID, decoration: const InputDecoration( diff --git a/packages/quick_blue/lib/quick_blue.dart b/packages/quick_blue/lib/quick_blue.dart index 4bb5139..972ce11 100644 --- a/packages/quick_blue/lib/quick_blue.dart +++ b/packages/quick_blue/lib/quick_blue.dart @@ -33,6 +33,8 @@ class QuickBlue { static void connect(String deviceId) => _platform.connect(deviceId); + static void pair(String deviceId) => _platform.pair(deviceId); + static void disconnect(String deviceId) => _platform.disconnect(deviceId); static void setConnectionHandler(OnConnectionChanged? onConnectionChanged) { diff --git a/packages/quick_blue/lib/src/method_channel_quick_blue.dart b/packages/quick_blue/lib/src/method_channel_quick_blue.dart index 87d3ef1..51fcf53 100644 --- a/packages/quick_blue/lib/src/method_channel_quick_blue.dart +++ b/packages/quick_blue/lib/src/method_channel_quick_blue.dart @@ -1,6 +1,6 @@ import 'dart:async'; import 'dart:typed_data'; - +import 'dart:io'; import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; @@ -140,4 +140,13 @@ class MethodChannelQuickBlue extends QuickBluePlatform { }).then((_) => _log('requestMtu invokeMethod success')); return await _mtuConfigController.stream.first; } -} + + @override + void pair(String deviceId) { + bool supportedPlatforms = Platform.isAndroid || Platform.isWindows; + if (!supportedPlatforms) throw UnimplementedError(); + _method.invokeMethod('pair', { + 'deviceId': deviceId, + }).then((_) => _log('pair invokeMethod success')); + } +} diff --git a/packages/quick_blue/lib/src/quick_blue_linux.dart b/packages/quick_blue/lib/src/quick_blue_linux.dart index 3cf1211..146227a 100644 --- a/packages/quick_blue/lib/src/quick_blue_linux.dart +++ b/packages/quick_blue/lib/src/quick_blue_linux.dart @@ -187,6 +187,11 @@ class QuickBlueLinux extends QuickBluePlatform { // TODO: implement requestMtu throw UnimplementedError(); } + + @override + void pair(String deviceId) { + // TODO: implement pair + } } extension BlueZDeviceExtension on BlueZDevice { diff --git a/packages/quick_blue/lib/src/quick_blue_platform_interface.dart b/packages/quick_blue/lib/src/quick_blue_platform_interface.dart index 7f1f5a3..f382c87 100644 --- a/packages/quick_blue/lib/src/quick_blue_platform_interface.dart +++ b/packages/quick_blue/lib/src/quick_blue_platform_interface.dart @@ -43,6 +43,8 @@ abstract class QuickBluePlatform extends PlatformInterface { void connect(String deviceId); + void pair(String deviceId); + void disconnect(String deviceId); OnConnectionChanged? onConnectionChanged; diff --git a/packages/quick_blue/windows/quick_blue_plugin.cpp b/packages/quick_blue/windows/quick_blue_plugin.cpp index d8a0791..a4f8d8d 100644 --- a/packages/quick_blue/windows/quick_blue_plugin.cpp +++ b/packages/quick_blue/windows/quick_blue_plugin.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -36,6 +37,7 @@ using namespace winrt::Windows::Devices::Radios; using namespace winrt::Windows::Devices::Bluetooth; using namespace winrt::Windows::Devices::Bluetooth::Advertisement; using namespace winrt::Windows::Devices::Bluetooth::GenericAttributeProfile; +using namespace winrt::Windows::Devices::Enumeration; using flutter::EncodableValue; using flutter::EncodableMap; @@ -152,6 +154,8 @@ class QuickBluePlugin : public flutter::Plugin, public flutter::StreamHandler> connectedDevices{}; winrt::fire_and_forget ConnectAsync(uint64_t bluetoothAddress); + winrt::fire_and_forget PairAsync(uint64_t bluetoothAddress, DevicePairingProtectionLevel level); + void BluetoothLEDevice_PairingRequested(DeviceInformationCustomPairing sender, DevicePairingRequestedEventArgs args); void BluetoothLEDevice_ConnectionStatusChanged(BluetoothLEDevice sender, IInspectable args); void CleanConnection(uint64_t bluetoothAddress); winrt::fire_and_forget DiscoverServicesAsync(BluetoothDeviceAgent &bluetoothDeviceAgent); @@ -245,6 +249,11 @@ void QuickBluePlugin::HandleMethodCall( } else { result->Error("IllegalState", "Bluetooth unavailable"); } + }else if (method_name.compare("pair") == 0) { + auto args = std::get(*method_call.arguments()); + auto deviceId = std::get(args[EncodableValue("deviceId")]); + PairAsync(std::stoull(deviceId), DevicePairingProtectionLevel::Encryption); + result->Success(nullptr); } else if (method_name.compare("connect") == 0) { auto args = std::get(*method_call.arguments()); auto deviceId = std::get(args[EncodableValue("deviceId")]); @@ -444,6 +453,28 @@ winrt::fire_and_forget QuickBluePlugin::ConnectAsync(uint64_t bluetoothAddress) }); } +winrt::fire_and_forget QuickBluePlugin::PairAsync(uint64_t bluetoothAddress, DevicePairingProtectionLevel level) { + BluetoothLEDevice device = co_await BluetoothLEDevice::FromBluetoothAddressAsync(bluetoothAddress); + if(device != nullptr) { + if(device.DeviceInformation().Pairing().CanPair()) { + try { + device.DeviceInformation().Pairing().Custom().PairingRequested({ this, &QuickBluePlugin::BluetoothLEDevice_PairingRequested }); + auto result = co_await device.DeviceInformation().Pairing().Custom().PairAsync(DevicePairingKinds::ConfirmOnly, DevicePairingProtectionLevel::Encryption); + if(result.Status() != DevicePairingResultStatus::Paired && result.Status() != DevicePairingResultStatus::AlreadyPaired) { + OutputDebugString((L"PairAsync error: " + winrt::to_hstring((int32_t)result.Status()) + L"\n").c_str()); + } + } catch(winrt::hresult_error const& ex) { + OutputDebugString((L"PairAsync " + ex.message() + L"\n").c_str()); + } + } + } + co_return; +} + +void QuickBluePlugin::BluetoothLEDevice_PairingRequested(DeviceInformationCustomPairing sender, DevicePairingRequestedEventArgs args) { + args.Accept(); +} + void QuickBluePlugin::BluetoothLEDevice_ConnectionStatusChanged(BluetoothLEDevice sender, IInspectable args) { OutputDebugString((L"ConnectionStatusChanged " + winrt::to_hstring((int32_t)sender.ConnectionStatus()) + L"\n").c_str()); if (sender.ConnectionStatus() == BluetoothConnectionStatus::Disconnected) {